日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢(xún)
選擇下列產(chǎn)品馬上在線(xiàn)溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷(xiāo)解決方案
Vue.js設(shè)計(jì)與實(shí)現(xiàn)之五-設(shè)計(jì)一個(gè)完善的響應(yīng)系統(tǒng)

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