阅读 61

分支切换与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.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. effect注册副作用函数中为effectFn增添一个属性deps,用来存储依赖集合,

    2. track函数中,进行依赖集合的收集

  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.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); } 复制代码

  1. 当设置响应式对象的值时,触发trigger函数,遍历依赖集合,

  2. 遍历的过程中,每个回合,被包裹的副作用函数执行,


    1. cleanup,把副作用函数从依赖集合中删除

    2. 触发副作用函数

    3. 副作用函数执行触发响应式数据的get操作,重新收集依赖函数

  1. 继续遍历

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

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

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添加到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


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