新聞中心
1、寫(xiě)在前面
上篇文章主要介紹了如何簡(jiǎn)易的實(shí)現(xiàn)一個(gè)響應(yīng)系統(tǒng),只是個(gè)簡(jiǎn)易的仍然存在很多未知的不可控的問(wèn)題,比如副作用函數(shù)嵌套、如何避免無(wú)限遞歸以及多個(gè)副作用函數(shù)之間會(huì)產(chǎn)生什么影響?

本文將會(huì)解決以下幾個(gè)問(wèn)題:
- 分支切換
- 嵌套的effect
- 無(wú)限遞歸
- 可調(diào)度性
2、分支切換與cleanup
分支切換
在進(jìn)行頁(yè)面渲染時(shí),我們需要避免副作用函數(shù)產(chǎn)生的遺留。為什么這么說(shuō)呢?先看下面的代碼片段,在副作用函數(shù)effect內(nèi)部的箭頭函數(shù)中有個(gè)三元表達(dá)式,根據(jù)state.flag的值去切換頁(yè)面渲染的值,這是我們期待的分支切換。
const data = {
name:"pingping",
age:18,
flag:true
};
const state = new Proxy(data,{
/* 其他代碼省略 */
});
//副作用函數(shù),effect執(zhí)行渲染了頁(yè)面
effect(()=>{
console.log("render");
document.body.innerHTML = state.flag ? state.name : state.age;
})
flag的值為初始值true時(shí),頁(yè)面渲染的結(jié)果如圖所示:
但是事實(shí)上,分支切換可能會(huì)產(chǎn)生遺留的副作用函數(shù)。上面代碼片段,flag的初始值是true,此時(shí)會(huì)去響應(yīng)式對(duì)象state中獲取字段flag的值,此時(shí)effect函數(shù)會(huì)執(zhí)行觸發(fā)flag和name的讀取操作,副作用函數(shù)會(huì)與響應(yīng)數(shù)據(jù)之間建立聯(lián)系。
flag初始值為true的時(shí)候,事實(shí)上的Map的key值只有flag和name與副作用函數(shù)建立了聯(lián)系,也只會(huì)收集這兩個(gè)響應(yīng)式數(shù)據(jù)的依賴(lài)--副作用函數(shù)。
flag字段值修改為false時(shí),會(huì)觸發(fā)副作用函數(shù)effect重新執(zhí)行,按道理name的值不會(huì)被讀取,只會(huì)觸發(fā)flag和age的讀取操作,理想情況應(yīng)該是依賴(lài)集合收集的是這兩個(gè)字段所對(duì)應(yīng)的副作用函數(shù)。
副作用函數(shù)與響應(yīng)數(shù)據(jù)之間的關(guān)系
但是事實(shí)上,在上面代碼中實(shí)現(xiàn)不了這種變化,在修改字段flag的值會(huì)觸發(fā)副作用函數(shù)重新執(zhí)行后,整個(gè)依賴(lài)關(guān)系會(huì)保持flag為true時(shí)的關(guān)系圖,name字段所產(chǎn)生的副作用函數(shù)會(huì)遺留。
// 設(shè)置一個(gè)不存在的屬性時(shí)
setTimeout(()=>{
state.flag = false;
},1000)
如上面代碼,遺留的副作用函數(shù)會(huì)導(dǎo)致數(shù)據(jù)不必要的更新,之所以這樣說(shuō),是因?yàn)閒lag的值改為false后,會(huì)觸發(fā)更新導(dǎo)致副作用函數(shù)重新執(zhí)行。此時(shí)應(yīng)該不存在name的依賴(lài)關(guān)系,即不會(huì)讀取name的值了,無(wú)論flag的值怎么變化都應(yīng)該只是讀取age的值而非name。
上面代碼實(shí)際執(zhí)行效果如下圖所示,頁(yè)面的渲染值沒(méi)有改變,控制臺(tái)打印顯示:
// 設(shè)置一個(gè)不存在的屬性時(shí)
setTimeout(()=>{
state.flag = false;
setTimeout(()=>{
console.log("更改了name的值,理論上是不會(huì)更新頁(yè)面數(shù)據(jù)的...");
state.name = "onechuan"
})
},1000)
即使我們?cè)趕etTimeout中繼續(xù)修改name的值,頁(yè)面依然渲染的是name的初始值"pingping",控制臺(tái)顯示我們是修改了name的值的。
cleanup
那么,我們應(yīng)該如何解決上面的副作用函數(shù)遺留問(wèn)題呢?其實(shí),我們只需要設(shè)置在每次副作用函數(shù)觸發(fā)執(zhí)行時(shí),先把它從所有與之相關(guān)聯(lián)的依賴(lài)集合中刪除。當(dāng)副作用函數(shù)執(zhí)行完畢后,會(huì)重新建立聯(lián)系,重新在依賴(lài)集合中收集副作用函數(shù),但是之前遺留的副作用函數(shù)已經(jīng)被清理。『打掃干凈屋子,重新請(qǐng)客』。
清除副作用函數(shù)與響應(yīng)式數(shù)據(jù)之間的聯(lián)系
我們應(yīng)該如何實(shí)現(xiàn)上面的理論呢,得先確定哪些依賴(lài)集合中包含了遺留的副作用函數(shù),我們需要重新設(shè)計(jì)副作用函數(shù)effect。
在effect函數(shù)內(nèi)部定義一個(gè)effectFn函數(shù),為其添加effectFn.deps數(shù)組,用于存儲(chǔ)所有包含當(dāng)前副作用函數(shù)的依賴(lài)集合。在每次執(zhí)行副作用函數(shù)前,都需要根據(jù)effectFn.deps獲取依賴(lài)集合,調(diào)用cleanupEffect函數(shù)完成清理遺留的副作用函數(shù)。
// 全局變量用于存儲(chǔ)被注冊(cè)的副作用函數(shù)
let activeEffect;
// effect用于注冊(cè)副作用函數(shù)
function effect(fn){
const effectFn = ()=>{
// 調(diào)用函數(shù)完成清理遺留副作用函數(shù)
cleanupEffect(effectFn)
// 當(dāng)調(diào)用effect注冊(cè)副作用函數(shù)時(shí),將副作用函數(shù)fn賦值給activeEffect
activeEffect = effectFn;
// 執(zhí)行副作用函數(shù)
fn();
}
//deps是用于存儲(chǔ)所有與該副作用函數(shù)相關(guān)聯(lián)的依賴(lài)集合
effectFn.deps = [];
// 執(zhí)行副作用函數(shù)effectFn
effectFn()
}
cleanupEffect函數(shù)的設(shè)計(jì)實(shí)現(xiàn)如下代碼段,其接收一個(gè)effectFn副作用函數(shù)作為參數(shù),遍歷收集依賴(lài)集合的effectFn.deps數(shù)組,將effectFn該函數(shù)從依賴(lài)集合中清除,最后重置effectFn.deps數(shù)組。
// 遺留的副作用函數(shù)的清除函數(shù)
function cleanupEffect(effectFn){
const { deps } = effectFn
// 遍歷依賴(lài)集合數(shù)組
for(let i = 0; i < deps.length; i++){
//從依賴(lài)集合中刪除
deps[i].delete(effectFn)
}
// 重置數(shù)組
deps.length = 0
}
那么,effectFn.deps數(shù)組又是如何收集依賴(lài)集合的呢?首先將當(dāng)前執(zhí)行的副作用函數(shù)activeEffect添加到依賴(lài)集合deps中,此時(shí)deps存儲(chǔ)的是與當(dāng)前副作用函數(shù)存在聯(lián)系的依賴(lài)集合,而后將其添加到activeEffect.deps數(shù)組中完成收集。
// 在get攔截函數(shù)中調(diào)用追蹤取值函數(shù)的變化
function track(target, key){
// 沒(méi)有activeEffect
if(!activeEffect) return
// 根據(jù)目標(biāo)對(duì)象從桶中獲得副作用函數(shù)
let depsMap = bucket.get(target);
// 判斷是否存在,不存在則創(chuàng)建一個(gè)Map
if(!depsMap) bucket.set(target, depsMap = new Map())
// 根據(jù)key從depsMap取的deps,存儲(chǔ)著與key相關(guān)的副作用函數(shù)
let deps = depsMap.get(key);
// 判斷key對(duì)應(yīng)的副作用函數(shù)是否存在
if(!deps) depsMap.set(key, deps = new Set())
// 最后將激活的副作用函數(shù)添加到桶里
deps.add(activeEffect)
// deps是與當(dāng)前副作用函數(shù)存在聯(lián)系的依賴(lài)集合
activeEffect.deps.push(deps)
}
注意:前面的代碼片段在副作用函數(shù)觸發(fā)時(shí)會(huì)執(zhí)行清理操作,在執(zhí)行后會(huì)進(jìn)行收集effect,但是在執(zhí)行過(guò)程中會(huì)導(dǎo)致無(wú)限循環(huán)執(zhí)行(死循環(huán))。
為什么會(huì)出現(xiàn)死循環(huán)呢?
這是因?yàn)樵趖rigger函數(shù)中,會(huì)遍歷存儲(chǔ)著副作用函數(shù)Set集合effects。在副作用函數(shù)執(zhí)行時(shí),會(huì)調(diào)用cleanup執(zhí)行清除操作,實(shí)際上就是從effects集合中找出當(dāng)前執(zhí)行的副作用函數(shù)進(jìn)行清除。但是副作用函數(shù)的執(zhí)行,會(huì)導(dǎo)致其重新被收集到effects集合中,這樣就不斷的清除和收集了。
在ECMA規(guī)范中:調(diào)用forEach對(duì)Set集合進(jìn)行遍歷時(shí),如果一個(gè)值已經(jīng)被訪問(wèn)過(guò),那么該值被刪除并重新添加到集合中,如果此時(shí)forEach遍歷沒(méi)有結(jié)束,該值就會(huì)重新被訪問(wèn)。
let effect = () => {};
let s = new Set([effect])
s.forEach(item=>{
s.delete(effect);
s.add(effect)}
); // 這樣就導(dǎo)致死循環(huán)了那么我們應(yīng)該如何打破循環(huán)呢?
很簡(jiǎn)單,只需要新構(gòu)造一個(gè)Set集合進(jìn)行遍歷即可。即在trigger函數(shù)中修改語(yǔ)句即可:
// 在set攔截函數(shù)中調(diào)用trigger函數(shù)觸發(fā)變化
function trigger(target, key){
// 根據(jù)target從桶中取的depMaps
const depMaps = bucket.get(target);
// 判斷是否存在
if(!depMaps) return
// 根據(jù)key值取得對(duì)應(yīng)的副作用函數(shù)
const effects = depMaps.get(key);
// 執(zhí)行副作用函數(shù)
// effects && effects.forEach(fn=>fn())
const effectsToRun = new Set(effects);
effectsToRun.forEach(effectFn=>effectFn());
}
此時(shí)就有:
修改age值前的頁(yè)面
控制臺(tái)打印結(jié)果:
3、嵌套的effect和effect棧
嵌套的effect
在實(shí)際開(kāi)發(fā)中,我們不可避免會(huì)寫(xiě)出effect函數(shù)嵌套,即一個(gè)effect函數(shù)內(nèi)部嵌套著另外一個(gè)effect函數(shù)。
effect(()=>{
effct(()=>{
/*...*/
})
})如果我們的響應(yīng)式系統(tǒng)不支持effect嵌套,那么會(huì)發(fā)生什么事情呢?
// 原始數(shù)據(jù)
const data = {
name:"pingping",
age:18,
flag:true
}
//代理對(duì)象
const state = new Proxy(data,{
/* 其他代碼省略 */
});
//全局變量
let temp1, temp2;
//effectFn1嵌套effectFn2
effect(()=>{
console.log("執(zhí)行effectFn1");
effect(()=>{
console.log("執(zhí)行effectFn2");
//在effectFn2中讀取state.name屬性
temp2 = state.name;
})
//在effectFn1中讀取state.age屬性
temp1 = state.age;
})
setTimeout(()=>{
state.age = 19
},1000)
在上面代碼中,簡(jiǎn)單的寫(xiě)了一個(gè)effect嵌套的demo,effectFn1內(nèi)部嵌套了effectFn2,那么effectFn1執(zhí)行會(huì)導(dǎo)致effectFn2的執(zhí)行。effectFn2中讀取了state.name的值,而effectFn1中讀取了state.age的值,且effectFn2的讀取操作優(yōu)先于effectFn1的讀取操作。即:
state
|__ name
|__ effectFn1
|__ age
|__ effectFn2
在這種情況下,理論上修改state.name的值只會(huì)觸發(fā)effectFn2的執(zhí)行,而當(dāng)修改state.age的值時(shí),會(huì)觸發(fā)effectFn1的執(zhí)行且間接觸發(fā)effectFn2函數(shù)的執(zhí)行。
但是,事實(shí)上修改state.age的值輸出的結(jié)果如下圖所示,打印了三次,effectFn1只執(zhí)行了一次,而effectFn2卻執(zhí)行了兩次,修改時(shí)的并沒(méi)有重新執(zhí)行effectFn1函數(shù)。
為什么會(huì)出現(xiàn)這種情況呢?
這是因?yàn)槲覀兦短琢硕鄠€(gè)effect函數(shù),而activeEffect全局變量同一時(shí)刻只能存儲(chǔ)一個(gè)通過(guò)effect函數(shù)注冊(cè)的副作用函數(shù)。當(dāng)effect發(fā)生嵌套時(shí),內(nèi)層effect產(chǎn)生的副作用函數(shù)會(huì)覆蓋掉activeEffect的值,并且永遠(yuǎn)不能回到過(guò)去了?!赫媸莻€(gè)渣男』。
effect執(zhí)行棧
那么應(yīng)該如何解決這個(gè)問(wèn)題呢?
想下js事件循環(huán)機(jī)制就知道,通過(guò)一個(gè)棧數(shù)據(jù)結(jié)構(gòu)去存儲(chǔ)當(dāng)前執(zhí)行的事件。同樣的,我們也可以添加一個(gè)副作用函數(shù)執(zhí)行棧effectStack,當(dāng)前副作用函數(shù)執(zhí)行時(shí),將其壓入棧中,在執(zhí)行完畢后將其出棧,并讓activeEffect指向棧頂?shù)母弊饔煤瘮?shù),即最近執(zhí)行的副作用函數(shù)。
let effectStack = [];
// effect用于注冊(cè)副作用函數(shù)
function effect(fn){
const effectFn = ()=>{
// 調(diào)用函數(shù)完成清理遺留副作用函數(shù)
cleanupEffect(effectFn)
// 當(dāng)調(diào)用effect注冊(cè)副作用函數(shù)時(shí),將副作用函數(shù)fn賦值給activeEffect
activeEffect = effectFn;
// 在副作用函數(shù)執(zhí)行前壓棧
effectStack.push(effectFn)
// 執(zhí)行副作用函數(shù)
fn();
// 執(zhí)行完畢后出棧
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
//deps是用于存儲(chǔ)所有與該副作用函數(shù)相關(guān)聯(lián)的依賴(lài)集合
effectFn.deps = [];
// 執(zhí)行副作用函數(shù)effectFn
effectFn()
}
在上面代碼片段中,定義了一個(gè)effectStack數(shù)組去存儲(chǔ)待執(zhí)行的副作用函數(shù),activeEffect始終指向當(dāng)前執(zhí)行的副作用函數(shù)。根據(jù)棧結(jié)構(gòu)的先進(jìn)后出原則,剛好外層effect先進(jìn)存儲(chǔ)在棧地,內(nèi)層effect后進(jìn)存儲(chǔ)在棧頂,在內(nèi)層執(zhí)行完畢后出棧執(zhí)行外層effect。這樣,響應(yīng)式數(shù)據(jù)只會(huì)收集直接讀取當(dāng)前值的副作用函數(shù)作為依賴(lài),從而避免錯(cuò)亂。
這樣控制打?。?/p>
打印結(jié)果
4、避免無(wú)限遞歸循環(huán)
前面在存儲(chǔ)當(dāng)前執(zhí)行的副作用函數(shù)的依賴(lài)集合時(shí),可能會(huì)出現(xiàn)循環(huán)執(zhí)行的情況,我們也添加了新Set集合進(jìn)行解決。當(dāng)我們?cè)诟弊饔煤瘮?shù)中,對(duì)同一個(gè)字段的值進(jìn)行無(wú)限遞歸循環(huán),那么會(huì)出現(xiàn)什么情況?
// 原始數(shù)據(jù)
const data = {
name:"pingping",
age:18,
flag:true
}
//代理對(duì)象
const state = new Proxy(data,{
/* 其他代碼省略 */
});
effect(()=>{
state.age++;
})
我們看到執(zhí)行結(jié)果出現(xiàn)爆棧的情況,內(nèi)存溢出:
內(nèi)存溢出
我們可以看到state.age++;語(yǔ)句中,既有state.age的讀取操作,又有設(shè)值操作,這樣前一個(gè)副作用函數(shù)還沒(méi)執(zhí)行完畢,又重新開(kāi)啟了新的執(zhí)行,這樣就無(wú)限遞歸調(diào)用自己了。『我調(diào)用我自己,超越本我』
那么,我們應(yīng)該如何避免棧溢出呢?
在前面的文章中知道,在對(duì)state.age的取值track和設(shè)值trigger操作都是在同一個(gè)副作用函數(shù)activeEffect中執(zhí)行的。那么只需要在trigger中增加守衛(wèi)條件:判斷下觸發(fā)trigger的副作用函數(shù)和當(dāng)前正在執(zhí)行的副作用函數(shù)是不是同一個(gè),如果是同一個(gè)則不觸發(fā)執(zhí)行,否則執(zhí)行。
// 在set攔截函數(shù)中調(diào)用trigger函數(shù)觸發(fā)變化
function trigger(target, key){
// 根據(jù)target從桶中取的depMaps
const depMaps = bucket.get(target);
// 判斷是否存在
if(!depMaps) return
// 根據(jù)key值取得對(duì)應(yīng)的副作用函數(shù)
const effects = depMaps.get(key);
const effectsToRun = new Set();
// 執(zhí)行副作用函數(shù)
effects && effects.forEach(effectFn=>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn=>effectFn());
}
在執(zhí)行觸發(fā)trigger時(shí),對(duì)觸發(fā)trigger的副作用函數(shù)和當(dāng)前執(zhí)行的副作用函數(shù)進(jìn)行比較篩選,即可避免棧內(nèi)存的溢出。
5、調(diào)度執(zhí)行
先了解下可調(diào)度性對(duì)于意義,就是trigger觸發(fā)副作用函數(shù)重新執(zhí)行時(shí),可以自定義決定副作用函數(shù)執(zhí)行的時(shí)機(jī)、次數(shù)、及執(zhí)行方式。
// 原始數(shù)據(jù)
const data = {
name:"pingping",
age:18,
flag:true
}
//代理對(duì)象
const state = new Proxy(data,{
/* 其他代碼省略 */
});
effect(()=>{
console.log(state.age);
});
state.age++;
console.log("run end");
執(zhí)行結(jié)果
如果我們需要改變代碼的執(zhí)行順序,得到不同的結(jié)果,那么需要提供給用戶(hù)調(diào)度能力,即允許使用者自定義調(diào)度器。
// effect用于注冊(cè)副作用函數(shù)
function effect(fn,options={}){
const effectFn = ()=>{
// 調(diào)用函數(shù)完成清理遺留副作用函數(shù)
cleanupEffect(effectFn)
// 當(dāng)調(diào)用effect注冊(cè)副作用函數(shù)時(shí),將副作用函數(shù)fn賦值給activeEffect
activeEffect = effectFn;
// 在副作用函數(shù)執(zhí)行前壓棧
effectStack.push(effectFn)
// 執(zhí)行副作用函數(shù)
fn();
// 執(zhí)行完畢后出棧
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 將options掛載到effectFn函數(shù)上
effectFn.options = options
//deps是用于存儲(chǔ)所有與該副作用函數(shù)相關(guān)聯(lián)的依賴(lài)集合
effectFn.deps = [];
// 執(zhí)行副作用函數(shù)effectFn
effectFn()
}
// 在set攔截函數(shù)中調(diào)用trigger函數(shù)觸發(fā)變化
function trigger(target, key){
// 根據(jù)target從桶中取的depMaps
const depMaps = bucket.get(target);
// 判斷是否存在
if(!depMaps) return
// 根據(jù)key值取得對(duì)應(yīng)的副作用函數(shù)
const effects = depMaps.get(key);
const effectsToRun = new Set();
// 執(zhí)行副作用函數(shù)
effects && effects.forEach(effectFn=>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn=>{
// 如果副作用函數(shù)中存在調(diào)度器
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn)
}else{
effectFn()
}
});
}
在上面代碼片段中,在trigger觸發(fā)副作用函數(shù)執(zhí)行時(shí),會(huì)先判斷該副作用函數(shù)中是否存在調(diào)度器:
- 存在調(diào)度器,直接執(zhí)行調(diào)度器函數(shù),并將當(dāng)前副作用函數(shù)作為參數(shù)傳遞effectFn.options.scheduler(effectFn)。
- 不存在調(diào)度器,則直接執(zhí)行副作用函數(shù)effectFn()。
effect(()=>{
console.log(state.age);
},{//options
scheduler(fn){//調(diào)度器
setTimeout(fn);
}
});
state.age++;
console.log("run end");執(zhí)行結(jié)果
這樣,系統(tǒng)設(shè)計(jì)實(shí)現(xiàn)了控制副作用函數(shù)的執(zhí)行順序。除此之外,我們還可以添加實(shí)現(xiàn)控制副作用函數(shù)的執(zhí)行次數(shù),同樣只需要修改調(diào)度器代碼就行,這里就不贅述了。
6、寫(xiě)在最后
在本文中,主要解決的問(wèn)題有:
- 分支切換導(dǎo)致遺留的副作用函數(shù),可以添加一個(gè)集合收集依賴(lài)集合,在每次執(zhí)行副作用函數(shù)前將其對(duì)應(yīng)的聯(lián)系清除,在執(zhí)行后重新建立聯(lián)系。
- 對(duì)于effect嵌套問(wèn)題可以通過(guò)添加一個(gè)effectStack執(zhí)行棧解決,外層副作用函數(shù)先入棧,內(nèi)層后入棧,activeEffect永遠(yuǎn)指向當(dāng)前要執(zhí)行的副作用函數(shù)。
- 對(duì)于避免無(wú)限遞歸循環(huán),可以在trigger觸發(fā)副作用函數(shù)執(zhí)行前進(jìn)行判斷,觸發(fā)的副作用函數(shù)與當(dāng)前執(zhí)行的副作用函數(shù)是否相同。
- 對(duì)于響應(yīng)系統(tǒng)的調(diào)度性,可以通過(guò)設(shè)置調(diào)度器去控制副作用函數(shù)執(zhí)行的順序、時(shí)機(jī)、次數(shù)等。
網(wǎng)站標(biāo)題:Vue.js設(shè)計(jì)與實(shí)現(xiàn)之五-設(shè)計(jì)一個(gè)完善的響應(yīng)系統(tǒng)
分享URL:http://m.5511xx.com/article/dhoeoce.html


咨詢(xún)
建站咨詢(xún)
