Vue3.0 响应式实现原理分析

865 阅读8分钟
Vue 3.0 系列文章

Vue 3.0组件的渲染流程

Vue 3.0组件的更新流程和diff算法详解

揭开Vue3.0 setup函数的神秘面纱

Vue 3.0 Props的初始化和更新流程的细节分析

Vue3.0 响应式实现原理分析

Vue 3.0 计算属性的实现原理分析

Vue3.0 常用响应式API的使用和原理分析(一)

Vue3.0 常用响应式API的使用和原理分析(二)

Vue 3.0 Provide和Inject实现共享数据

Vue 3.0 Teleport的使用和原理分析

Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3.0 指令

Vue3.0 内置指令的底层细节分析

Vue3.0 的事件绑定的实现逻辑是什么

Vue3.0 的双向绑定是如何实现的

Vue3.0的插槽是如何实现的?

探究Vue3.0的keep-alive和动态组件的实现逻辑

Vuex 4.x

Vue Router 4 的使用,一篇文章给你讲透彻

我们知道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

reactive

代码说明:

  1. 如果目标对象targetreadonly对象,直接返回目标对象,因为readonly对象不能设置成响应式对象
  2. 调用createReactiveObject函数继续流程。

createReactiveObject 创建响应式对象

createReactiveObject

代码说明:

  1. 如果目标对象不是数据或者对象,则直接返回对象,在开发环境给出错误警告提示。
  2. 如果target已经是一个Proxy对象,则直接返回target, (target['__v_raw']设计非常巧妙:如果targetProxy对象,target['__v_raw']触发get方法,在缓存对象reactiveMap中查找是否target对象的Proxy对象是否等于target自身)。这里处理了一个例外,如果是给响应式对象执行readonly函数则需要继续。
  3. reactiveMap中查找是否已经有了对应的Proxy对象,则直接返回对应的Proxy对象。
  4. 确保只有特定的数据能变成响应式,否则直接返回target。响应式白名单如下所示:
    • target没有被执行过markRaw方法,或者说target对象没有__v_skip属性值或者__v_skip属性的值为false
    • target不能是不可扩展对象,即target没有被执行过preventExtensions,sealfreeze这些方法;
    • targetObject或者Array;
    • targetMap,Set,WeakMap,WeakSet
  5. 通过使用Proxy函数劫持target对象,返回的结果即为响应式对象了。这里的处理函数会根据target对象不同而不同(这两个函数都是参数传入的):
    • Object或者Array的处理函数是collectionHandlers;
    • Map,Set,WeakMap,WeakSet的处理函数是baseHandlers;
  6. 将响应式对象存入reactiveMap中缓存起来,keytarget, valueproxy

mutableHandlers 处理函数

mutableHandlers

我们知道访问对象属性会触发get函数,设置对象属性会触发set函数,删除对象属性会触发deleteProperty函数,in操作符会触发has函数,getOwnPropertyNames会触发ownKeys函数。我们接下来看看你这几个函数的代码逻辑。

get函数

由于没有传参,isReadonlyshallow都是默认参数false

get

代码逻辑:

  1. 如果获取__v_isReactive属性,返回true, 表示target已经是一个响应式对象了;
  2. 获取__v_isReadonly属性,返回false;(readonly是响应式的另外一个API,暂不解释)
  3. 获取__v_raw属性,返回target本身,这个属性用来判断target是否已经是响应式对象;
  4. 如果target是数组,且命中了一些属性,例如includes, indexOf, lastIndexOf等,则执行的是数组的这些函数方法,并对数组的每个元素执行收集依赖track(arr, TrackOpTypes.GET, i + ''),然后通过Reflect获取数组函数的值;
  5. Reflect求值;
  6. 判断是否是特殊的属性值:symbol, __proto____v_isRef__isVue, 如果是直接返回前面得到的res,不做后续处理;
  7. 执行收集依赖
  8. 如果是ref, 如果target不是数组或者key不是整数,就执行数据拆包,这里涉及到另外一个响应式APIref, 暂不解释;
  9. 如果res是对象,递归执行reactive,把res变成响应式对象。这里是一个优化小技巧,只有属性值被访问后才会被被劫持,避免了初始化就全劫持的性能消耗。

get函数的的调用时机

回答这个问题前我们需要回到前面一篇关于setup的文章---揭开Vue3.0 setup函数的神秘面纱

