// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
近期 Vue 官方正式开放了 3.x 的源码,目前处于Pre Alpha阶段,笔者出于兴趣,抽空对 Vue 3.x 源码的数据响应式部分做了简单阅读。本文通过分析 Vue 3.x 的
reactive
API 的原理,可以更方便理解 Vue 3.x 比起 Vue 2.x 响应式原理的区别。在 Vue 3.x 源码开放之前,笔者曾写过Vue Composition API 响应式包装对象原理, Vue 3.x 的
reactive
API 的实现与之有类似,感兴趣的同学可以结合前文进行阅读。笔者之前也写过相关文章,也可以结合相关文章:
搭建Vue 3.x 运行环境
进入vue-next的项目仓库,我们可以把 Vue 3.x 项目代码都clone下来,可以看到,通过执行vue-next/scripts/build.js可以将 Vue 3.x 的代码使用 rollup 打包,生成一个名为
vue.global.js
,可供开发者引用。为了方便调试,我们执行vue-next/scripts/dev.js,此时开启 rollup 的 watch 模式,可以方便我们对源码进行调试、修改、输出。在项目目录下新建一个
test.html
,引用构建在项目目录下的packages/vue/dist/vue.global.js
,在项目目录下执行npm run dev
,写一个最简单 Vue 3.x 的 demo ,用浏览器打开可以直接运行,利用这个 demo ,我们构建好了 Vue 3.x 基本的运行环境,下面可以开始进行源码的调试了。Reactive源码解析
打开vue-next/packages/reactivity/src/reactive.ts,首先可以找到
reactive
函数如下:上面的代码很好理解,调用
reactive
,首先进行是否是 readonly 对象的判断,如果target
对象是 readonly 对象或者通过调用Vue.readonly
返回的代理对象,则是不可相应的,会直接返回 readonly 响应式代理对象。然后调用createReactiveObject
创建响应式对象。createReactiveObject
传递的五个参数分别是:目标对象、原始对象映射响应式对象的WeakMap、响应式对象映射原始对象的WeakMap、响应式数据的代理handler,一般是Object和Array、响应式集合的代理handler,一般是Set、Map、WeakMap、WeakSet。我们可以翻到vue-next/packages/reactivity/src/reactive.ts最上方,可以看到定义了以下常量:可以看到在
reactive
中会预存以下四个WeakMap
:rawToReactive
、reactiveToRaw
、rawToReadonly
、readonlyToRaw
,分别是原始对象到响应式对象和 readonly 代理对象到原始对象的相互映射,另外定义了readonlyValues
、nonReactiveValues
,分别是 readonly 代理对象的集合与调用Vue.markNonReactive
标记为不可相应对象的集合。collectionTypes
是Set
、Map
、WeakMap
、WeakSet
的集合用 WeakMap 来进行相互映射的原因是 WeakMap 的 key 是弱引用的。并且比起 Map , WeakMap 的赋值和搜索操作的算法复杂度均低于 Map ,具体原因可查阅相关文档。
下面来看
createReactiveObject
:看了上面的代码,我们知道
createReactiveObject
用于创建响应式代理对象:target
是否是对象类型,如果不是对象,直接返回,开发环境下会给警告toProxy
就是rawToReactive
这个WeakMap
,用于映射响应式ProxytoRaw
就是reactiveToRaw
这个WeakMap
,用于映射原始对象Set
、Map
、WeakMap
、WeakSet
的响应式对象handler与Object
和Array
的响应式对象handler不同,要分开处理rawToReactive
和reactiveToRaw
映射响应式代理陷阱
Object和Array的代理
下面的重心来到了分析
mutableCollectionHandlers
和mutableHandlers
,首先分析vue-next/packages/reactivity/src/baseHandlers.ts,这个handler用于创建Object
类型和Array
类型的响应式Proxy使用:我们知道,最重要的就是代理
get
陷阱和set
陷阱,首先来看get
陷阱:Reflect.get
,拿到原始的get行为track
来收集依赖res
结果是否是对象类型,如果是对象类型,再次调用reactive(res)来拿到结果,避免循环引用的情况下面来看
set
陷阱:oldValue
ref
对象,新赋值不是ref
对象,直接修改ref
包装对象的value
属性Reflect
拿到原始的set行为,如果原始对象里是否有新赋值的这个key,没有这个key,则是添加属性,否则是给原始属性赋值trigger
通知deps
更新,通知依赖这一状态的对象更新Set、Map、WeakMap、WeakSet的代理
分析了
mutableHandlers
,下面来分析mutableCollectionHandlers
,打开vue-next/packages/reactivity/src/collectionHandlers.ts,这个handler用于创建Set
、Map
、WeakMap
、WeakSet
的响应式Proxy使用:看上面的代码,我们看到
mutableCollectionHandlers
只有一个get
陷阱,这是为什么呢?因为对于Set
、Map
、WeakMap
、WeakSet
的内部机制的限制,其修改、删除属性的操作通过set
、add
、delete
等方法来完成,是不能通过Proxy
设置set
陷阱来监听的,类似于 Vue 2.x 数组的变异方法的实现,通过监听get
陷阱里的get
、has
、add
、set
、delete
、clear
、forEach
的方法调用,并拦截这个方法调用来实现响应式。那么我们理解了因为
Proxy
对于Set
、Map
、WeakMap
、WeakSet
的限制,与 Vue 2.x 的变异方法类似,通过拦截get
、has
、add
、set
、delete
、clear
、forEach
的方法调用来监听Set
、Map
、WeakMap
、WeakSet
数据类型的修改。看get
、has
、add
、set
、delete
、clear
、forEach
等方法就轻松多了,这些方法与对象类型的get
陷阱、has
、set
等陷阱handler类似,笔者在这里不做过多讲述。小结
本文是笔者处于继续对 Vue 3.x 相关动态的关注,首先,笔者讲述了如何搭建一个最简单的 Vue 3.x 代码的运行和调试环境,然后对 Vue 3.x 响应式核心原理进行解析,比起 Vue 2.x , Vue 3.x 对于响应式方面全面拥抱了 Proxy API,通过代理初始对象默认行为来实现响应式;
reactive
内部利用WeakMap
的弱引用性质和快速索引的特性,使用WeakMap
保存了响应式代理和原始对象, readonly 代理和原始对象的互相映射;最后,笔者分析了响应式代理的相关陷阱方法,可以知道对于对象和数组类型,是通过响应式代理的相关陷阱方法实现原始对象响应式,而对于Set
、Map
、WeakMap
、WeakSet
类型,因为受到Proxy
的限制,Vue 3.x 使用了劫持get
、has
、add
、set
、delete
、clear
、forEach
等方法调用来实现响应式原理。