我们知道Vue 2.0
是利用Ojbect.defineProperty
对对象的已有属性值的读取和修改进行劫持,但是这个API
不能监听对象属性的新增和删除,此外为了深度劫持对象的内部属性,必须在初始化的时候对内部属性进行递归调用Ojbect.defineProperty
,这就造成了一个性能上的消耗。为了解决这些问题,Vue 3.0
利用Proxy
重写了响应式逻辑并且优化了相关性能。
我们先来个示例看下Vue 3.0
的响应式API的写法:
changePerson
能改变响应式数据person
的值,person
值的变化会触发组件重新渲染而更新DOM。
这里我们可以看到Vue 3.0
的使用中,开发者利用reactive
函数自己去确定哪些数据为响应式数据,这样就可以避免一些不必要的响应式的性能消耗。例如案例中我们就不需要让nowIndex
成为响应式数据。(当然Vue 2.0
也可以在data
函数外定义数据,这样也是非响应式数据)
我们接下来看看reactive
函数的实现原理!
reactive
API相关的流程reactive
代码说明:
- 如果目标对象
target
是readonly对象,直接返回目标对象,因为readonly对象不能设置成响应式对象- 调用
createReactiveObject
函数继续流程。
createReactiveObject
创建响应式对象代码说明:
- 如果目标对象不是数据或者对象,则直接返回对象,在开发环境给出错误警告提示。
- 如果
target
已经是一个Proxy
对象,则直接返回target
, (target['__v_raw']
设计非常巧妙:如果target
是Proxy
对象,target['__v_raw']
触发get
方法,在缓存对象reactiveMap
中查找是否target
对象的Proxy
对象是否等于target
自身)。这里处理了一个例外,如果是给响应式对象执行readonly
函数则需要继续。- 在
reactiveMap
中查找是否已经有了对应的Proxy
对象,则直接返回对应的Proxy
对象。- 确保只有特定的数据能变成响应式,否则直接返回
target
。响应式白名单如下所示:
target
没有被执行过markRaw
方法,或者说target
对象没有__v_skip
属性值或者__v_skip
属性的值为false
;target
不能是不可扩展对象,即target
没有被执行过preventExtensions
,seal
和freeze
这些方法;target
为Object
或者Array
;target
为Map
,Set
,WeakMap
,WeakSet
;- 通过使用
Proxy
函数劫持target
对象,返回的结果即为响应式对象了。这里的处理函数会根据target
对象不同而不同(这两个函数都是参数传入的):
Object
或者Array
的处理函数是collectionHandlers
;Map
,Set
,WeakMap
,WeakSet
的处理函数是baseHandlers
;- 将响应式对象存入
reactiveMap
中缓存起来,key
是target
,value
是proxy
。
mutableHandlers
处理函数我们知道访问对象属性会触发get
函数,设置对象属性会触发set
函数,删除对象属性会触发deleteProperty
函数,in
操作符会触发has
函数,getOwnPropertyNames
会触发ownKeys
函数。我们接下来看看你这几个函数的代码逻辑。
get
函数由于没有传参,isReadonly
和shallow
都是默认参数false
。
代码逻辑:
- 如果获取
__v_isReactive
属性,返回true
, 表示target
已经是一个响应式对象了;- 获取
__v_isReadonly
属性,返回false
;(readonly
是响应式的另外一个API,暂不解释)- 获取
__v_raw
属性,返回target
本身,这个属性用来判断target
是否已经是响应式对象;- 如果target是数组,且命中了一些属性,例如
includes
,indexOf
,lastIndexOf
等,则执行的是数组的这些函数方法,并对数组的每个元素执行收集依赖track(arr, TrackOpTypes.GET, i + '')
,然后通过Reflect
获取数组函数的值;Reflect
求值;- 判断是否是特殊的属性值:
symbol
,__proto__
,__v_isRef
,__isVue
, 如果是直接返回前面得到的res
,不做后续处理;- 执行收集依赖;
- 如果是
ref
, 如果target
不是数组或者key
不是整数,就执行数据拆包,这里涉及到另外一个响应式APIref
, 暂不解释;- 如果
res
是对象,递归执行reactive
,把res
变成响应式对象。这里是一个优化小技巧,只有属性值被访问后才会被被劫持,避免了初始化就全劫持的性能消耗。
get
函数的的调用时机回答这个问题前我们需要回到前面一篇关于setup
的文章—揭开Vue3.0 setup函数的神秘面纱。
在setupStatefulComponent
函数中会执行setup()
函数,并得到执行结果:
handleSetupResult
处理结果的逻辑是间隔setupResult
赋值给instance.setupState
:
这个instance.setupState
被instance.ctx
代理,所以访问和修改instance.ctx
就能直接访问和修改instance.setupState
:
我们以前提到过渲染生成子树VNode就是调用render
函数,我们用模板编译看看我们例子中的render
函数长啥样子?
很清晰了,当渲染模板的时候,会从ctx
中取person
属性对象,其实就是取setupState
的person
属性对象。当取setupState
的person
属性对象的name
,age
,address
时都会触发get
函数的调用,获取对应的值。
总结:组件实例对象执行
render
函数生成子树VNode时,会调用响应式对象的get
函数。
track
收集依赖我们上面的get
函数的代码解释中两次提到了收集依赖,那什么是收集依赖呢?
要实现响应式,就是当数据变化后会自动实现一些功能,比如执行某些函数等。因为副作用渲染函数能触发组件的重新渲染而更新DOM,所以这里收集的依赖就是当数据变化后需要执行的副作用渲染函数。
也就是说,当执行get
函数时就会收集对应组件的副作用渲染函数。
我们可以拿我们的例子说明最后的结果:
set
函数代码逻辑:
- 如果值没有变化,直接返回;
- 通过
Reflect
设置新值;- 不是原型链上的属性,如果是新增属性执行
add
类型的trigger
,如果是修改属性执行set
类型的trigger
。(如果Reflect.set原型链上的属性会再次调用setter,所以不用两次执行trigger)。
trigger
分发依赖
trigger
代码逻辑很清晰,就是从get
函数中收集来的依赖targetMap
中找到对应的函数,然后执行这些副作用渲染函数,更新DOM。
get
和副作用渲染函数关联我们回过头来再解答一个疑问:就是从get
函数中收集来的副作用渲染函数是怎么确定的,即访问person.name
时如何确定关联哪个副作用渲染函数呢?
我们接下来一步步梳理其中的逻辑:
mountComponent
最后一步是执行带副作用的渲染函数:setupRenderEffect
先定义了一个componentUpdateFn
组件渲染函数,然后将这个componentUpdateFn
封装在了ReactiveEffect
中,并将ReactiveEffect
对象的run
方法赋值给组件对象的update
属性,然后执行update
方法,其实就是执行ReactiveEffect
对象的run
方法。ReactiveEffect
的run方法持有了传入的函数,当前场景为componentUpdateFn
组件渲染函数,并且利用了两个全局的变量effectStack
和activeEffect
。run
方法时先将componentUpdateFn
赋值给activeEffect
,并且压入effectStack
栈中,然后执行componentUpdateFn
方法。当执行完成后componentUpdateFn
出栈,并且赋值activeEffect
为新的栈顶的函数。componentUpdateFn
执行的时候会调用renderComponentRoot
,本质是执行组件实例对象的render
方法。render
方法中如果访问相应式数据就会触发get
函数,get
中收集的就是这里设计一个栈的结构,主要是为了解决
effect
嵌套的问题。
如果仔细思考下可能会有一个疑问?name
,age
,address
都修改了,然后他们都关联了同一个渲染函数,理论上同时修改这三个值会触发三次组件重新渲染呢,这明显是不合理的。那Vue
是如何控制只执行一次呢?
ReactiveEffect
封装componentUpdateFn
渲染函数的地方,我们先看一眼第二个参数scheduler
:scheduler
则会执行scheduler
:queueJob
的执行逻辑是如果任务在队列中就过滤掉不执行。本文详细介绍了Vue3.0
的相应式原理:利用Proxy
劫持对象,访问对象的时候会触发get
方法,此时会进行依赖的收集;当修改对象数据的时候会触发set
方法,此时会派发依赖,即调用组件的副作用渲染函数(其实不限于), 这样组件就能重新渲染,DOM更新。
本文介绍了响应式原理,接下来我将介绍一些常用的响应式API(例如readonly
, ref
等)的实现逻辑。