setupStatefulComponent函数中会执行setup()函数,并得到执行结果:

setupStatefulComponent

handleSetupResult处理结果的逻辑是间隔setupResult赋值给instance.setupState:

handleSetupResult

这个instance.setupStateinstance.ctx代理,所以访问和修改instance.ctx就能直接访问和修改instance.setupState

ctx

我们以前提到过渲染生成子树VNode就是调用render函数,我们用模板编译看看我们例子中的render函数长啥样子?

render

很清晰了,当渲染模板的时候,会从ctx中取person属性对象,其实就是取setupStateperson属性对象。当取setupStateperson属性对象的name,age,address时都会触发get函数的调用,获取对应的值。

总结:组件实例对象执行render函数生成子树VNode时,会调用响应式对象的get函数。

track 收集依赖

我们上面的get函数的代码解释中两次提到了收集依赖,那什么是收集依赖呢? 要实现响应式,就是当数据变化后会自动实现一些功能,比如执行某些函数等。因为副作用渲染函数能触发组件的重新渲染而更新DOM,所以这里收集的依赖就是当数据变化后需要执行的副作用渲染函数

也就是说,当执行get函数时就会收集对应组件的副作用渲染函数

track

tractEffect

我们可以拿我们的例子说明最后的结果:

结果

set函数

set

代码逻辑:

  1. 如果值没有变化,直接返回;
  2. 通过Reflect设置新值;
  3. 不是原型链上的属性,如果是新增属性执行add类型的trigger,如果是修改属性执行set类型的trigger。(如果Reflect.set原型链上的属性会再次调用setter,所以不用两次执行trigger)。

trigger 分发依赖

trigger

trigger 代码逻辑很清晰,就是从get函数中收集来的依赖targetMap中找到对应的函数,然后执行这些副作用渲染函数,更新DOM。

get和副作用渲染函数关联

我们回过头来再解答一个疑问:就是从get函数中收集来的副作用渲染函数是怎么确定的,即访问person.name时如何确定关联哪个副作用渲染函数呢?

我们接下来一步步梳理其中的逻辑:

  • 组件挂载mountComponent最后一步是执行带副作用的渲染函数

mountComponent

  • setupRenderEffect先定义了一个componentUpdateFn组件渲染函数,然后将这个componentUpdateFn封装在了ReactiveEffect中,并将ReactiveEffect对象的run方法赋值给组件对象的update属性,然后执行update方法,其实就是执行ReactiveEffect对象的run方法。

setupRenderEffect

  • ReactiveEffect的run方法持有了传入的函数,当前场景为componentUpdateFn组件渲染函数,并且利用了两个全局的变量effectStackactiveEffect。 在执行run方法时先将componentUpdateFn赋值给activeEffect,并且压入effectStack栈中,然后执行componentUpdateFn方法。当执行完成后componentUpdateFn出栈,并且赋值activeEffect为新的栈顶的函数。

ReactiveEffect

  • componentUpdateFn执行的时候会调用renderComponentRoot,本质是执行组件实例对象的render方法。

componentUpdateFn

  • 目前为止就到了本文的内容了,render方法中如果访问相应式数据就会触发get函数,get中收集的就是

tractEffects

这里设计一个栈的结构,主要是为了解决effect嵌套的问题。

副作用渲染函数的执行过滤

如果仔细思考下可能会有一个疑问?name,age,address都修改了,然后他们都关联了同一个渲染函数,理论上同时修改这三个值会触发三次组件重新渲染呢,这明显是不合理的。那Vue是如何控制只执行一次呢?

  • 我们需要再次回到ReactiveEffect封装componentUpdateFn渲染函数的地方,我们先看一眼第二个参数scheduler

ReactiveEffect

  • 派发依赖的时候如果有scheduler则会执行scheduler

在这里插入图片描述

  • queueJob的执行逻辑是如果任务在队列中就过滤掉不执行。

queueJob

结尾

本文详细介绍了Vue3.0的相应式原理:利用Proxy劫持对象,访问对象的时候会触发get方法,此时会进行依赖的收集;当修改对象数据的时候会触发set方法,此时会派发依赖,即调用组件的副作用渲染函数(其实不限于), 这样组件就能重新渲染,DOM更新。

本文介绍了响应式原理,接下来我将介绍一些常用的响应式API(例如readonly, ref等)的实现逻辑。