iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0

「有時間去整整容吧。」——王寶強

上回書說道,effect函數的功用其實除了渲染組件,還能執行客製化的方法,例如當watch監視的響應式變數的set被觸發,是可以執行這個watch的回調的。

既然能觸發watch的回調,自然也能觸發computed的回調。只是computed又比watch更加特殊,因為它是個回調返回的值,所以它既是effect,同時也能觸發其他effect。

要實現這個身兼effect的響應式變數,我們需要以下步驟:

  1. 把回調變成effect
  2. get被觸發時依賴收集
  3. 當自己的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);
};

computed

我們把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。

optional 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」


上一篇
[Day 07]watch
下一篇
[Day 09] 視圖
系列文
淺談vue3源碼,很淺的那種31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言