Vue.js設(shè)計(jì)與實(shí)現(xiàn)之六-computed計(jì)算屬性的實(shí)現(xiàn)
1、寫(xiě)在前面
在前面文章介紹了effect的實(shí)現(xiàn),可以用于注冊(cè)副作用函數(shù),同時(shí)允許一些選項(xiàng)參數(shù)options,可以指定調(diào)度器去控制副作用函數(shù)的執(zhí)行時(shí)機(jī)和次數(shù)等。還有用于追蹤和收集依賴的track函數(shù),以及用于觸發(fā)副作用函數(shù)重新執(zhí)行的trigger函數(shù),結(jié)合這些我們可以實(shí)現(xiàn)一個(gè)計(jì)算屬性--computed。
2、懶執(zhí)行的effect
在研究計(jì)算屬性的實(shí)現(xiàn)之前,需要先去了解下懶執(zhí)行的effect(lazy的effect)。在當(dāng)前設(shè)計(jì)的effect函數(shù)中,它會(huì)在調(diào)用時(shí)立即執(zhí)行傳遞過(guò)來(lái)的副作用函數(shù)。但是事實(shí)上,希望在某些場(chǎng)景并不希望它立即執(zhí)行,而是在需要的時(shí)候才執(zhí)行,前面了解到想要改變effect的執(zhí)行可以在options參數(shù)中設(shè)置。
const data = {
name:"pingping",
age:18,
flag:true
}
const state = new Proxy(data,{
/*...*/
})
effect(()=>{
console.log(state.name);
},{
//指定lazy選項(xiàng),這樣函數(shù)不會(huì)立即執(zhí)行
lazy: true
})
就這樣,通過(guò)設(shè)置options選項(xiàng),去修改effect函數(shù)的實(shí)現(xiàn)邏輯,當(dāng)options.lazy為true時(shí)不會(huì)立即執(zhí)行副作用函數(shù):
// 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)的依賴集合
effectFn.deps = [];
// 只有非lazy的時(shí)候才執(zhí)行
if(!options.lazy){
// 執(zhí)行副作用函數(shù)effectFn
effectFn()
}
//否則返回副作用函數(shù)
return effectFn
}
在上面代碼片段中,在effect函數(shù)中先判斷了是否需要懶執(zhí)行,對(duì)此會(huì)判斷options.lazy的值為true時(shí),則將effectFn副作用函數(shù)作為參數(shù)返回到effect。這樣,用戶在調(diào)用執(zhí)行effect函數(shù)時(shí),可以通過(guò)返回值去拿到對(duì)應(yīng)的effectFn函數(shù),這樣可以手動(dòng)執(zhí)行該函數(shù)。
const effectFn = effect(()=>{
console.log(state.name);
},{
//指定lazy選項(xiàng),這樣函數(shù)不會(huì)立即執(zhí)行
lazy: true
});
//手動(dòng)執(zhí)行副作用函數(shù)
effectFn();
但是僅僅實(shí)現(xiàn)手動(dòng)執(zhí)行副作用函數(shù),對(duì)于我們的使用意義并不大,如果將返回到effect的副作用函數(shù)作為getter,那么通過(guò)這個(gè)取值函數(shù)就能獲取返回任何值。
const effectFn = effect(
()=>state.name + state.age,
{
//指定lazy選項(xiàng),這樣函數(shù)不會(huì)立即執(zhí)行
lazy: true
});
//手動(dòng)執(zhí)行副作用函數(shù),可以獲取到返回的值
const value = effectFn();
這樣就可以實(shí)現(xiàn)在調(diào)用的時(shí)候,手動(dòng)執(zhí)行獲取到各種想要得到的值。在effect函數(shù)內(nèi)部只需要做出些改變,只需要在執(zhí)行副作用函數(shù)時(shí)將副作用的值返回即可:
// 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ù),將執(zhí)行結(jié)果存儲(chǔ)到res中
const res = fn();
// 執(zhí)行完畢后出棧
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 將res作為effectFn的返回值
return res
}
// 將options掛載到effectFn函數(shù)上
effectFn.options = options
//deps是用于存儲(chǔ)所有與該副作用函數(shù)相關(guān)聯(lián)的依賴集合
effectFn.deps = [];
// 只有非lazy的時(shí)候才執(zhí)行
if(!options.lazy){
// 執(zhí)行副作用函數(shù)effectFn
effectFn()
}
//否則返回副作用函數(shù)
return effectFn
}
現(xiàn)在,我們已經(jīng)實(shí)現(xiàn)了能夠進(jìn)行懶執(zhí)行的副作用函數(shù),能夠拿到執(zhí)行返回的結(jié)果,做后續(xù)的處理。
3、computed屬性
懶計(jì)算的computed屬性
其實(shí),基于前面的設(shè)計(jì)和代碼實(shí)現(xiàn),大概有了computed屬性函數(shù)的實(shí)現(xiàn)雛形,就是接收一個(gè)getter函數(shù)作為副作用函數(shù),用于創(chuàng)建一個(gè)懶執(zhí)行的effect。computed函數(shù)的執(zhí)行會(huì)返回包含一個(gè)訪問(wèn)器屬性的對(duì)象,只有在讀取value值的時(shí)候才會(huì)去執(zhí)行effectFn并返回結(jié)果。
function computed(getter){
const effectFn = effect(
getter,
{
//指定lazy選項(xiàng),這樣函數(shù)不會(huì)立即執(zhí)行
lazy: true
});
const state = {
//當(dāng)對(duì)value進(jìn)行讀取操作時(shí),執(zhí)行effectFn并將結(jié)果進(jìn)行返回
get value(){
return effectFn();
}
}
return state;
}
在上面代碼中,只是粗略做了懶計(jì)算處理,只有在真正對(duì)sumRes.value的值進(jìn)行讀取操作時(shí),才會(huì)去進(jìn)行計(jì)算并得到值。但是在進(jìn)行多次讀取sumRes.value的值,每次訪問(wèn)計(jì)算得到的值都是相同的,并不符合我們需要使用上次計(jì)算值的要求。『計(jì)算屬性需要有緩存機(jī)制,這樣就可以使用到上次計(jì)算的結(jié)果。』
const sumRes = computed(()=>state.name + state.age);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
運(yùn)行結(jié)果:
之所以發(fā)生這種情況,多次讀取sumRes.value的值時(shí),每次訪問(wèn)都會(huì)重新調(diào)用effectFn重新計(jì)算。
帶有緩存的computed
為了解決前面獲取不到上次計(jì)算值的問(wèn)題,需要在實(shí)現(xiàn)computed函數(shù)時(shí),添加對(duì)計(jì)算值的緩存操作。其實(shí)實(shí)現(xiàn)很簡(jiǎn)單,就是添加兩個(gè)變量value和dirty,value用于緩存上次計(jì)算的值,dirty則標(biāo)識(shí)是否需要重新計(jì)算。
function computed(getter){
let value;
let dirty = true;
const effectFn = effect(
getter,
{
//指定lazy選項(xiàng),這樣函數(shù)不會(huì)立即執(zhí)行
lazy: true,
//在調(diào)度器重置dirty為true
scheduler(){
dirty = true
}
});
const state = {
//當(dāng)對(duì)value進(jìn)行讀取操作時(shí),執(zhí)行effectFn并將結(jié)果進(jìn)行返回
get value(){
//只有當(dāng)dirty標(biāo)識(shí)為true值時(shí),才會(huì)將計(jì)算值進(jìn)行緩存,下一次訪問(wèn)直接使用緩存的值
if(dirty){
value = effectFn();
dirty = false
}
return value
}
}
return state;
}
在上面代碼中,初始化設(shè)置dirty為true,這樣就會(huì)把計(jì)算值進(jìn)行緩存,下次進(jìn)行同樣computed計(jì)算操作時(shí),就會(huì)直接使用緩存的值,而非每次重新計(jì)算。同時(shí),在computed函數(shù)的effect中添加scheduler屬性,在函數(shù)內(nèi)部將dirty的值重置為true,在下次訪問(wèn)sumRes.value時(shí)重新調(diào)用effectFn的計(jì)算值。
const sumRes = computed(()=>state.name + state.age);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
state.age++;
console.log("hello", sumRes.value);
執(zhí)行結(jié)果為:
但是,在當(dāng)前設(shè)計(jì)的計(jì)算屬性在另一個(gè)effect函數(shù)中讀取時(shí),修改響應(yīng)數(shù)據(jù)state上的屬性值并不會(huì)觸發(fā)副作用函數(shù)的重新渲染。其實(shí)根本原因就是這里存在一個(gè)effect嵌套問(wèn)題,computed內(nèi)部是effect函數(shù)實(shí)現(xiàn)的,而在effect中讀取computed的值相當(dāng)于對(duì)effect進(jìn)行了嵌套,外層的effect不會(huì)被內(nèi)層effect的響應(yīng)式數(shù)據(jù)收集。
當(dāng)然,問(wèn)題很簡(jiǎn)單,解決方法同樣很簡(jiǎn)單。只需要在讀取計(jì)算屬性值的時(shí)候,手動(dòng)調(diào)用track函數(shù)進(jìn)行追蹤,當(dāng)計(jì)算屬性依賴的響應(yīng)式數(shù)據(jù)發(fā)生變化時(shí),手動(dòng)調(diào)用trigger函數(shù)觸發(fā)響應(yīng):
function computed(getter){
let value;
let dirty = true;
const effectFn = effect(
getter,
{
//指定lazy選項(xiàng),這樣函數(shù)不會(huì)立即執(zhí)行
lazy: true,
//在調(diào)度器重置dirty為true
scheduler(){
dirty = true
trigger(state, "value")
}
}
);
const state = {
//當(dāng)對(duì)value進(jìn)行讀取操作時(shí),執(zhí)行effectFn并將結(jié)果進(jìn)行返回
get value(){
//只有當(dāng)dirty標(biāo)識(shí)為true值時(shí),才會(huì)將計(jì)算值進(jìn)行緩存,下一次訪問(wèn)直接使用緩存的值
if(dirty){
value = effectFn();
dirty = false
}
// 對(duì)value進(jìn)行取值操作時(shí),手動(dòng)調(diào)用track函數(shù)進(jìn)行追蹤
track(state, "value")
return value
}
}
return state;
}
寫(xiě)一段簡(jiǎn)單的demo進(jìn)行實(shí)驗(yàn):
const sumRes = computed(()=>state.name + state.age);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
effect(()=>{
console.log(sumRes.value);
})
state.age++
console.log("hello", sumRes.value);
執(zhí)行結(jié)果:
根據(jù)上面的實(shí)現(xiàn)demo可以分析出對(duì)應(yīng)的計(jì)算屬性的響應(yīng)聯(lián)系圖:
計(jì)算屬性的響應(yīng)聯(lián)系
4、寫(xiě)在最后
計(jì)算屬性computed其實(shí)是一個(gè)懶執(zhí)行的副作用函數(shù),可以通過(guò)lazy選項(xiàng)使得副作用函數(shù)可以懶執(zhí)行,被標(biāo)記為懶執(zhí)行的副作用函數(shù)可以通過(guò)手動(dòng)執(zhí)行。在讀取計(jì)算屬性的值時(shí),可以手動(dòng)執(zhí)行副作用函數(shù),在依賴的響應(yīng)式數(shù)據(jù)發(fā)生變化時(shí),通過(guò)scheduler將dirty標(biāo)記設(shè)置為true,即為臟數(shù)據(jù),在下次讀取計(jì)算屬性的值,就會(huì)重新計(jì)算得到真正的值。