「有時間去整整容吧。」——王寶強
上回書說道,effect函數的功用其實除了渲染組件,還能執行客製化的方法,例如當watch監視的響應式變數的set被觸發,是可以執行這個watch的回調的。
既然能觸發watch的回調,自然也能觸發computed的回調。只是computed又比watch更加特殊,因為它是個回調返回的值,所以它既是effect,同時也能觸發其他effect。
要實現這個身兼effect的響應式變數,我們需要以下步驟:
為了實現以上步驟,我們先新建/src/vue/computed.ts文件,並在其中寫上:
import { ReactiveEffect, trackEffects, triggerEffect } from './effect';
class ComputedRefImpl<Value = unknown> {
public effect;
public __v_isReadonly = true;
public __v_isRef = true;
public _value: Value;
public dep: Set<ReactiveEffect> = new Set;
constructor(getter: () => Value) {
this.effect = new ReactiveEffect(getter, () => {
triggerEffect(this.dep);
});
}
get value() {
trackEffects(this.dep);
this._value = this.effect.run();
return this._value;
}
}
export const computed = (getter: () => any) => {
return new ComputedRefImpl(getter);
};
我們把ComputedRefImpl的建構子和get value一起看。
constructor(getter: () => Value) {
this.effect = new ReactiveEffect(getter, () => {
triggerEffect(this.dep);
});
}
get value() {
trackEffects(this.dep);
this._value = this.effect.run();
return this._value;
}
當computed對象的value屬性被訪問時,會先進行依賴收集,把提及這個computed對象的effect收集到其dep屬性中,並返回this.effect.run(),而這個effect.run()就是在建構子中接收的getter,也就是compute接收的回調。此時computed可以像響應式變數一樣進行依賴收集。
由於在effect.run()時的activeEffect就是computed的effect,因此computed也能被其回調中所提及的響應式變數依賴收集。
當computed的回調中提及的響應式變數更新時,會觸發它們的set,調用computed的effect在建構子的第二個參數接收的schedule;而computed的schedule就是triggerEffect(this.dep),重新運行它收集的effect。換言之,當computed的回調提及的響應式變數更新,便會間接觸發有提及computed的value屬性的所有effect。
如此一來便實現了既是響應式變數又是effect,既能被依賴收集,也能收集依賴的computed。
但computed除了接收回調,也能接收一個包含get方法和set方法的配置項,來定義當computed.value被修改時該有的行為。
因此我們先修改我們暴露出去的computed方法:
interface getterAndSetter {
get:()=>any
set:(value: any)=>void
}
export const computed = (getterOrOptions:getterAndSetter | (() => any)) => {
let getter
let setter
if(typeof getterOrOptions === 'function') getter = getterOrOptions
else {
getter = (getterOrOptions as getterAndSetter).get
setter = (getterOrOptions as getterAndSetter).set
}
return new ComputedRefImpl(getter,setter)
}
如果接收到的是個回調,就只傳回調給ComputedRefImpl的建構子;如果接收到的是包含get和set方法的配置項,就把get和set都傳給ComputedRefImpl的建構子。
既然傳了set,就也修改ComputedRefImpl的建構子,讓它可以接收第二個setter參數吧:
class ComputedRefImpl<Value = unknown> {
// ......
constructor(getter: () => Value, public setter?: (value: any) => void) {
// ......
}
// get value ......
set value(newValue: Value) {
this.setter(newValue)
}
}
要做的事也很簡單,有setter的話,當value屬性被修改時調用setter,僅此而已。
然而在ComputedRefImpl的get value,我們可以看到一個問題。
get value() {
trackEffects(this.dep);
this._value = this.effect.run();
return this._value;
}
this._value = this.effect.run(),這意味著每當computed的value被訪問,哪怕它的getter提及的每個響應式變數都沒被更新,仍會重新執行一次this.effect.run(),重新調用getter,這對效能有負面的影響。
因此我們給ComputedRefImpl新增一個屬性_dirty,用以記錄computed對象的getter提及的響應式變數是否更新:
class ComputedRefImpl<Value = unknown> {
// ......
public _dirty = true;
constructor(getter: () => Value, public setter?: (value: any) => void) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
triggerEffect(this.dep);
}
});
}
get value() {
trackEffects(this.dep);
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run();
}
return this._value;
}
// ......
}
當computed對象的getter提及的響應式數據更新時,會觸發其set,執行其收集的所有effect;而computed的effect的schedule,也就是ReactiveEffect建構子接收的第二個參數,會將computed對象的_dirty屬性設置為true,代表這個computed對象不乾淨了。
而在computed對象的get value中,會判斷_dirty是否為true。若是,我們就能判斷出它的getter提及的響應式變數有更新,僅此時需要重新運行this.effect.run()來同步更新computed的value;反之,則代表它的getter提及的響應式變數都沒有更新,便無需重新運行this.effect.run(),直接返回緩存的_value即可。
githubmain分支commit「[Day 08]computed」