本文是对Vue Mastery中Vue 3响应式原理课程的学习与总结。从一段简单的JavaScript代码一步一步实现Vue 3的响应式。
从一段计算总数的JavaScript代码开始
let price = 5;
let quantity = 2;
let total = price * quantity;
console.log(`total is ${total}`); // total is 10
price = 20;
console.log(`total is ${total}`); // total is 10
对于一般的JavaScript代码,修改了price
的值后,很显然total
的值不会更新,因为不是响应式。本文需要做的是一步一步实现Vue 3中建立响应式的方法。
需手动调用的“响应式”
思路
-
将更新变量值的代码保存。
let total = price * quantity;
-
需要时再运行此段代码。
代码实现
总体代码逻辑结构:
let price = 5;
let quantity = 2;
let total = 0;
let effect = () => { total = price * quantity; } // 想要保存的代码
track(); // 调用此方法来保存effect中的代码
effect(); // 首次调用此方法来计算total的值
// do something...
trigger(); // 调用此方法来运行所有保存的代码
首先需要一个dep
集合存储代码,来表示依赖关系。并实现track
函数和trigger
函数。
let dep = new Set(); // 存放代码(effects)的集合(即依赖)
function track() {
dep.add(effect); // 将effect储存
}
function trigger() {
dep.forEach(effect => effect()); // 运行所有报错的代码
}
修改quantity
的值后调用trigger
函数,total
的值便会更新。
console.log(`total is ${total}`); // total is 10
quantity = 3;
console.log(`total is ${total}`); // total is 10
trigger();
console.log(`total is ${total}`); // total is 15
通常我们使用的对象由很多个属性,需要让每个属性都拥有自己的dep
。因此需要为每个对象一个depsMap
,key为对象的属性名,value为对应其属性的dep
。
// let product = { price: 5, quantity: 2 };
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 product = { price: 5, quantity: 2 };
let total = 0;
let effect = () => {
total = product.prioce * product.quantity;
}
track('quantity');
effect();
console.log(`total is ${total}`); // total is 10
product.quantity = 3;
console.log(`total is ${total}`); // total is 10
trigger();
console.log(`total is ${total}`); // total is 15
现在已经有了一个对象多个属性的跟踪依赖的方法。如果有多个响应式对象呢?则再需要一个Map来存储每个响应式对象的depsMap
,称作targetMap
,类型为WeakMap。
WeakMap是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。使用WeakMap是为了防止当某个响应式对象不需要时,Map仍存有该对象的强引用使得对象不会被垃圾回收的情况发生。
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) {
const depsMap = targetMap.get(target);
if (!depsMap) { return; }
let dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => { effect(); })
}
}
改写后的逻辑代码如下:
let product = { price: 5, quantity: 2 };
let total = 0;
let effect = () => {
total = product.prioce * product.quantity;
}
track(product, 'quantity');
effect();
console.log(`total is ${total}`); // total is 10
product.quantity = 3;
console.log(`total is ${total}`); // total is 10
trigger(product);
console.log(`total is ${total}`); // total is 15
最终的数据结构及职责
实现响应式对象
上一部分代码仍然在手动调用track()
与trigger()
来保存与触发effect
。而我们需要让它们能够自动的被调用。
思路
effect
中如果访问了某个属性(GET),则调用track
去保存effect
。- 如果某个属性改变了(SET),则调用
trigger
运行保存了的effect
代码实现
Vue 3中使用了ES6的Reflect
和Proxy
来实现此功能。
function reactive(target) {
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver);
track(target, key);
return result;
},
set(target, key, receiver) {
let oldValue = target[key];
let result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key);
}
return result;
}
};
return new Proxy(target, handler);
}
改写后的逻辑代码如下,total
的值会随着quantity
的更新而更新。
let product = reactive({ price: 5, quantity: 2 });
let total = 0;
let effect = () => {
total = product.price * product.quantity;
// Proxy会调用track(product, 'price')和track(product, 'quantity');
}
effect();
console.log(`total is ${total}`); // total is 10
product.quantity = 3; // Proxy会调用trigger(product, 'quantity');
console.log(`total is ${total}`); // total is 15
然而由于Proxy对GET的拦截,可能会导致一些不必要的track
的调用。如:
console.log("quantity = " + product.quantity);
实际上只应该在effect
中对某属性GET才调用track
。为此需要引入activeEffect
变量。
let activeEffect = null; // 表示正在运行中的effect
// 声明一个名为effect的函数
function effect(eff) {
activeEffect = eff;
activeEffect();
activeEffect = null;
}
// 修改effect的调用方式
effect(() => { total = product.price * product.quantity; });
// 修改track函数
function track(target, key) {
// 仅当acticeEffect时才调用track
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(activeEffect);
}
}
Vue 2的局限
Vue 2中使用了ES5的Object.defineProperty()
去拦截GET和SET。创建了一个响应式对象后,无法再添加新的响应式属性。示例如下(代码并非真正的Vue 2写法):
let product = reactive({ price: 5, quantity: 2 });
product.name = 'Shoes';
effect(() => {
console.log(`Product name is now ${product.name}`);
});
product.name = 'Socks'; // 不会再次运行effect,因为name属性不是响应式的
原因在于Vue 2中GET、SET属性是被添加在对象的各个属性下的,若要给对象添加新的属性,则需要另一种操作:
Vue.set(product, name, 'Socks');
而Vue 3中使用了Proxy
,就可以为对象直接添加新属性,其也是响应式的。
实现响应式变量
之前部分的代码实现了将对象变成响应式对象,但是还没有实现将一个普通变量变成相应时变量。下面代码就无法完全实现响应式:
let product = reactive({ price: 5, quantity: 2 });
let salePrice = 0;
let total = 0;
effect(() => { total = salePrice * product.quantity; });
effect(() => { salePrice = product.price * 0.9; });
当修改product.price
的值后,salePrice
的值会更新,但是total
的值却不会更新。因为salePrice
不是一个响应式变量。我们需要一个方法将普通的变量也转换成响应式变量。ref
接受一个值,并返回一个响应式对象,其只有一个value
属性。
代码实现
一个简单的实现如下,只需创建一个只有value
属性的响应式对象。
function ref(initialValue) {
return reactive({ value: initialValue });
}
Vue 3的实现则使用了对象访问器,思路如下:
function ref(raw) {
const r = {
get value() {
track(r, 'value');
return raw;
},
set value(newVal) {
/**
* 此处只实现核心思想,实际Vue实现上还需其他判断
* 条件,避免出现set出现死递归等错误。例如可以使
* 用简单的 if (raw != newVal) 来判断
*/
raw = newVal;
trigger(r, 'value');
}
}
return r;
}
最后只需将salePrice
也变为响应式变量即可:
let salePrice = ref(0);
computed
计算属性
前面代码的计算逻辑可以使用computed
计算属性来简化代码,如下:
let product = reactive({ price: 5, quantity: 2 });
let salePrice = computed(() => {
return product.price * 0.9;
})
let total = computed(() => {
return salePrice.value * product.quantity;
})
接下来只需要利用之前的响应式变量对computed
计算属性进行实现。
代码实现
function computed(getter) {
let result = ref();
effect(() => { result.value = getter(); });
return result;
}
参考资料
Vue 3 Reactivity - Vue 3 Reactivity | Vue Mastery
【课程】Vue 3响应式原理 (Vue 3 Reactivity)【中英字幕】- Vue Mastery_哔哩哔哩_bilibili