分支切换与cleanup
4. 分支切换与cleanup
了解分支切换,代码示例如下
const data = { ok: true, text: "hello world" };
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
return target[key];
},
// 在set操作中,赋值,然后调用effect函数
set(target, key, value) {
target[key] = value;
trigger(target, key);
return true;
},
});
}
const obj = reactive(data);
effect(function effectFn(){
document.body.innerText = obj.ok ? obj.text : "not";
});
当代码字段obj.ok
发生变化时,代码执行的分支会跟着变化,这就是分支切换。
分支切换可能会产生遗留的副作用函数。
上面代码中有个三元运算式,如果obj.ok = true
,则展示obj.text
,此时,effectFn
执行会触发obj.ok
和obj.text
的读取操作,否则展示"not"
此时的依赖收集如下图展示:
const data = { ok: true, text: "hello world" };
const obj = reactive(data);
effect(function effectFn(){
document.body.innerText = obj.ok ? obj.text : "not";
});
分支切换导致的问题
当发生obj.ok
改变且为false
时,此时obj.text
对应的依赖effectFn
不会执行,
但是obj.text
发生改变时,对应的effectFn
却会执行,页面的内容会被修改掉。这是不期望发生的!
此时,是key为ok对应的effectFn依旧有效,
key为text对应的effectFn为无效,应该清除掉,如下图展示
如何清除掉副作用函数的无效关联关系?
- 每次副作用函数执行前,可以先把它从所有与之关联的依赖集合中删除,然后清空依赖集合的收集,
- 当副作用函数执行,所有会重新建立关联。(副作用函数中,会重新执行响应式数据的get操作,从而进行收集依赖)
步骤:
- 副作用函数收集与自身关联的依赖集合
-
- 在
effect
注册副作用函数中为effectFn
增添一个属性deps
,用来存储依赖集合, - 在
track
函数中,进行依赖集合的收集
- 在
- 将副作用函数从与之关联的所有依赖集合中移除,
-
- 在
effect
注册副作用函数中,触发副作用函数前,清除副作用函数的依赖集合
- 在
疑问:为什么对传入的副作用函数进行一层包裹?
- 为了对副作用函数进行更多操作,
-
- 为副作用函数增加deps属性,作为收集依赖集合的容器
- 清除副作用函数的依赖集合
function effect(fn) {
const effectFn = () => {
activeFn = effectFn;
cleanup(effectFn);
fn();
};
effectFn.deps = [];
effectFn();
}
function cleanup(effectFn) {
// 从副作用函数关联的依赖集合中删除副作用函数,从而断开关联
for (const deps of effectFn.deps) {
deps.delete(effectFn);
}
// 重置effectFn.deps
effectFn.deps.length = 0;
}
// 收集effectFn的依赖集合
function track(target, key) {
if (!activeFn) return target[key];
let depMap = bucket.get(target);
if (!depMap) {
depMap = new Map();
bucket.set(target, depMap);
}
let deps = depMap.get(key);
if (!deps) {
deps = new Set();
depMap.set(key, deps);
}
deps.add(activeFn);
// 收集effectFn的依赖集合
activeFn.deps.push(deps);
}
完整代码
// 响应式数据的基本实现
let activeFn = undefined;
const bucket = new WeakMap();
let times = 0;
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log(target, key);
if (times > 10) {
throw "超出";
}
times++;
console.log(times);
track(target, key);
return target[key];
},
// 在set操作中,赋值,然后调用effect函数
set(target, key, value) {
target[key] = value;
trigger(target, key);
return true;
},
});
}
// 收集effectFn的依赖集合
function track(target, key) {
console.log("track");
if (!activeFn) return target[key];
let depMap = bucket.get(target);
if (!depMap) {
depMap = new Map();
bucket.set(target, depMap);
}
let deps = depMap.get(key);
if (!deps) {
deps = new Set();
depMap.set(key, deps);
}
deps.add(activeFn);
// 收集effectFn的依赖集合
activeFn.deps.push(deps);
}
function trigger(target, key) {
const depMap = bucket.get(target);
if (!depMap) return;
const effects = depMap.get(key);
if (!effects) return;
effects.forEach((fn) => {
fn();
});
}
const data = { ok: true, text: "hello world" };
const obj = reactive(data);
function effect(fn) {
const effectFn = () => {
activeFn = effectFn;
cleanup(effectFn);
fn();
};
effectFn.deps = [];
effectFn();
}
function cleanup(effectFn) {
// 从副作用函数关联的依赖集合中删除副作用函数,从而断开关联
for (const deps of effectFn.deps) {
deps.delete(effectFn);
}
// 重置effectFn.deps
effectFn.deps.length = 0;
}
function effect0() {
console.log("%cindex.js line:83 obj.text", "color: #007acc;", obj.text);
}
effect(effect0);
obj.text = "hello vue";
产生的问题:代码运行发生栈溢出
具体问题代码:
obj.text = "hello vue";
// 触发trigger函数
function trigger(target, key) {
...
// 调用包装的副作用函数
effects.forEach((fn) => { // 1.effects
fn();
});
}
// 上面的fn
const effectFn = () => {
activeFn = effectFn;
// 把副作用函数从依赖集合中删除
cleanup(effectFn);
// 执行副作用函数,重新收集依赖
fn();
};
function cleanup(effectFn) {
// 从副作用函数关联的依赖集合中删除副作用函数,从而断开关联
for (const deps of effectFn.deps) { // 此处的deps是上面的 1.effects
// deps删除effectFn
// effects中的副作用函数减少
deps.delete(effectFn);
}
// 重置effectFn.deps
effectFn.deps.length = 0;
}
function track(target, key) {
...
// 此处的deps是上面的 1.effects
// effects添加副作用函数
deps.add(activeFn);
// 收集effectFn的依赖集合
activeFn.deps.push(deps);
}
- 当设置响应式对象的值时,触发
trigger
函数,遍历依赖集合, - 遍历的过程中,每个回合,被包裹的副作用函数执行,
-
cleanup
,把副作用函数从依赖集合中删除- 触发副作用函数
- 副作用函数执行触发响应式数据的
get
操作,重新收集依赖函数
- 继续遍历
所以: 在遍历的过程中,每个回合删除元素,增加元素,导致遍历无法结束,导致栈溢出。
问题简单用代码展示如下:
const set = new Set([1])
set.forEach(item => {
set.delete(1)
set.add(1)
console.log('遍历中')
})
如何解决此种情况下的栈溢出?
将遍历effects
变成遍历effects
的拷贝的值,不修改到efftcts就可以了
function trigger(target, key) {
const depMap = bucket.get(target);
if (!depMap) return;
const effects = depMap.get(key);
if (!effects) return;
const effectsToRun = new Set(effects)
effectsToRun.forEach((fn) => {
fn();
});
}
5. 嵌套的effect
与effect
栈
effect
嵌套的场景?
在Vue中,Vue的渲染函数就是在一个effect中执行的
主要的场景是:组件嵌套组件。
如果不支持effect嵌套,产生的后果
初始化
function effect(fn) {
const effectFn = () => {
activeFn = effectFn;
activeFn.fnName = fn.name;
console.log("fnName", activeFn.fnName);
cleanup(effectFn);
fn();
};
effectFn.deps = [];
effectFn();
}
effect(function effect1() {
console.log("effect1");
effect(function effect2() {
console.log("effect2", obj.text);
});
console.log("effect1", obj.ok);
});
// fnName effect1
// effect1
// fnName effect2
// effect2 hello world
// effect1 true
obj.ok = false;
// fnName effect2
// effect2 hello world
原因:
- 执行
effect(effect1)
代码 - 执行
effectFn
effectFn
函数中,activeFn
包裹的副作用函数为effect1
- 执行
effect1
- 触发了
effect(effect2)
,此时effect1
还没有被收集 - 执行
effectFn
effectFn
函数中,activeFn
包裹的副作用函数为effect2
- 执行
effect2
effect2
被收集,effect2
执行完成- 继续执行
effect1
,此时activeFn
包裹的副作用函数仍为effect2
- 所以此时收集的副作用函数又为
effect2
- 执行
obj.ok = false;
- 遍历对应的依赖集合,触发
effect2
支持嵌套
- 需要把正在执行,且没有执行完的被包裹的副作用函数存入栈中
- 当最上面的被包裹的副作用函数执行完,弹出
const effectStack = [];
function effect(fn) {
const effectFn = () => {
activeFn = effectFn;
cleanup(effectFn);
// 把当前执行的函数压入栈中
effectStack.push(effectFn);
fn();
// 函数执行完毕,弹出
effectStack.pop();
// activeFn赋值为还未执行完的副作用函数
activeFn = effectStack[effectStack.length - 1];
};
effectFn.deps = [];
effectFn();
}
6. 避免无限递归循环
产生无限递归循环的代码:
const data = {foo : 1}
const obj = reactive(data)
effect(()=> obj.foo++)
原因分析:
() => {
obj.foo = obj.foo + 1
}
obj.foo
在读取自身之后又设置自身
- 读取
obj.foo
会触发track
track
收集依赖后,然后继续执行上面的赋值操作- 设置
obj.foo
会触发trigger
- 然后遍历依赖集合,再次触发
obj.foo
的读取 - 循环
解决循环
- 设置和读取是在一个副作用函数中进行的,都是
activeEffect
- 如果
trigger
触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
function trigger(target, key) {
const depMap = bucket.get(target);
if (!depMap) return;
const effects = depMap.get(key);
if (!effects) return;
const effectsToRun = new Set();
effects.forEach((fn) => {
if (fn !== activeFn) {
// 当触发的fn与当前执行的副作用函数不同时
// 将fn添加到effectsToRun
effectsToRun.add(fn);
}
});
effectsToRun.forEach((fn) => {
fn();
});
}
完整代码
// 响应式数据的基本实现
let activeFn = undefined;
const bucket = new WeakMap();
// 副作用函数调用栈
const effectStack = [];
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
return target[key];
},
// 在set操作中,赋值,然后调用effect函数
set(target, key, value) {
target[key] = value;
trigger(target, key);
return true;
},
});
}
// 收集effectFn的依赖集合
function track(target, key) {
if (!activeFn) return target[key];
let depMap = bucket.get(target);
if (!depMap) {
depMap = new Map();
bucket.set(target, depMap);
}
let deps = depMap.get(key);
if (!deps) {
deps = new Set();
depMap.set(key, deps);
}
deps.add(activeFn);
// 收集effectFn的依赖集合
activeFn.deps.push(deps);
}
function trigger(target, key) {
const depMap = bucket.get(target);
if (!depMap) return;
const effects = depMap.get(key);
if (!effects) return;
const effectsToRun = new Set();
effects.forEach((fn) => {
if (fn !== activeFn) {
// 当触发的fn与当前执行的副作用函数不同时
// 将fn添加到effectsToRun
effectsToRun.add(fn);
}
});
effectsToRun.forEach((fn) => {
if (fn.options.scheduler) {
fn.options.scheduler(fn);
} else {
fn();
}
});
}
const data = { ok: true, text: "hello world" };
const obj = reactive(data);
function effect(fn) {
const effectFn = () => {
activeFn = effectFn;
cleanup(effectFn);
// 把当前执行的函数压入栈中
effectStack.push(effectFn);
fn();
// 函数执行完毕,弹出
effectStack.pop();
// activeFn赋值为还未执行完的副作用函数
activeFn = effectStack[effectStack.length - 1];
};
effectFn.deps = [];
effectFn();
}
function cleanup(effectFn) {
// 从副作用函数关联的依赖集合中删除副作用函数,从而断开关联
for (const deps of effectFn.deps) {
deps.delete(effectFn);
}
// 重置effectFn.deps
effectFn.deps.length = 0;
}
function effect0() {
console.log("%cindex.js line:83 obj.text", "color: #007acc;", obj.text);
}
effect(effect0);
obj.text = "hello vue";
图片来源:
《Vue.js设计与实现》
转载自:https://juejin.cn/post/7170135046945243166