近期 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 的实现与之有类似,感兴趣的同学可以结合前文进行阅读。
阅读此文之前,如果对以下知识点不够了解,可以先了解以下知识点:
笔者之前也写过相关文章,也可以结合相关文章:
- 你可能忽视的ES6语法——反射和代理
- Vue 3.0 最新进展,Composition API
- Vue 3.0 前瞻,体验 Vue Function API
- Vue Composition 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 基本的运行环境,下面可以开始进行源码的调试了。
<!DOCTYPE html>
<html>
<head>
<title>vue-demo</title>
</head>
<body>
<div id="app"></div>
<script src="./packages/vue/dist/vue.global.js"></script>
<script>
const { createComponent, createApp, reactive, toRefs } = Vue;
const component = createComponent({
template: `
<div>
{{ count }}
<button @click="addHandler">add</button>
</div>
`,
setup(props) {
const data = reactive({
count: 0,
});
const addHandler = () => {
data.count++;
};
return {
...toRefs(data),
addHandler,
};
},
});
createApp().mount(component, document.querySelector('#app'));
</script>
</body>
</html>
Reactive源码解析
打开vue-next/packages/reactivity/src/reactive.ts,首先可以找到reactive函数如下:
export function reactive(target: object) {
// 如果是readonly对象的代理,那么这个对象是不可观察的,直接返回readonly对象的代理
if (readonlyToRaw.has(target)) {
return target
}
// 如果是readonly原始对象,那么这个对象也是不可观察的,直接返回readonly对象的代理,这里使用readonly调用,可以拿到readonly对象的代理
if (readonlyValues.has(target)) {
return readonly(target)
}
// 调用createReactiveObject创建reactive对象
return createReactiveObject(
target, // 目标对象
rawToReactive, // 原始对象映射响应式对象的WeakMap
reactiveToRaw, // 响应式对象映射原始对象的WeakMap
mutableHandlers, // 响应式数据的代理handler,一般是Object和Array
mutableCollectionHandlers // 响应式集合的代理handler,一般是Set、Map、WeakMap、WeakSet
)
}
上面的代码很好理解,调用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最上方,可以看到定义了以下常量:
// 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])
可以看到在reactive中会预存以下四个WeakMap:rawToReactive、reactiveToRaw、rawToReadonly、readonlyToRaw,分别是原始对象到响应式对象和 readonly 代理对象到原始对象的相互映射,另外定义了readonlyValues、nonReactiveValues,分别是 readonly 代理对象的集合与调用Vue.markNonReactive标记为不可相应对象的集合。collectionTypes是Set 、 Map 、 WeakMap 、 WeakSet的集合
用 WeakMap 来进行相互映射的原因是 WeakMap 的 key 是弱引用的。并且比起 Map , WeakMap 的赋值和搜索操作的算法复杂度均低于 Map ,具体原因可查阅相关文档。
下面来看createReactiveObject:
function createReactiveObject(
target: unknown,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 如果不是对象,直接返回,开发环境下会给警告
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 目标对象已经是可观察的,直接返回已创建的响应式Proxy,toProxy就是rawToReactive这个WeakMap,用于映射响应式Proxy
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// 目标对象已经是响应式Proxy,直接返回响应式Proxy,toRaw就是reactiveToRaw这个WeakMap,用于映射原始对象
if (toRaw.has(target)) {
return target
}
// 目标对象是不可观察的,直接返回目标对象
if (!canObserve(target)) {
return target
}
// 下面是创建响应式代理的核心逻辑
// Set、Map、WeakMap、WeakSet的响应式对象handler与Object和Array的响应式对象handler不同
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// 创建Proxy
observed = new Proxy(target, handlers)
// 更新rawToReactive和reactiveToRaw映射
toProxy.set(target, observed)
toRaw.set(observed, target)
// 看reactive的源码,targetMap的用处目前还不清楚,应该是作者预留的尚未完善的feature而准备的
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
看了上面的代码,我们知道createReactiveObject用于创建响应式代理对象:
- 首先判断
target是否是对象类型,如果不是对象,直接返回,开发环境下会给警告 - 然后判断目标对象是否已经是可观察的,如果是,直接返回已创建的响应式Proxy,
toProxy就是rawToReactive这个WeakMap,用于映射响应式Proxy - 然后判断目标对象是否已经是响应式Proxy,如果是,直接返回响应式Proxy,
toRaw就是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使用:
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
我们知道,最重要的就是代理get陷阱和set陷阱,首先来看get陷阱:
function createGetter(isReadonly: boolean) {
return function get(target: object, key: string | symbol, receiver: object) {
// 通过Reflect拿到原始的get行为
const res = Reflect.get(target, key, receiver)
// 如果是内置方法,不需要另外进行代理
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
// 如果是ref对象,代理到ref.value
if (isRef(res)) {
return res.value
}
// track用于收集依赖
track(target, OperationTypes.GET, key)
// 判断是嵌套对象,如果是嵌套对象,需要另外处理
// 如果是基本类型,直接返回代理到的值
return isObject(res)
// 这里createGetter是创建响应式对象的,传入的isReadonly是false
// 如果是嵌套对象的情况,通过递归调用reactive拿到结果
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
- get 陷阱首先通过
Reflect.get,拿到原始的get行为 - 然后判断如果是内置方法,不需要另外进行代理
- 然后判断如果是ref对象,代理到ref.value
- 然后通过
track来收集依赖 - 最后判断拿到的
res结果是否是对象类型,如果是对象类型,再次调用reactive(res)来拿到结果,避免循环引用的情况
下面来看set陷阱:
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 首先拿到原始值oldValue
value = toRaw(value)
const oldValue = (target as any)[key]
// 如果原始值是ref对象,新赋值不是ref对象,直接修改ref包装对象的value属性
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
// 原始对象里是否有新赋值的这个key
const hadKey = hasOwn(target, key)
// 通过Reflect拿到原始的set行为
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 操作原型链的数据,不做任何触发监听函数的行为
if (target === toRaw(receiver)) {
/* istanbul ignore else */
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
// 没有这个key,则是添加属性
// 否则是给原始属性赋值
// trigger 用于通知deps,通知依赖这一状态的对象更新
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
}
return result
}
- 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使用:
// 需要监听的方法调用
const mutableInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key, toReactive)
},
get size(this: IterableCollections) {
return size(this)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false)
}
// ...
function createInstrumentationGetter(
instrumentations: Record<string, Function>
) {
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) =>
// 如果是`get`、`has`、`add`、`set`、`delete`、`clear`、`forEach`的方法调用,或者是获取`size`,那么改为调用mutableInstrumentations里的相关方法
Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(mutableInstrumentations)
}
看上面的代码,我们看到mutableCollectionHandlers只有一个get陷阱,这是为什么呢?因为对于Set、Map、WeakMap、WeakSet的内部机制的限制,其修改、删除属性的操作通过set、add、delete等方法来完成,是不能通过Proxy设置set陷阱来监听的,类似于 Vue 2.x 数组的变异方法的实现,通过监听get陷阱里的get、has、add、set、delete、clear、forEach的方法调用,并拦截这个方法调用来实现响应式。
关于为什么
Set、Map、WeakMap、WeakSet不能做到响应式,笔者在why-is-set-incompatible-with-proxy找到了答案。
那么我们理解了因为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等方法调用来实现响应式原理。