Vue3响应式原理
什么是响应式?以及为什么需要响应式?
可以参考下面这个例子,我们希望总价total可以根据单价price和数量quantity自动计算,而不是我们每次手动计算。
let price = 10, quantity = 2;
let total = price * quantity;
console.log(`total: ${total}`); // total: 20
price = 20;
console.log(`total: ${total}`); // total: 20
total = total = price * quantity;
console.log(`total: ${total}`); // total: 40
简单的来说,我们希望能够监听某个值的变化,并且在这个值发生变化后,能够执行相应的操作
实现单个值的手动响应
要实现这个响应式,我们需要解决3个问题:
- 需要知道什么时候值发生了变化(when)
- 需要知道值变化了要执行什么操作(what)
- 在值变化的时候,执行对应的操作(do)
我们先来解决what和do,我们新增三个方法effect、track、trigger
let price = 10, quantity = 2, total = 0;
const dep = new Set(); // ①
const effect = () => { total = price * quantity };
const track = () => { dep.add(effect) }; // ②
const trigger = () => { dep.forEach( effect => effect() )}; // ③
track();
console.log(`total: ${total}`); // total: 0
trigger();
console.log(`total: ${total}`); // total: 20
price = 20;
trigger();
console.log(`total: ${total}`); // total: 40
quantity = 3;
trigger();
console.log(`total: ${total}`); // total: 60
我们创建一个dep的Set,来保存数据变化时要执行的操作
effect函数我们用来定义要执行的操作,这里我们用来计算total总价,
track函数我们用来想dep中添加要执行的操作
trigger函数则会帮我们执行所有需要执行的函数
这样,我们在使用的时候,先使用track添加要执行的操作,然后在对应值变化后,再调用trigger来执行对应的逻辑。
但是这样和我们想要的响应式还是有一点距离,现在每次改变数据后都需要手动执行trigger函数,做不到自动执行,我们后面再来优化这个问题。
实现单个对象的多个属性的手动响应
现在我们先考虑另一个问题,如果数据是对象,并且有多个对象,我们应该怎么来保存要执行的操作,以及如何来执行操作呢?
这里用到了ES6新的数据类型Map,相对于直接使用对象来存储键值对,Map可以支持更多的数据类型来作为键,而不像对象仅支持字符串来作为键。
这里我们用depsMap来存储不用的属性对应的在数据变化时要做的操作
let product = {price: 10, quantity: 2}, total;
const depsMap = new Map();
function track(key) {
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}
function trigger(key) {
let dep = depsMap.get(key);
if (dep) {
dep.forEach((effect) => {
effect();
})
}
}
let effect = () => {
total = product.price * product.quantity;
}
effect();
console.log(total); // total: 20
track("quantity");
product.quantity = 3;
trigger("quantity");
console.log(total); // total: 30
track("price");
product.price = 20;
trigger("price");
console.log(total); // total: 60
对多个对象的多个属性实现手动响应
要实现对多个对象的多个属性进行响应,我们实现的思路和之前类似。之前我们用了一个Map来保存不同属性的要执行的操作。现在我们也可以再使用一个WeakMap来保存不同对象的要执行的操作。
let product = { price: 5 };
let sales = { quantity: 2 }
let total = 0;
const targetMap = new WeakMap();
function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}
function trigger(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
let dep = depsMap.get(key);
if (dep) {
dep.forEach((effect) => {
effect();
});
}
}
let effect = () => {
total = product.price * sales.quantity;
};
effect();
console.log(total); // total: 10
track(product, "price");
track(salse, "quantity");
product.price = 10;
trigger(product, "price");
console.log(total); // total: 20
sales.quantity = 3;
trigger(salse, "quantity");
console.log(total); // total: 30
到目前为止,我们不同对象上不同的键的关系如下图:
变为自动响应
我们使用Proxy和Reflect来实现自动响应
const targetMap = new WeakMap();
function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}
function trigger(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
let dep = depsMap.get(key);
if (dep) {
dep.forEach((effect) => {
effect();
});
}
}
let effect = () => {
total = product.price * sales.quantity;
};
function reactive(target) {
const handlers = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver);
track(target, key);
return result;
},
set(target, key, value, receiver) {
let oldValue = target[key];
let result = Reflect.set(target, key, value, receiver);
if (result && oldValue != value) {
trigger(target, key);
}
return result;
}
}
return new Proxy(target, handlers);
}
let product = reactive({ price: 5, quantity: 2 });
let total = 0;
effect();
console.log(total); // output: 10
product.quantity = 3;
console.log(total); // output: 15
优化自动响应过程
上述代码还有两个问题:
effect是固定的,不能设置不同的effect- 在我们设置
quantity为3的时候,trigger调用了对应的effect,这里的effect函数执行来计算的total时,会再走一遍proxy中的get流程。所以就会再次触发track的流程,但是我们并不需要触发track然后再保存一遍effect。
可以优化这两个问题
我们增加一个全局变量activeEffect,默认为null,用来保存需要被track函数保存的操作,当activeEffect为null时,track函数不做任何操作。
function track(target, key) {
if (activeEffect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}
}
然后修改effect函数的实现,使得只有在调用effect函数时,activeEffect才可能有值,可以执行track函数对应的逻辑
function effect(eff) {
activeEffect = eff;
activeEffect();
activeEffect = null;
}
完整的代码如下,其余部分没有做修改
const targetMap = new WeakMap();
let activeEffect = null;
function track(target, key) {
if (activeEffect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}
}
function trigger(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
let dep = depsMap.get(key);
if (dep) {
dep.forEach((effect) => {
effect();
});
}
}
function reactive(target) {
const handlers = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver);
track(target, key);
return result;
},
set(target, key, value, receiver) {
let oldValue = target[key];
let result = Reflect.set(target, key, value, receiver);
if (result && oldValue != value) {
trigger(target, key);
}
return result;
}
}
return new Proxy(target, handlers);
}
function effect(eff) {
activeEffect = eff;
activeEffect();
activeEffect = null;
}
let product = reactive({ price: 5, quantity: 2 });
let salePrice = 0;
let total = 0;
effect(() => {
total = product.price * product.quantity;
});
effect(() => {
salePrice = product.price * 0.9;
});
console.log(total, salePrice); // output: 10, 4.5
// 设置quantity只会重新计算total
product.quantity = 3;
console.log(total, salePrice); // output: 15, 4.5
// 设置price后,total和salePrice对应的effect都会执行,都会被重新计算
product.price = 10;
console.log(total, salePrice); // output: 30, 9
Vue3 Composition API 中 reactive 方法的实现流程已经被我们手动大致实现了
实现ref
在 Vue3 Composition API 设计中,reactive 主要用于引用类型,另外专门提供了一个 ref 方法实现对原始类型的响应式。
function ref(raw) {
const r = {
get value() {
// 在get之前,先保存到targetMap中
track(r, 'value');
return raw;
},
set value(newVal) {
raw = newVal;
// set了之后,触发effect更新
trigger(r, 'value');
},
};
return r;
}
实现computed
function computed(getter) {
// 创建一个响应式的引用
let result = ref();
// 用effect封装着调用getter,将结果设给result.value的同时,也将eff保存在了targetMap中的对应位置
effect(() => (result.value = getter()));
// 最后把result返回
return result;
}
Vue3源码
Vue3 整体是用 Typescript 写的,reactivity 是一个独立的模块,源代码位于packages/reactivity/src目录下,几个不同的方法分别位于不同文件中