三、activeEffect 和 ref
在上一节代码中,还存在一个问题: track
函数中的依赖( effect
函数)是外部定义的,当依赖发生变化, track
函数收集依赖时都要手动修改其依赖的方法名。
比如现在的依赖为 foo
函数,就要修改 track
函数的逻辑,可能是这样:
const foo = () => { /**/ };
const track = (target, key) => { // ②
// ...
dep.add(foo);
}
那么如何解决这个问题呢?
1. 引入 activeEffect 变量
接下来引入 activeEffect
变量,来保存当前运行的 effect 函数。
let activeEffect = null;
const effect = eff => {
activeEffect = eff; // 1. 将 eff 函数赋值给 activeEffect
activeEffect(); // 2. 执行 activeEffect
activeEffect = null;// 3. 重置 activeEffect
}
然后在 track
函数中将 activeEffect
变量作为依赖:
const track = (target, key) => {
if (activeEffect) { // 1. 判断当前是否有 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); // 2. 添加 activeEffect 依赖
}
}
使用方式修改为:
effect(() => {
total = product.price * product.quantity
});
这样就可以解决手动修改依赖的问题,这也是 Vue3 解决该问题的方法。完善一下测试代码后,如下:
const targetMap = new WeakMap();
let activeEffect = null; // 引入 activeEffect 变量
const effect = eff => {
activeEffect = eff; // 1. 将副作用赋值给 activeEffect
activeEffect(); // 2. 执行 activeEffect
activeEffect = null;// 3. 重置 activeEffect
}
const track = (target, key) => {
if (activeEffect) { // 1. 判断当前是否有 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); // 2. 添加 activeEffect 依赖
}
}
const trigger = (target, key) => {
const depsMap = targetMap.get(target);
if (!depsMap) return;
let dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect());
}
};
const reactive = (target) => {
const handler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key);
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue != result) {
trigger(target, key);
}
return result;
}
}
return new Proxy(target, handler);
}
let product = reactive({ price: 10, quantity: 2 });
let total = 0, salePrice = 0;
// 修改 effect 使用方式,将副作用作为参数传给 effect 方法
effect(() => {
total = product.price * product.quantity
});
effect(() => {
salePrice = product.price * 0.9
});
console.log(total, salePrice); // 20 9
product.quantity = 5;
console.log(total, salePrice); // 50 9
product.price = 20;
console.log(total, salePrice); // 100 18
思考一下,如果把第一个 effect
函数中 product.price
换成 salePrice
会如何:
effect(() => {
total = salePrice * product.quantity
});
effect(() => {
salePrice = product.price * 0.9
});
console.log(total, salePrice); // 0 9
product.quantity = 5;
console.log(total, salePrice); // 45 9
product.price = 20;
console.log(total, salePrice); // 45 18
得到的结果完全不同,因为 salePrice
并不是响应式变化,而是需要调用第二个 effect
函数才会变化,也就是 product.price
变量值发生变化。
代码地址: https://github.com/Code-Pop/vue-3-reactivity/blob/master/05-activeEffect.js
2. 引入 ref 方法
熟悉 Vue3 Composition API 的朋友可能会想到 Ref,它接收一个值,并返回一个响应式可变的 Ref 对象,其值可以通过 value
属性获取。
ref:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value。
官网的使用示例如下:
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
我们有 2 种方法实现 ref 函数:
- 使用
reactive
函数
const ref = intialValue => reactive({value: intialValue});
这样是可以的,虽然 Vue3 不是这么实现。
- 使用对象的属性访问器(计算属性)
属性方式包括:getter 和 setter。
const ref = raw => {
const r = {
get value(){
track(r, 'value');
return raw;
},
set value(newVal){
raw = newVal;
trigger(r, 'value');
}
}
return r;
}
使用方式如下:
let product = reactive({ price: 10, quantity: 2 });
let total = 0, salePrice = ref(0);
effect(() => {
salePrice.value = product.price * 0.9
});
effect(() => {
total = salePrice.value * product.quantity
});
console.log(total, salePrice.value); // 18 9
product.quantity = 5;
console.log(total, salePrice.value); // 45 9
product.price = 20;
console.log(total, salePrice.value); // 90 18
在 Vue3 中 ref 实现的核心也是如此。
代码地址: https://github.com/Code-Pop/vue-3-reactivity/blob/master/06-ref.js