近期 Vue 官方正式开放了 3.x 的源码,目前处于Pre Alpha阶段,笔者出于兴趣,抽空对 Vue 3.x 源码的数据响应式部分作了简单阅读。本文经过分析 Vue 3.x 的 reactive
API 的原理,能够更方便理解 Vue 3.x 比起 Vue 2.x 响应式原理的区别。javascript
在 Vue 3.x 源码开放以前,笔者曾写过Vue Composition API 响应式包装对象原理, Vue 3.x 的 reactive
API 的实现与之有相似,感兴趣的同窗能够结合前文进行阅读。html
阅读此文以前,若是对如下知识点不够了解,能够先了解如下知识点:vue
笔者以前也写过相关文章,也能够结合相关文章:java
进入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 模式,能够方便咱们对源码进行调试、修改、输出。react
在项目目录下新建一个test.html
,引用构建在项目目录下的packages/vue/dist/vue.global.js
,在项目目录下执行npm run dev
,写一个最简单 Vue 3.x 的 demo ,用浏览器打开能够直接运行,利用这个 demo ,咱们构建好了 Vue 3.x 基本的运行环境,下面能够开始进行源码的调试了。git
<!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>
复制代码
打开vue-next/packages/reactivity/src/reactive.ts,首先能够找到reactive
函数以下:github
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最上方,能够看到定义了如下常量:npm
// 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
的集合api
用 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
是不是对象类型,若是不是对象,直接返回,开发环境下会给警告toProxy
就是rawToReactive
这个WeakMap
,用于映射响应式ProxytoRaw
就是reactiveToRaw
这个WeakMap
,用于映射原始对象Set
、Map
、WeakMap
、WeakSet
的响应式对象handler与Object
和Array
的响应式对象handler不一样,要分开处理rawToReactive
和reactiveToRaw
映射下面的重心来到了分析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
}
}
复制代码
Reflect.get
,拿到原始的get行为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
}
复制代码
oldValue
ref
对象,新赋值不是ref
对象,直接修改ref
包装对象的value
属性Reflect
拿到原始的set行为,若是原始对象里是否有新赋值的这个key,没有这个key,则是添加属性,不然是给原始属性赋值trigger
通知deps
更新,通知依赖这一状态的对象更新分析了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
等方法调用来实现响应式原理。