Vue 3 响应式模块 — reactive、effect 原理分析
2021/03/25
前言
Vue 2 的响应式是利用 Object.defineProperty 来实现的,有不少缺陷,就像一个“不完全体”,而 Vue 3 的响应式利用 ES6 中的 Proxy 和 Reflect 来实现,变身“完全体”。其中 Vue3 中响应式的核心方法是 reactive 和 effect , reactive 负责将数据变成响应式,effect 则是负责收集依赖,更新依赖。
Vue 3 响应式流程图如下:
接下来我们将通过源码分析 reactive 和 effect 的实现方式 。
源码解析
Vue 3 项目开发采用的是 monorepo 模式,在其 packages 目录下托管许多相互关联的应用程序包。其中负责响应式部分的代码位于 packages 下的 reactivity 目录,它不涉及 Vue 的其他的任何部分,也作为一个独立的 npm 包—— @vue/reactivity 发布。
reactive 模块
reactive 模块有三个核心点,分别是 reactive()、baseHandlers、collectionHandlers。reactive() 负责接收一个 target 对象然后返回该target对象的响应式代理;baseHandlers是当 target类型为 Object 或者 Array 时使用的Proxy handler;collectionHandlers是当 target类型为 Set、Map、WeakMap 或者 WeakSet 的 Proxy handler。
创建响应式对象:reactive()
源码如下:
reactive() 函数需要传入一个 Object 类型的参数 target,首先会判断传入的 target 是否为 readonly 对象,如果是就直接返回此对象,否则调用 createReactiveObject() 函数来创建响应式对象。
createReactiveObject() 函数用于创建响应式对象,其处理过程大致如下:
- 首先判断
target是否为Object类型,如果不是会将target直接返回,如果当前处于开发环境还会发出警告提示;
- 然后判断
target已经是Proxy对象,如果是则会将target直接返回;
- 然后判断
target是否有相应的 Proxy 对象,如果有就返回target的 Proxy 对象;
- 然后判断
target是否是在只能观察值类型白名单里,若是就返回target;
- 创建
proxy,根据target的类型来选择相对应的 handler ,如果target是 Set、Map、WeakMap 或者 WeakSet 则使用collectionHandlers,否则使用baseHandlers;
- 最后将创建的
proxy和target存入proxyMap,以target为key,proxy为value,并返回proxy。
baseHandlers
baseHandlers 是用于处理 Object 和 Array 的 Proxy handler,它对应有 4 种场景,分别 mutableHandlers、readonlyHandlers、shallowReactiveHandlers 和 shallowReadonlyHandlers。
在这里以 mutableHandlers 为例,源码如下:
其中最重要的是 get 和 set 捕获器(trap,即拦截操作的方法)。
下面先来分析一下 get 捕获器:
其处理过程大致如下:
createGetter()有两个形参,分别是isReadonly是否只读和shallow是否浅处理,返回一个 get 函数;
- 针对
__v_isReactive、__v_isReadonly、__v_raw进行判断,并返回对应的值;
- 判断
target是否为数组,如果target是非只读的数组的话,会使用Reflect.get(arrayInstrumentations, key, receiver)获取并返回值,其中arrayInstrumentations劫持了Array.prototype上面的includes/indexOf/lastIndexOf方法来实现这三个方法依赖收集;
- 使用
Reflect.get()获取并保存值为res,判断key是否是 Symbol 类型 和 原型链上的一些属性,若是直接返回 ;
- 判断是否是
isReadonly,若不是则使用track来收集依赖;
- 判断是否是
shallow若是则直接返回结果;
- 如果是
ref对象并且不是数组或者是数组但key不是数字,则返回res.value,否则返回res;
- 判断
target是否是 Object 类型,若是则对res进行响应式处理并返回;
- 最后返回
res。
再来分析一下 set 捕获器:
其处理过程大致如下:
- 首先拿到原始值
oldValue;
- 然后判断,如果
shallow为fasle并且target不是数组、原始值oldValue是 ref 对象,新赋值value不是 ref 对象,直接修改oldValue的value属性并返回true;
- 使用
Reflect.set()设置值并保存,如果target里有新赋值的key就触发add操作,否则就触发set操作,通过调用trigger通知deps更新,通知依赖这一状态的对象进行更新。
collectionHandlers
collectionHandlers 是用于处理 Set、Map、WeakMap 以及 WeakSet 的 Proxy handler,它总共有 3 种形态,分别为 mutableCollectionHandlers、readonlyCollectionHandlers 和 shallowCollectionHandlers。
在这里以 mutableCollectionHandlers 为例,源码如下:
从上面的代码可以看出,mutableCollectionHandlers 只有一个 get 捕获器,这是因为Proxy 具有有局限性, Map、Set、WeakMap、WeakSet 这 4 个内建对象使用了 “内部插槽”,它们类似于属性,但仅限于内部使用。
例如,Map 将项目(item)存储在 [[MapData]] 中。内建方法可以直接访问它们,而不通过 [[Get]]/[[Set]] 内部方法。所以 Proxy 无法拦截它们。
Vue 3 对其处理方式类似于 Vue 2 里变异数组方法,通过 get 捕获器获取 Map、Set、WeakMap、WeakSet 内置方法调用,并拦截其内置方法来实现响应式。
effect 模块
effect 模块中也有三个核心点,分别是 effect()、track()、trigger()。
effect() 负责定义副作用函数,track() 负责跟踪收集依赖(收集 effect),trigger() 负责触发响应(执行 effect)。track() 和 trigger() 需要配合 effect() 函数使用。
定义副作用:effect()
源码如下:
effect() 首先会接收两个参数 fn 原始帧听函数和 options 配置选项,然后调用 isEffect 判断传入的函数是否是 effect 函数,若是则将其转换成原始侦听函数,在这里 effect() 始终会返回一个新创建的侦听函数,然后调用 createReactiveEffect() 来执行创建 effect 侦听函数。
createReactiveEffect() 接收来自 effect() 的两个参数,其内部会初始化一个侦听函数 reactiveEffect,reactiveEffect 内部会先判断自身的 active 属性,若为 fasle 并且自定义了调度函数 scheduler 就会结束程序,如果没有自定义调度函数,则会执行原始监听函数 fn ,然后判断 effectStack 中是否有当前的侦听函数,如果有就会 调用 cleanup() 来清理依赖(cleanup() 在每次即将执行副作用函数之前都会执行,保证依赖属性时刻对应最新的侦听函数),然后再进行依赖收集,最后在 reactiveEffect 上挂载一些属性。
收集依赖:track()
源码如下:
track() 接收3个参数,target 是要跟踪的目标对象,type 是跟踪操作的类型,key 是 target 的属性名。
track() 内部首先会判断,如果关闭 track 或者当前没有 activeEffect 则无需收集依赖,track() 会终止执行;然后会获取 depsMap,depsMap 是存在 targetMap 里面的(targetMap 是以 target 为 key、 depsMap 为 value 的 WeakMap,depsMap 是以 target 的 key 为 key,以依赖 dep 为 value 的 Map, dep 是一个 Set,存放的是对应的侦听函数),如果 depsMap 不存在则进行初始化,然后从 depsMap 中获取依赖集合 dep ,如果 dep 不存在也进行初始;然后进行依赖收集,会先检测 dep 中是否有 activeEffect ,如果没有则把它存进去,并且更新 activeEffect 里的 deps 属性,将 dep 也放到 activeEffect.deps里,用于描述当前响应式对象的依赖;最后是在开发环境下时,去触发相应的钩子函数。
触发响应:trigger()
源码如下:
trigger() 接收5个参数,target 是目标对象、type 是触发的操作类型、key 是触发的键名、还有newValue、oldValue、oldTarget。
trigger() 先获取目标对象的 depsMap ,若 depsMap 不存在,说明没有收集过该目标对象的依赖,无需触发更新,直接返回,否则初始化一个 Set 用来存放要被执行的侦听函数,然后创建一个函数 add 用于将侦听函数添加到 effects 集合里;然后对触发的操作类型 type 进行判断,如果操作类型为 CLEAR,将所有的 effect 都添加进 effects 集合,触发所有的依赖函数,如果是有关修改数组长度的操作,则将 depsMap 中关于 length 的侦听函数添加进 effects 集合,如果操作类型不是 CLEAR 并且也不是修改数组的操作则进入另一分支进行判断;接下来是一系列对传入的 type 操作类型和 key 键名的判断,最终目的是将对应的 dep 依赖集合添加进 effects 集合中;最后定义了一个 run 方法用来执行更新响应操作,如果配置了调度函数 scheduler,则使用此 scheduler 来执行 effect 侦听函数,否则直接执行 effect 侦听函数。
总结
本文分析了 Vue 3 使用 reactive 和 effect 实现响应式的原理,reactive 模块使用了 Proxy API,通过对原始对象进行代理的方式来实现响应式,对于 Array 和 Object 可以直接使用 Proxy 的捕获器,而 Map、Set、WeakMap、WeakSet 因为 Proxy 的缺陷,是通过劫持其内部方法来实现响应式;effect 模块分为 effect()、track()、tigger() 三部分,分别负责定义副作用、收集依赖和触发响应,当对象变化时会调用 track() 收集依赖,trigger() 来触发依赖,完成数据通知和响应。
