从官方源码中解析vue3的响应式原理
前言
学习了前端这么多年,也看许多大大小小的文章,在vue响应式这一块我觉得自己也有了一些理解。之前有几次想要去看vue官方的源码,但无一例外,我都看不懂🤔。 之后我经常会看一些博客,也会学习一些mini-vue的项目,但我总感觉有哪些地方不太对劲。
比如有些博客会说各种各样抽象的名词,什么订阅者watcher啦,什么观察者Observer啦,然后还会有很详细的图来表示它们的关系,这样我们当然可以去理解部分vue的原理,但我个人觉得这些都是非常抽象层面,脱离代码层面以一种抽象的方式讲解,并且正所谓一千个读者就有一千个哈姆雷特,每个人对于它的理解不同,所以我们从这些博客中理解到的极大程度的依赖与写这篇文章的人(maybe那个人还是看了别人的博客写的),而自己最近代码能力有些许提升,于是就又跑去翻了翻vue的源码,发现竟然可以大概看懂一部分了,所以我想写一篇从源码里去带大家分析响应式原理的文章。
除此之外,通过本篇文章你可以收获一份入门源码的信心,也就是说,你可以通过本片文章所讲的内容作为入口去更深入学习vue的源码
ps:以下内容我都会贴上vue中官方仓库的源码片段,并表明我们需要关注的函数或者变量以及源码所在文件夹和行数,以及github地址
ps:以下贴图的代码不需要大家仔细研究,只需要关注我画红圈或者蓝圈的地方以及所在函数的位置即可,重点关注解读部分
- vue版本: 3.2.45
- 仓库地址: github.com/vuejs/core
仓库结构
在解析源码之前,我们首先要大概讲一下代码结构
路径:packages/
我们可以看到,packages里面有非常多的包
compiler-*:编译相关的包(可忽略)
reactivity-*:响应式相关的包(核心为core)
runtime-*:运行时相关的包(核心为core)
不要怕,我们只需要重点关注两个包即可,即reactivity-core
、runtime-core
前置内容
在解析之前我们还需要说一些前置知识,vue运行的的响应式是以组件为单位去触发的。就是说,在一个组件内的响应式变量一般
都是只影响当前组件的更新渲染的,而组件的更新,则是通过生成虚拟dom,通过diff算法去比对虚拟dom,然后再去更新。
为什么这样做呢?大家可以去想一想,高精度的控制(以单个dom为单位),意味着我们更新的的速度越快,更新越精确,但会损耗大量内存资源占用;而如果是以整个应用为单位更新,那恐怕页面会卡的不行或者卡死。所以组件正好就是一个折中的方案。
针对这一块你可以单独去详细了解,推荐去看一下《深入浅出vue》
这本书
源码解读
入口
接下来我不会直接从响应式部分入手,而是通过第一次渲染的流程,来逐步引入大家去了解响应式原理,因为这样有助于大家对vue的全貌有一个基本认识,不是干巴巴的只了解响应式部分。对于入口我不会做大量解说,只是通过入口引入到我们的响应式部分。
路径:packages/runtime-core/src/apiCreateApp.ts
function:createAppAPI -> createApp
行数:326
解读: 这个createAppApi
会被runtime-dom
这个包引入然后再导出我们经常使用的那个方法,当然不用关注这个。我们可以看到在我们调用mount
方法后,vue会先创建一个虚拟dom Vnode
,然后会调用render
函数渲染这个虚拟dom,那么响应式就是在就是在render
里面去绑定的啦。
ps:mount
方法会在runtime-dom
中被重写,因为在创建vue App应用的时候需要一些额外的操作,不需要关注
render函数
在render函数中其实有很多的子方法,比如mountChildren、patchElement等等,这些其实与diff有很多的关系,但我们不关注这个,我们只需要关注一个函数,那就是mountComponent
。
ps:我去整个render函数2000行,吐槽一下。。。😂
寻找组件渲染的函数
路径:packages/runtime-core/src/renderer.ts
function:baseCreateRenderer -> mountComponent
行数:1218
解读: 在mountComponet
中我们可以看到,首先根据虚拟dom生成一个组件实例,这个阶段组件里面是空空如也的,也没有进行渲染。
接下来我们关注一个函数,setupComponent
,这个阶段可以说是对组件进行初始化,在初始化完后肯定要进行渲染啦。
那么就该setupRenderEffect
函数登场啦,这个函数可以说是组件响应式绑定的核心了,在这个函数里会引入响应式部分的函数,并且在对组件第一次渲染的时候进行响应式绑定。不着急,让我们一点一点来看。
寻找组件渲染时绑定响应式变量的函数
路径:packages/runtime-core/src/renderer.ts
function:setupRenderEffect
行数:1541
解读: 组件更新的函数,这个函数里面会调用我们自己写的组件的render
函数,并返回虚拟dom,然后渲染到页面上
解读:这一部分的代码就是核心中的核心了。首先我们可以看到,它new了一个ReactiveEffect
,熟悉effect
的小伙伴可能知道,effecf
是一个副作用函数,可以绑定响应式变量的,我们先不看这个类怎么实现的,我先说这个ReactiveEffect
所生成的实例的作用。
ReactiveEffect
需要我们传入一个内部带有响应式变量(我们在自己的组件中写的那些响应式变量)的函数,第二个参数是响应式变量更新后所调用的函数,生成实例后并不会立即执行第一个参数传入的函数。而生成的effect
实例,会有一个run
方法,这个run
方法才会真正执行第一个参数传入的参数,也就是内部有响应式变量的函数。
当我们调用run的时候,会发生什么?
我们知道调用run
,就会访问到响应式变量,进而触发响应式变量的get
函数。我们想一想,响应式变量更新,它怎么就知道要调用哪个函数呢?唯一的答案就是,在我们调用run
之前,先把某个我们要更新的函数挂载到一个全局变量上,然后调用run
,触发get
,get
函数内部去找这个全局变量,把这个更新函数保存到这个响应式变量可以找到的地方,然后响应式变量更新,触发set
,进而找到这个更新函数,再调用这个函数触发更新即可。
这么一说,整条链路是不是就打通了?但我目前讲的响应式部分还是抽象的,接下来我会带你具体看一下,vue的响应式部分是怎么实现的。
响应式部分源码解读
看到这里,我们终于可以进入正题了,最起码你已经知道,组件在渲染的时候是怎么绑定响应式的了,也知道组件怎么通过改变响应式变量更新的了。
effect副作用
上面我们看到了一个类ReactiveEffect
,我们来看看这个类是怎么实现的。
ReactiveEffect
路径:packages/reactivity/src/effect.ts
function:ReactiveEffect
行数:100
解读:首先是这个类的传参,这是我们上面所提到的,这个fn就是上面componentUpdatFn
ps:在看这段代码,你只需要关注三个部分,第一个run这个函数的位置,其他两个我圈住的部分,其余的代码不需要关注。
解读:这个run
方法就是上面我们提到的,在调用run
的时候会把自己挂载到activeEffect
上面,这个activeEffect
是什么呢
没错就是我上面提到的全局变量,怎么样我没骗你吧。
挂载之后才会触发fn,进而触发响应式变量。那么接下来,我们是不是就应该去看一看,响应式变量的get是怎么写的了,是不是在get里面去找到这个activeEffect呢
响应式变量的get函数
路径:packages/reactivity/src/reactive.ts
function:createReactiveObject
行数:98 and 214
响应式变量声明的方式主要有两种,reactive
和ref
,我们主要看一看reactive
解读:reactive
会调用一个creaReactiveObject
函数去创建响应式对象
解读:es6的代理大家应该很熟悉了,第一个是我们需要监控的目标对象,第二个是一个handler
对象,里面有get
和set
函数
解读:而handler
函数是通过mutableHhandles
传进来的,让我们看看mutableHhandles
这个对象。
路径:packages/reactivity/src/baseHandlers.ts
function:createGetter -> get
行数:94 and 135
解读:我们可以看到mutableHhandles
的get
函数是通过createGetter
创建的,而在createGetter
中,我们终于可以看到get
函数的全貌的,get
函数里面有很多判断,我们都不需要关注,我们只需要关注一个地方,track
,顾名思义,跟踪就是指我们要收集这个响应式变量的依赖,而在这个track
中又做了哪些事呢?
路径:packages/reactivity/src/effect.ts
function:track
行数:215 and 228
解读:可以看到,在track
函数中真的引用了activeEffect
这个变量,还记得我们上面说的,在触发响应式变量之前,会在run
方法中把自己挂载到这个activeEffect
变量上吗。
还有一个地方就是,targetMap
,又是什么东西呢?,事实上targetMap
也是一个全局变量,它放的是一个Map对象
,这个Map
保存原始target对象
以及对应的Dep Map对象
,即(target,depsMap)
。
那这个dep又是什么玩意呢?
我们知道一个对象有很多属性,不同的属性肯定要有所区分,所以depsMap
保存的就是对象的不同属性和这个属性所有的更新函数了。
你可以通俗理解为,这个targetMap
就是存放响应式变量和它的更新函数(effect)的地方,可以让响应式变量找到它到底有哪些依赖它的effect
函数,从而在更新时调用。
虽然在track
函数中引用了activeEffect
,但只是做了一个判断而已,我来看看trackEffects
这个函数
function:trackEffects
行数:248
解读:可以看到,在trackEffects
这个函数中才真正把activeEffect
添加到这个响应式变量对应的dep
中,以便下次这个响应式变量可以找到这个effect
函数。
总结
至此,我们总算打通了响应式变量收集依赖的过程,核心函数就是这个track
了。我们接下来再看看响应式变量更新的过程。
响应式变量的set函数
通过上面的get
函数的路径我们就可以找到它的set
函数,所以重复的地方我就不截图了
路径:packages/reactivity/src/baseHandlers.ts
function:createSetter -> set
行数:193
解读:可以看到,在set
函数中,我们要关注一个函数,trigger
函数,顾名思义,触发,也就是触发响应式变量所有的effect
的调度函数,就是我们最开始在ReactiveEffect
传入的第二个函数,已经忘了的小伙伴可以向上翻一翻再看一下。
我们来看看trigger
函数里面做了哪些事
路径:packages/reactivity/src/effect.ts
function:trigger
行数:267 and 340 and 373
解读:我们可以看到,在trigger
函数里面也是先去全局变量targetMap找deps,这一步就是找get函数里面绑定的依赖了。在trigger
函数的末尾,会调用triggerEffects
这个函数👇👇👇(看下面这个图)
解读:看到这里,基本上就真相大白了。寻找effect
,可以看到,在最后会判断有没有scheduler
调度函数,也就是ReactiveEffect
的第二个函数,如果没有直接执行run
方法。
总结
看到这里,整个响应式变量的更新过程也就清晰了,无非主要就是两个全局变量,一个用来放更新用的effect
,一个是存储响应式变量依赖的数据中心。
结尾
感谢大家阅读这篇文章,如果有不对的地方欢迎指正👏👏👏。
转载自:https://juejin.cn/post/7188810490817019959