Vue3响应式原理
1. 你需要先了解的
1.1. Set
集合对象。
Set中的元素只会出现一次,即Set中的元素是唯一的。
1.2. Map
Map对象。
一组键值对结构。能够快速通过键查询值。
1.3. WeakMap
WeakMap对象。
同Map一样也是一组键值对的结构,但它的键必须是对象(Object类型),而值可以是任意的。
1.4. Proxy
一个ES6的新特性。
Proxy是一个对象,它封装了另一个对象或函数,并允许我们拦截对其的访问。
Proxy构造器接受2个参数:
- target
- 需要被代理的对象
- handler
- 处理器对象,包含所有trap(用于拦截不同操作)
实例:
let product = { price: 5, quantity: 2 };
let proxiedProduct = new Proxy(product, {
get(target, key) {
console.log('通过代理访问');
return target[key];
},
set(target, key, value) {
console.log('通过代理修改');
target[key] = value;
return true;
}
});
proxiedProduct.quantity = 4;
console.log(proxiedProduct.quantity);
// result
// 通过代理修改
// 通过代理访问
// 4
所以当一个被Proxy封装的对象被访问或被修改时,可以自动触发一些操作。
1.5. Reflect
Reflect是一个对象,提供了多种与Proxy trap签名相同的方法。
使用Reflect是为了确保trap的执行结果与默认行为保持一致,而避免因为手动操作产生的任何副作用。
用Reflect改写一下上面的例子:
let product = { price: 5, quantity: 2 };
let proxiedProduct = new Proxy(product, {
get(target, key, receiver) {
console.log('通过代理访问');
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log('通过代理修改');
return Reflect.set(target, key, value, receiver);
}
});
proxiedProduct.quantity = 4;
console.log(proxiedProduct.quantity);
// 通过代理修改
// 通过代理访问
// 4
2. 响应式原理
2.1. 实现响应式需要解决的问题
- 一个响应式的属性被修改时,需要执行一个或多个回调,这个回调需要被记录在某个地方以备Vue调用
- 一个对象可能包含多个响应式的属性,它们所对应的回调需要被记录在某个地方以备Vue调用
- 可能存在多个对象包含多个响应式的属性,它们所对应的回调需要被记录在某个地方以备Vue调用
- 当一个响应式属性被访问时,记录其所在的回调
- 当一个响应式的属性被修改时,调用其所在的回调
2.2. 收集依赖
这里的依赖就是指当一个响应式的属性被修改后,需要执行的回调,在Vue3中被称为effect。
// 为了解决问题3,我们利用WeakMap数据结构,记录不同对象的依赖,可以直接将对象作为键来查询
const targetMap = new WeakMap()
// track函数用于记录依赖
function track(target, key) {
// 首先查询目标对象是否已经被记录过依赖
let depsMap = targetMap.get(target)
if (!depsMap) {
// 如果目标对象没有被记录过依赖,则在WeakMap中新建一个键值对,目标对象为键,值为一个空Map对象
// 为了解决问题2,我们利用Map数据结构,记录对象中不同属性的依赖,通过属性名来查询
targetMap.set(target, (depsMap = new Map()))
}
// 根据属性名在Map中查询对应的依赖
let dep = depsMap.get(key)
if (!dep) {
// 如果当前属性没有被记录过依赖,则在Map中新建一个键值对,目标属性为键,值为一个空Set对象
// 为了解决问题1,我们利用Set数据结构,记录对象属性所对应的所有依赖,不需要单独查询
depsMap.set(key, (dep = new Set()))
}
// 将依赖添加进目标属性对应的Set对象
// 这里的effect是一个全局变量,它的值就是目标属性的依赖
dep.add(effect)
}
2.4. 自动化收集/执行依赖
现在有了收集和执行依赖的能力,那么我们需要在访问对象属性时收集依赖,修改对象属性时执行依赖。
function trigger(target, key) {
// 首先根据目标对象查询
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
// 再根据目标属性查询
let dep = depsMap.get(key)
if (dep) {
// 查询到对应的依赖后,遍历Set中的依赖并执行
dep.forEach(effect => {
effect()
})
}
}
2.5. 实现响应式
let product = reactive({ price: 5, quantity: 2 });
let total = 0;
let effect = () => {
// product.price和product.quantity被访问后,当前回调会被记录
// 当product.price或product.quantity被修改后,当前回调会被执行
total = product.price * product.quantity;
};
// 首次执行effect用于收集依赖
effect();
console.log(total); // 10
// 修改响应式属性,将会执行其对应的依赖,从而更新total
product.quantity = 3;
console.log(total); // 15