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()
来触发依赖,完成数据通知和响应。