代码示例如下
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为无效,应该清除掉,如下图展示
步骤:
在effect
注册副作用函数中为effectFn
增添一个属性deps
,用来存储依赖集合,
在track
函数中,进行依赖集合的收集
在effect
注册副作用函数中,触发副作用函数前,清除副作用函数的依赖集合
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(); }); }
在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(); }
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";