vue開(kāi)發(fā)設(shè)計(jì)分支切換與cleanup實(shí)例詳解
分支切換與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操作中,賦值,然后調(diào)用effect函數(shù)
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";
});
當(dāng)代碼字段obj.ok發(fā)生變化時(shí),代碼執(zhí)行的分支會(huì)跟著變化,這就是分支切換。
分支切換可能會(huì)產(chǎn)生遺留的副作用函數(shù)。
上面代碼中有個(gè)三元運(yùn)算式,如果obj.ok = true,則展示obj.text,此時(shí),effectFn執(zhí)行會(huì)觸發(fā)obj.ok和obj.text的讀取操作,否則展示"not"
此時(shí)的依賴收集如下圖展示:

const data = { ok: true, text: "hello world" };
const obj = reactive(data);
effect(function effectFn(){
document.body.innerText = obj.ok ? obj.text : "not";
});
分支切換導(dǎo)致的問(wèn)題
當(dāng)發(fā)生obj.ok改變且為false時(shí),此時(shí)obj.text對(duì)應(yīng)的依賴effectFn不會(huì)執(zhí)行,
但是obj.text發(fā)生改變時(shí),對(duì)應(yīng)的effectFn卻會(huì)執(zhí)行,頁(yè)面的內(nèi)容會(huì)被修改掉。這是不期望發(fā)生的!
此時(shí),是key為ok對(duì)應(yīng)的effectFn依舊有效,
key為text對(duì)應(yīng)的effectFn為無(wú)效,應(yīng)該清除掉,如下圖展示


