最近一直在读Vue3.0的源代码,也给Vue3.0贡献了一些代码,因此想开个坑记录一下自己阅读Vue3.0源代码的一些心得,供大家参考。
本篇文章主要会对响应式数据部分(reactivity)进行解读和阐释。
在vue3.0的源代码中,源码集中在packages目录下,其响应式数据的源代码集中在packages/reactivity目录下。
我们都知道vue2的响应式数据是通过Object.definePropery实现的。
Object.defineProperty(obj, prop, descriptor)
obj:要在其上定义属性的对象。
prop:要定义或修改的属性的名称。
descriptor:将被定义或修改的属性描述符。
descriptor具有以下两种可选值:
get:给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象。
set:给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。
举个例子:
<body>
<div id="app">
<input type="text" id="txt">
<p id="show"></p>
</div>
<script>
let obj = {
};
Object.defineProperty(obj, 'txt', {
get: function () {
return obj
},
set: function (newValue) {
document.getElementById('txt').value = newValue;
document.getElementById('show').innerHTML = newValue;
}
});
document.addEventListener('keyup', function (e) {
obj.txt = e.target.value;
})
</script>
</body>
当input出发keyup事件时,obj的txt属性被重新设置,此时会被set descriptor捕获到,并且更改其值,达到响应式的效果。
但是在Vue3里面,用的并不是defineProperty而是Proxy。
let p = new Proxy(target, handler);
target:用Proxy包装的目标对象(可以是任何类型的对象)。
handler:一个对象,其属性是当执行一个操作时定义代理的行为的函数,其可以定义的行为有以下几种。
1、get 获取某个key值
2、set 设置某个key值
3、has 使用in操作符判断某个key是否存在
4、apply 函数调用,仅在代理对象为function时有效
5、ownKeys 获取目标对象所有的key
6、construct 函数通过实例化调用,仅在代理对象为function时有效
7、isExtensible 判断对象是否可扩展,Object.isExtensible的代理
8、deleteProperty 删除一个property
9、defineProperty 定义一个新的property
10、getPrototypeOf 获取原型对象
11、setPrototypeOf 设置原型对象
12、preventExtensions 设置对象为不可扩展
13、getOwnPropertyDescriptor 获取一个自有属性 (不会去原型链查找) 的属性描述
举个例子:
let obj = {
}
const test = new Proxy(obj, {
get(target, name){
console.log("Get.")
},
set(target, key, value) {
console.log("Set.")
},
defineProperty(target, prop, descriptor) {
console.log("define.")
},
deleteProperty(target, prop) {
console.log("delete.")
},
});
可以看到Proxy从功能上来讲完全能够替代defineProperty,唯一的缺点就是兼容性不好,Proxy是javascript ES6的规范,这意味着Proxy在一些老旧的浏览器上无法使用(IE:你们为什么都在看我?)
接下来进入正题,我会为大家简单分析一下Vue3的响应式数据的原理,其代码主要在packages/reactivity/reactive.ts中(如果没学过typescript的话请右转百度先去了解一下typescript)
最关键的代码在这里,直接看注释就好:
// 这两个WeakMap用于存储未代理对象和已代理的响应式对象的对应关系,方便查找。
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
function reactive(target: object) {
// ...无关代码
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject(
target: unknown,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// ... 无关代码,包含判断target是否已经被代理的逻辑
observed = new Proxy(target, handlers) // 这部分代码就将handlers代理到target对象中
toProxy.set(target, observed) // 将<未代理对象,已代理对象>储存到一个WeakMap中,方便查找
toRaw.set(observed, target) // 将<已代理对象,未代理对象>储存到一个WeakMap中,方便查找
// ... 无关代码
return observed
}
我们知道Proxy的set handler只能感知一层数据,那么对于多层嵌套的数据,Vue3是如何处理的呢?
关键代码在package/reactivity/baseHandlers.ts里:
function createGetter(isReadonly: boolean) {
return function get(target: object, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver)
// ...无关代码
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
什么?你在问Reflect是什么?打个比方 :
“有一个全局对象叫做Reflect,上面直接挂载了某些特殊方法,这些方法可以通过Reflect.xxxxx这种形式来使用。”
更详细请参考百度。
简单来说就是,调用更深一层的数据时,Proxy的get handler会触发,所以我们利用Reflect.get(target, key, receiver)对内层数据再进行一次代理。
当然在Vue3中,我们还判断了这个res是否只读、是否是对象,然后分别返回对应的readonly(res)、reactive(res)或者res。
如果大家使用过Proxy,就会发现它有个很大的问题——只要对某个对象的任意一个属性进行了更改,其set handler都会触发。比如,如果你对数组进行代理,然后进行push操作,你会发现set handler触发了两次,一次是push数据时触发,一次是修改length属性时触发。
let obj = []
const test = new Proxy(obj, {
set(target, key, value) {
console.log("Set.")
}
});
obj.push(1) // 控制台会输出两次Set.
那这个问题是如何避免的呢?
// 判断val中是否有key
const hasOwn = (val: object,key: string | symbol): key is keyof typeof val => {
return hasOwnProperty.call(val, key)
}
// 判断value和oldValue是否一致
const hasChanged = (value: any, oldValue: any): boolean => {
return value !== oldValue && (value === value || oldValue === oldValue)
}
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// ...无关代码
const oldValue = (target as any)[key]
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
return result
}
简单来说,就是通过判断 key 是否为 target 自身属性,以及设置val是否跟target[key]相等(如果是在原型链上的自动更新的属性,如Array.length,其val和target[key]必然相等)可以确定 trigger 的类型,并且避免多余的 trigger。
基本就是这样:)