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