如何清除掉副作用函數(shù)的無(wú)效關(guān)聯(lián)關(guān)系?
- 每次副作用函數(shù)執(zhí)行前,可以先把它從所有與之關(guān)聯(lián)的依賴集合中刪除,然后清空依賴集合的收集,
- 當(dāng)副作用函數(shù)執(zhí)行,所有會(huì)重新建立關(guān)聯(lián)。(副作用函數(shù)中,會(huì)重新執(zhí)行響應(yīng)式數(shù)據(jù)的get操作,從而進(jìn)行收集依賴)
步驟:
- 副作用函數(shù)收集與自身關(guān)聯(lián)的依賴集合
在
effect注冊(cè)副作用函數(shù)中為effectFn增添一個(gè)屬性deps,用來(lái)存儲(chǔ)依賴集合,在
track函數(shù)中,進(jìn)行依賴集合的收集- 將副作用函數(shù)從與之關(guān)聯(lián)的所有依賴集合中移除,
在
effect注冊(cè)副作用函數(shù)中,觸發(fā)副作用函數(shù)前,清除副作用函數(shù)的依賴集合
疑問(wèn):為什么對(duì)傳入的副作用函數(shù)進(jìn)行一層包裹?
- 為了對(duì)副作用函數(shù)進(jìn)行更多操作,
- 為副作用函數(shù)增加deps屬性,作為收集依賴集合的容器
- 清除副作用函數(shù)的依賴集合
function effect(fn) {
const effectFn = () => {
activeFn = effectFn;
cleanup(effectFn);
fn();
};
effectFn.deps = [];
effectFn();
}
function cleanup(effectFn) {
// 從副作用函數(shù)關(guān)聯(lián)的依賴集合中刪除副作用函數(shù),從而斷開(kāi)關(guān)聯(lián)
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);
}
完整代碼
// 響應(yīng)式數(shù)據(jù)的基本實(shí)現(xiàn)
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操作中,賦值,然后調(diào)用effect函數(shù)
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) {
// 從副作用函數(shù)關(guān)聯(lián)的依賴集合中刪除副作用函數(shù),從而斷開(kāi)關(guān)聯(lián)
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";
產(chǎn)生的問(wèn)題:代碼運(yùn)行發(fā)生棧溢出
具體問(wèn)題代碼:
obj.text = "hello vue";
// 觸發(fā)trigger函數(shù)
function trigger(target, key) {
...
// 調(diào)用包裝的副作用函數(shù)
effects.forEach((fn) => { // 1.effects
fn();
});
}
// 上面的fn
const effectFn = () => {
activeFn = effectFn;
// 把副作用函數(shù)從依賴集合中刪除
cleanup(effectFn);
// 執(zhí)行副作用函數(shù),重新收集依賴
fn();
};
function cleanup(effectFn) {
// 從副作用函數(shù)關(guān)聯(lián)的依賴集合中刪除副作用函數(shù),從而斷開(kāi)關(guān)聯(lián)
for (const deps of effectFn.deps) { // 此處的deps是上面的 1.effects
// deps刪除effectFn
// effects中的副作用函數(shù)減少
deps.delete(effectFn);
}
// 重置effectFn.deps
effectFn.deps.length = 0;
}
function track(target, key) {
...
// 此處的deps是上面的 1.effects
// effects添加副作用函數(shù)
deps.add(activeFn);
// 收集effectFn的依賴集合
activeFn.deps.push(deps);
}
- 當(dāng)設(shè)置響應(yīng)式對(duì)象的值時(shí),觸發(fā)
trigger函數(shù),遍歷依賴集合, - 遍歷的過(guò)程中,每個(gè)回合,被包裹的副作用函數(shù)執(zhí)行,
cleanup,把副作用函數(shù)從依賴集合中刪除觸發(fā)副作用函數(shù)
副作用函數(shù)執(zhí)行觸發(fā)響應(yīng)式數(shù)據(jù)的
get操作,重新收集依賴函數(shù)- 繼續(xù)遍歷
所以: 在遍歷的過(guò)程中,每個(gè)回合刪除元素,增加元素,導(dǎo)致遍歷無(wú)法結(jié)束,導(dǎo)致棧溢出。
問(wèn)題簡(jiǎn)單用代碼展示如下:
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();
});
}
嵌套的effect與effect棧
effect嵌套的場(chǎng)景?
在Vue中,Vue的渲染函數(shù)就是在一個(gè)effect中執(zhí)行的
主要的場(chǎng)景是:組件嵌套組件。
如果不支持effect嵌套,產(chǎn)生的后果
初始化
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
原因:
- 執(zhí)行
effect(effect1)代碼 - 執(zhí)行
effectFn effectFn函數(shù)中,activeFn包裹的副作用函數(shù)為effect1- 執(zhí)行
effect1 - 觸發(fā)了
effect(effect2),此時(shí)effect1還沒(méi)有被收集 - 執(zhí)行
effectFn effectFn函數(shù)中,activeFn包裹的副作用函數(shù)為effect2- 執(zhí)行
effect2 effect2被收集,effect2執(zhí)行完成- 繼續(xù)執(zhí)行
effect1,此時(shí)activeFn包裹的副作用函數(shù)仍為effect2 - 所以此時(shí)收集的副作用函數(shù)又為
effect2 - 執(zhí)行
obj.ok = false; - 遍歷對(duì)應(yīng)的依賴集合,觸發(fā)
effect2
支持嵌套
- 需要把正在執(zhí)行,且沒(méi)有執(zhí)行完的被包裹的副作用函數(shù)存入棧中
- 當(dāng)最上面的被包裹的副作用函數(shù)執(zhí)行完,彈出
const effectStack = [];
function effect(fn) {
const effectFn = () => {
activeFn = effectFn;
cleanup(effectFn);
// 把當(dāng)前執(zhí)行的函數(shù)壓入棧中
effectStack.push(effectFn);
fn();
// 函數(shù)執(zhí)行完畢,彈出
effectStack.pop();
// activeFn賦值為還未執(zhí)行完的副作用函數(shù)
activeFn = effectStack[effectStack.length - 1];
};
effectFn.deps = [];
effectFn();
}
避免無(wú)限遞歸循環(huán)
產(chǎn)生無(wú)限遞歸循環(huán)的代碼:
const data = {foo : 1}
const obj = reactive(data)
effect(()=> obj.foo++)
原因分析:
() => {
obj.foo = obj.foo + 1
}
obj.foo在讀取自身之后又設(shè)置自身
- 讀取
obj.foo會(huì)觸發(fā)track track收集依賴后,然后繼續(xù)執(zhí)行上面的賦值操作- 設(shè)置
obj.foo會(huì)觸發(fā)trigger - 然后遍歷依賴集合,再次觸發(fā)
obj.foo的讀取 - 循環(huán)
解決循環(huán)
- 設(shè)置和讀取是在一個(gè)副作用函數(shù)中進(jìn)行的,都是
activeEffect - 如果
trigger觸發(fā)執(zhí)行的副作用函數(shù)與當(dāng)前正在執(zhí)行的副作用函數(shù)相同,則不觸發(fā)執(zhí)行
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) {
// 當(dāng)觸發(fā)的fn與當(dāng)前執(zhí)行的副作用函數(shù)不同時(shí)
// 將fn添加到effectsToRun
effectsToRun.add(fn);
}
});
effectsToRun.forEach((fn) => {
fn();
});
}
完整代碼
// 響應(yīng)式數(shù)據(jù)的基本實(shí)現(xiàn)
let activeFn = undefined;
const bucket = new WeakMap();
// 副作用函數(shù)調(diào)用棧
const effectStack = [];
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
return target[key];
},
// 在set操作中,賦值,然后調(diào)用effect函數(shù)
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) {
// 當(dāng)觸發(fā)的fn與當(dāng)前執(zhí)行的副作用函數(shù)不同時(shí)
// 將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);
// 把當(dāng)前執(zhí)行的函數(shù)壓入棧中
effectStack.push(effectFn);
fn();
// 函數(shù)執(zhí)行完畢,彈出
effectStack.pop();
// activeFn賦值為還未執(zhí)行完的副作用函數(shù)
activeFn = effectStack[effectStack.length - 1];
};
effectFn.deps = [];
effectFn();
}
function cleanup(effectFn) {
// 從副作用函數(shù)關(guān)聯(lián)的依賴集合中刪除副作用函數(shù),從而斷開(kāi)關(guān)聯(lián)
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開(kāi)發(fā)設(shè)計(jì)分支切換與cleanup實(shí)例詳解的詳細(xì)內(nèi)容,更多關(guān)于vue開(kāi)發(fā)設(shè)計(jì)分支切換cleanup的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue3+Vue Router實(shí)現(xiàn)動(dòng)態(tài)路由導(dǎo)航的示例代碼
隨著單頁(yè)面應(yīng)用程序(SPA)的日益流行,前端開(kāi)發(fā)逐漸向復(fù)雜且交互性強(qiáng)的方向發(fā)展,在這個(gè)過(guò)程中,Vue.js及其生態(tài)圈的工具(如Vue Router)為我們提供了強(qiáng)大的支持,本文將介紹如何在Vue 3中使用Vue Router實(shí)現(xiàn)動(dòng)態(tài)路由導(dǎo)航,需要的朋友可以參考下2024-08-08
Vue 通過(guò)公共字段,拼接兩個(gè)對(duì)象數(shù)組的實(shí)例
今天小編就為大家分享一篇Vue 通過(guò)公共字段,拼接兩個(gè)對(duì)象數(shù)組的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11
解決vue數(shù)據(jù)更新但table內(nèi)容不更新的問(wèn)題
這篇文章主要給大家介紹了vue數(shù)據(jù)更新table內(nèi)容不更新解決方法,文中有詳細(xì)的代碼示例供大家作為參考,感興趣的同學(xué)可以參考閱讀一下2023-08-08
Vue Element前端應(yīng)用開(kāi)發(fā)之動(dòng)態(tài)菜單和路由的關(guān)聯(lián)處理
這篇文章主要介紹了Vue Element前端應(yīng)用開(kāi)發(fā)之動(dòng)態(tài)菜單和路由的關(guān)聯(lián)處理,對(duì)vue感興趣的同學(xué),可以參考下2021-05-05
Vue 實(shí)例中使用$refs的注意事項(xiàng)
這篇文章主要介紹了Vue 實(shí)例中使用$refs的注意事項(xiàng),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-01-01
vue動(dòng)態(tài)生成新表單并且添加驗(yàn)證校驗(yàn)規(guī)則方式
這篇文章主要介紹了vue動(dòng)態(tài)生成新表單并且添加驗(yàn)證校驗(yàn)規(guī)則方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10
vue依賴包報(bào)錯(cuò)問(wèn)題eslint\lib\cli-engine\cli-engine.js:421
這篇文章主要介紹了vue依賴包報(bào)錯(cuò)問(wèn)題eslint\lib\cli-engine\cli-engine.js:421,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
VUE如何利用vue-print-nb實(shí)現(xiàn)打印功能詳解
這篇文章主要給大家介紹了關(guān)于VUE如何利用vue-print-nb實(shí)現(xiàn)打印功能的相關(guān)資料,文中還給大家介紹了vue-print-nb使用中的常見(jiàn)問(wèn)題,如空白頁(yè),需要的朋友可以參考下2022-04-04

