分支切换与cleanup

本文探讨了在前端开发中,特别是在JavaScript和HTML环境下,分支切换可能导致的遗留副作用函数问题。文章详细介绍了如何清除无效的副作用函数关联,以及解决由此引发的栈溢出问题。此外,还讨论了effect的嵌套和避免无限递归循环的策略。

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.okobj.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操作,从而进行收集依赖)

步骤:

1.副作用函数收集与自身关联的依赖集合

1.1.在effect注册副作用函数中为effectFn增添一个属性deps,用来存储依赖集合,2.在track函数中,进行依赖集合的收集

2.将副作用函数从与之关联的所有依赖集合中移除,

1.1.在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.depseffectFn.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.depseffectFn.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.effectsfn();});
}

// 上面的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.depseffectFn.deps.length = 0;
}

function track(target, key) {
	...// 此处的deps是上面的 1.effects// effects添加副作用函数deps.add(activeFn);// 收集effectFn的依赖集合activeFn.deps.push(deps);
} 

1.当设置响应式对象的值时,触发trigger函数,遍历依赖集合,
2.遍历的过程中,每个回合,被包裹的副作用函数执行,

1.1.cleanup,把副作用函数从依赖集合中删除2.触发副作用函数3.副作用函数执行触发响应式数据的get操作,重新收集依赖函数

3.继续遍历

所以: 在遍历的过程中,每个回合删除元素,增加元素,导致遍历无法结束,导致栈溢出。

问题简单用代码展示如下:

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. 嵌套的effecteffect

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 
原因:

1.执行effect(effect1)代码
2.执行effectFn
3.effectFn函数中,activeFn包裹的副作用函数为effect1
4.执行effect1
5.触发了effect(effect2),此时effect1还没有被收集
6.执行effectFn
7.effectFn函数中,activeFn包裹的副作用函数为effect2
8.执行effect2
9.effect2被收集,effect2执行完成
10.继续执行effect1,此时activeFn包裹的副作用函数仍为effect2
11.所以此时收集的副作用函数又为effect2
12.执行obj.ok = false;
13.遍历对应的依赖集合,触发effect2

支持嵌套

1.需要把正在执行,且没有执行完的被包裹的副作用函数存入栈中
2.当最上面的被包裹的副作用函数执行完,弹出

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添加到effectsToRuneffectsToRun.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添加到effectsToRuneffectsToRun.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.depseffectFn.deps.length = 0;
}

function effect0() {console.log("%cindex.js line:83 obj.text", "color: #007acc;", obj.text);
}

effect(effect0);
obj.text = "hello vue"; 

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值