问题
在代码运行时,副作用函数关联的响应式对象属性可能不同。比如下面的三元运算符。
const data = {
obj: true,
text1: "hello world 1",
text2: "hello world 2",
};
const obj = new Proxy(data, ...)
function effectOk() {
console.log("effectOk", obj.ok ? obj.text1: obj.text2);
}
代码一开始,由于obj.ok为true,副作用函数等价于:
function effectOk() {
console.log("effectOk", obj.text1)
}
副作用函数effectOk和obj.text1相关联。
假设在2s之后,obj.ok变为false,这时候的副作用函数等价于
function effectOk() {
console.log("effectOk", obj.text2)
}
副作用函数effectOk和obj.text2相关联;而和obj1.text1无关联。 我们期望,后续obj.text2的变化会导致副作用函数effectOk的运行;而obj.text1的变化不会导致副作用函数effectOk的运行。
但我们运行代码发现,结果不是这样的。
根据上一节,我们的effect.js的代码如下:
// effect.js
// 存放副作用函数的集合容器,用Set数据结构,是为了防止相同的副作用函数重复收集
const bucket = new WeakMap();
// 表示当前正在运行的副作用函数
let activeEffect = null;
// 用于执行副作用函数的函数
export function myEffect(fn) {
activeEffect = fn;
fn(); // 执行副作用函数
}
// 响应式对象。响应式对象为原始对象的Proxy代理
export const myReactive = (data) => new Proxy(data, {
get(target, key) {
if(!activeEffect) return target[key]
track(target, key);
return target[key];
},
set(target, key, val) {
target[key] = val;
trigger(target, key);
return true;
},
});
function track(target, key){
if(!activeEffect) return
let depsMap = bucket.get(target)
if(!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
let deps = depsMap.get(key)
if(!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
debug.js如下:
// debug.js
import { myEffect, myReactive } from './effect.js'
// 原始对象,包含两个属性
const data = {
ok: true,
text1: "hello world 1",
text2: "hello world 2",
};
// 响应式对象
const obj = myReactive(data);
// 定义第一个副作用函数
function effectOk() {
console.log("effectOk", obj.ok ? obj.text1: obj.text2);
}
// 初始化依次执行副作用函数,触发 get
myEffect(effectOk);
// 模拟2s后修改ok
setTimeout(() => {
obj.ok = false;
}, 2000);
// 模拟4s后修改text1
setTimeout(() => {
obj.text1 = 'HELLO WORLD 1';
}, 4000)
// 初始化输出如下:
// effectOk hello world 1
// 2s之后输出如下:
// effectOk hello world 2
// 4s之后输入如下:
// effectOk hello world 2
我们在2s之后将obj.ok的值由true变为false,然后我们再4s的时候观察,我们改变obj.text1的值的时候是否执行副作用函数。 结果发现obj.text1的变化,会导致运行副作用函数effectOk!
分析
简单梳理下为什么会出现上面的现象。
- 副作用函数初始化运行时,由于obj.ok为true,触发obj.text1的读操作。根据track代码,副作用函数effectOk会被塞入obj.text1关联的deps中(Set数据结构)。
- 后续obj.ok变为false,触发obj.text2的读操作。根据track代码,副作用函数effectOk会被塞入obj.text2关联的deps中。
- 最后obj.text1更新,触发trigger函数。根据trigger代码,会将obj.text1对应的deps里所有副作用函数逐一执行。
上述第2步,我们虽然将副作用函数的关联属性从之前的obj.text1变为ob.text2,但是没有将副作用函数从obj.text1对应的deps里移除,结果导致了在第3步中,也出发了副作用函数的运行。因此我们需要修改下相关代码。
修改的思路如下:
每次运行副作用函数的时候,将该副作用函数从与之关联的所有对象属性的deps中移除。这样,就不会存在副作用函数还会遗留在之前的对象属性的deps中了。
随之而来的第二个问题是,我们如何获取副作用函数的关联的对象属性呢? 为此我们可以将副作用函数也增加一个属性deps(为Array数据结构),这个deps用于保存所有关联的对象属性。
let activeEffect = null
function myEffect(fn) {
const effectFn = () => {
cleanup(effectFn); // 每次运行副作用函数,清空和其他对象属性的关联关系
activeEffect = effectFn;
fn();
};
effectFn.deps = []; // 用于保存该副作用函数所关联的对象属性
effectFn();
}
cleanup函数定义如下:
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]; // deps为对象属性关联的deps
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
track函数修改如下:
function track(target, key) {
if(!activeEffect) return
let depsMap = bucket.get(target)
if(!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
let deps = depsMap.get(key)
if(!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
// 将deps增加到activeEffect的deps中
activeEffect.deps.push(deps)
}
trigger函数修改如下:
function trigger(target, key) {
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set(effects) // 拷贝一份新的Set快照数据
effectsToRun && effectsToRun.forEach(fn => fn())
}
trigger代码中,之所以拷贝一份effects,是因为在运行副作用函数的时候,会调用cleanup,将副作用函数从当前的effects中移除;但由于副作用函数的运行,又会触发track函数的执行,导致副作用函数又会被添加到effects中,导致Set的forEach的方法会无限运行。 解决方法就是在执行trigger的时候,拷贝一份当前的effect的快照effectsToRun,然后forEach这个快照。
完整代码见这里
总结
响应式系统中,副作用函数的遗留处理,主要的思路就是:在每次副作用函数运行时候,将副作用函数和之前的对象属性解除关联,然后将副作用函数和当前的对象属性重新关联。这样就能保证历史的关联关系不复存在了,而当前的关联关系是最新的。
代码
参考
- 《Vue.js设计与实现》,作者:霍春阳,ISBN: 9787115583864