vue2.x 是利用 Object.defineProperty 方法侦测对象的属性变化,但该方法有一些固有的缺陷:
- 性能较差;
- 在对象上新增属性是无法被侦测的;
- 改变数组的 length 属性是无法被侦测的。
vue3.0 是非常大的重构,源码使用 TypeScript 重写,目前的代码 98% 以上使用 TypeScript 编写。其中数据响应化 使用 ES6 的 Proxy 取代 Object.defineProperty 方法,性能更优异,而且数组和对象一样,可以直接触发 get 和 set 方法。Proxy 称为代理,是一种可以拦截并改变底层 javascript 引擎操作的包装器。
调用 new Proxy(target, handler)可以伟一个目标对象创建一个代理,代理可以拦截 javascript 引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数,在调用 Proxy 构造函数时,需要传入两个参数,target 为目标对象,handler 是一个包含陷阱函数的处理器对象。
下面使用 Proxy 模拟实现 vue3.0 的响应式系统。
对象代理
// 创建对象响应式核心方法
function reactive(target){
// 如果 target 不是对象,则直接返回
if(target === null || typeof target !== 'object') return target;
return new Proxy(target, {
get(target, property, receiver) {
console.log('获取值')
const result = Reflect.get(target, property, receiver);
return result;
},
set(target, property, value, receiver) {
console.log('设置值')
const result = Reflect.set(target, property, value, receiver);
return result;
},
deleteProperty(target, property) {
return Reflect.deleteProperty(target, property);
}
});
}
Reflect 是一个内置对象,它提供了可拦截 javascript 操作的方法。每个代理陷阱对应一个命名和参数都相同的 Reflect 方法。
const obj = {name: 'icon'};
const proxy = reactive(obj);
proxy.name = 'lee';
console.log(proxy.name);
上面执行结果为
这样就算了初步实现了数据对象的代理,但是这样的话是无法侦测多层对象的。所以我们需要在 get 陷阱函数中对返回值做一个判断,如果返回值是一个对象,则返回值也创建代理对象,也就是递归调用。
function reactive(target){
...
get(target, property, receiver) {
console.log('获取值')
const result = Reflect.get(target, property, receiver);
// 这里新增了多层对象的判断
if(target !== null || typeof target === 'object') return reactive(result);
return result;
},
...
}
const obj = {name: 'icon', address: {province: '广东省'}};
const proxy = reactive(obj);
proxy.address.province = '北京';
上面执行结果为
这样就解决了多层对象无法侦测的问题,但是接下来又有新的问题,比如
- 对统一目标对象进行了多次代理
- 目标对象代理后又对代理对象进行代理 这两种情况是没有意义的,接下来我们一起使用 WeakMap 数据结构 来解决,当然有别的解决方法,这就看你自己的咯。
// key: 目标对象 指向 value: 代理对象
const toProxy = new WeakMap();
// key: 代理对象 指向 value: 目标对象
const toRaw = new WeakMap();
function reactive(target){
...
// 如果目标对象已经有代理对象,则直接返回该代理对象
const proxy = toProxy.get(target);
if(proxy) return proxy;
// 如果目标对象是代理对象,并且有对应的真实对象,则直接返回对象
if(toRaw.get(target)) return target;
// 这里就不直接返回 proxy 对象了, 因为需要存储
const observerd = new Proxy(target, {
...
});
toProxy.set(target, observerd);
toRaw.set(observerd, target);
// 最后再返回 proxy 对象
return observerd;
}
大家是否以为这样就完美了呢?
当然不是了,如果你去执行数组的 push 的话,会发现 get, set 陷阱函数 执行了两次。
因为 push 方法向数组中添加元素的同时还会修改数组的长度,因此有两次陷阱函数的触发,一次是将数组索引为 3 的位置新增值为 4,一次是修改数组的 length 属性为 4。这样的话,假如在 set 陷阱函数中更新视图,那么就会出现更新两次的情况。
接下来我们再加上对属性是新增还是修改的相关判断
...
const observerd = new Proxy(target, {
...
set(target, property, value, receiver) {
console.log('设置值')
const oldValue = Reflect.get(target, property);
const result = Reflect.set(target, property, value, receiver);
// 判断当前对象是否有指定属性
if(!target.hasOwnProperty(property)) {
console.log('新增属性')
}else if(oldValue !== value) {
console.log('修改属性')
}
return result;
},
...
});
...
好了,这样的话数据对象的代理就告一段落咯。
依赖收集
接下来就是 vue3.0 中比较难理解的依赖收集了,vue3.0 使用了 effect 函数来包装依赖,称为副作用。effect 函数的模拟实现如下。
...
// 保存 effect 的数组,以栈的形式存储
const effectStack = [];
function effect(fn) {
// 创建响应式 effect
const effect = createReactiveEffect(fn);
// 默认先执行一次 effect,本质上调用的是传入的 fn 函数
effect();
}
// 创建响应式 effect 函数
function createReactiveEffect(fn) {
// 响应式 effect
const effect = function() {
try {
// 将 effect 保存到全局 effectStack 栈中
effectStack.push(effect);
return fn();
} finally {
// 调用玩依赖后,弹出 effect
effectStack.pop();
}
}
return effect;
}
const proxy = reactive({name: 'icon'});
effect(() => {
console.log(proxy.name);
});
proxy.name = 'lee';
运行结果
从输出结果中可以看到,除了默认执行一次的 effect 外,当name 属性发生变化时,effect 并没有被执行。为了在对象属性发生变化时,让 effect 再次执行,需要将对象的属性与 effect 进行关联,这可以采用 Map 来存储,考虑到一个属性可能会关联多个依赖,那么存储在映射关系应该是 key 为对象属性,value 为 保存所有 effect 的 Set 对象,之所以选择 Set 而不是数组,是因为 Set 有去重的效果。另外,属性毕竟是对象的属性,不能脱离对象而单独存在,要跟踪不同对象属性的依赖,还需要一个 WeakMap, key 为对象本身,value 为保存所有属性与依赖关系的 Map。
定义好数据结构后,我们接下来就可以编写一个依赖收集函数 track。
...
// 保存对象与其属性依赖关系的 Map,key 是对象,value 是 Map
const targetMap = new WeakMap();
// 跟踪依赖
function track(target, property) {
// 获取全局数组栈 effectStack 中的依赖
const effect = effectStack[effectStack.length - 1];
// 如果存在依赖
if(effect) {
// 取出该对象对应的 Map
let depsMap = targetMap.get(target);
// 如果不存在,则以目标对象为 key,新建的 Map 为 value,保存到 targetMap 中
if(!depsMap) {
targetMap.set(target, depsMap = new Map())
}
// 从 Map 中取出该属性对应的所有 effect
let deps = depsMap.get(property);
// 如果不存在,则以属性为 key,新建的 set 为 value,存储到 depsMap 中
if(!deps) {
depsMap.set(property, deps = new Set());
}
// 判断 Set 中是否已经存在 effect,如果没有,则添加到 deps 中
if(!deps.has(effect)) {
deps.add(effect);
}
}
}
接下来是当属性发生变化时,触发属性关联的所有 effect 执行,因此,我们再编写一个 trigger 函数
...
// 执行属性关联的所有 effect
function trigger(target, type, property) {
const depsMap = targetMap.get(target);
if(depsMap) {
let deps = depsMap.get(property);
// 当前属性关联的所有 effect 依次执行
if(deps) {
deps.forEach(effect => effect());
}
}
}
依赖收集的函数和触发依赖的函数都 OK 了,那么自然需要在某个地方去执行收集依赖和触发依赖,依赖收集放到 get 陷阱函数中,而触发依赖是在属性发生变化时执行依赖,那当然是 set 陷阱函数中。
在Proxy 中 添加 依赖的收集和触发
完善代码
// key: 目标对象 指向 value: 代理对象
const toProxy = new WeakMap();
// key: 代理对象 指向 value: 目标对象
const toRaw = new WeakMap();
function reactive(target){
// 如果 target 不是对象,则直接返回
if(target === null || typeof target !== 'object') return target;
// 如果目标对象已经有代理对象,则直接返回该代理对象
const proxy = toProxy.get(target);
if(proxy) return proxy;
// 如果目标对象是代理对象,并且有对应的真实对象,则直接返回对象
if(toRaw.get(target)) return target;
// proxy 代理
const observerd = new Proxy(target, {
get(target, property, receiver) {
console.log('获取值')
const result = Reflect.get(target, property, receiver);
// 依赖收集
track(target, property);
if(target !== null || typeof target === 'object') return reactive(result);
return result;
},
set(target, property, value, receiver) {
console.log('设置值')
const oldValue = Reflect.get(target, property);
const result = Reflect.set(target, property, value, receiver);
// 判断当前对象是否有指定属性
if(!target.hasOwnProperty(property)) {
console.log('新增属性')
trigger(target, 'add', property);
}else if(oldValue !== value) {
console.log('修改属性')
trigger(target, 'set', property);
}
return result;
},
deleteProperty(target, property) {
return Reflect.deleteProperty(target, property);
}
});
toProxy.set(target, observerd);
toRaw.set(observerd, target);
// 最后再返回 proxy 对象
return observerd;
}
// 保存 effect 的数组,以栈的形式存储
const effectStack = [];
function effect(fn) {
// 创建响应式 effect
const effect = createReactiveEffect(fn);
// 默认先执行一次 effect,本质上调用的是传入的 fn 函数
effect();
}
// 创建响应式 effect 函数
function createReactiveEffect(fn) {
// 响应式 effect
const effect = function() {
try {
// 将 effect 保存到全局 effectStack 栈中
effectStack.push(effect);
return fn();
} finally {
// 调用玩依赖后,弹出 effect
effectStack.pop();
}
}
return effect;
}
// 保存对象与其属性依赖关系的 Map,key 是对象,value 是 Map
const targetMap = new WeakMap();
// 跟踪依赖
function track(target, property) {
// 获取全局数组栈 effectStack 中的依赖
const effect = effectStack[effectStack.length - 1];
// 如果存在依赖
if(effect) {
// 取出该对象对应的 Map
let depsMap = targetMap.get(target);
// 如果不存在,则以目标对象为 key,新建的 Map 为 value,保存到 targetMap 中
if(!depsMap) {
targetMap.set(target, depsMap = new Map())
}
// 从 Map 中取出该属性对应的所有 effect
let deps = depsMap.get(property);
// 如果不存在,则以属性为 key,新建的 set 为 value,存储到 depsMap 中
if(!deps) {
depsMap.set(property, deps = new Set());
}
// 判断 Set 中是否已经存在 effect,如果没有,则添加到 deps 中
if(!deps.has(effect)) {
deps.add(effect);
}
}
}
// 执行属性关联的所有 effect
function trigger(target, type, property) {
const depsMap = targetMap.get(target);
if(depsMap) {
let deps = depsMap.get(property);
// 当前属性关联的所有 effect 依次执行
if(deps) {
deps.forEach(effect => effect());
}
}
}
到现在我们 模拟实现 vue3.0 响应式的代码就全部编写完毕啦,当然 vue3.0 的响应式 API 不止 reactive 还包括 ref、computed、watch,其内部原理都是类似的。