iT邦幫忙

2023 iThome 鐵人賽

DAY 7
1

「我囸你大擊錘沒存到檔!」——自尤雨溪望

大家早安,今天是我第二次寫watch了。為什麼是第二次寫呢?因為昨天晚上趕到凌晨1點的watch沒存到檔!!
你喵的為什麼it鐵人沒有自動存檔功能啊?不會用websocket或擔心給服務器造成負擔的話也可以存localStorage啊QwQ……

……總之書接上回,我們給value屬性做get及set,get被觸發時收集依賴,set觸發時重新觸發effect,實現了ref。不知道各位有沒有注意到,其實只要是由響應式變數的get收集,set觸發的任何行為都可以是effect。

正如我在[Day 03]的文章所說,effect的功能不止於渲染組件,computed的回調收集的響應式變數更新時會重新觸發回調,更新自己的值;watch所監視的響應式變數更新時會重新觸發回調,執行我們客製化定義的行為。這些都是effect函數。

今天我們就來示範effect是怎麼實現watch的。

事前準備

請容我再次強調,watch所監視的響應式變數更新時會重新觸發回調,執行我們客製化定義的行為。
這句話當中有幾個重點:

  1. 監視的響應式變數
  2. 執行客製化定義的行為

由於這次響應式變數更新時並不是重新渲染組件,而是執行客製化定義的行為,因此我們會需要改造一下/src/vue/effect.ts的ReactiveEffect類:

export class ReactiveEffect {
  parent?: ReactiveEffect;
  constructor(public fn: Function, public schedule?: Function) { }

  run() {
    try {
      this.parent = activeEffect;
      activeEffect = this;
      return this.fn();
    } finally {
      activeEffect = this.parent;
    }
  }
}

我們在ReactiveEffect的建構子中多開放了一個可選參數schedule,讓它代表我們客製化定義的行為。並且我們修改同樣在effect.ts暴露的triggerEffect方法:

export const triggerEffect = (effects: Dep) => {
  effects.forEach(effect => {
    if (effect !== activeEffect) {
      if (effect.schedule) effect.schedule();
      else effect.run();
    }
  });
};

加上一個如果有schedule,就執行schedule代替run的判斷,以客製化的schedule代替重新渲染組件。

然後雖然不是必要,但方便起見,/src/vue/reactive.ts也再多暴露一個isReactive方法,讓我們之後能更便利地判斷一個對象是不是reactive:

export function isReactive(value: any) {
  return !!(value && value[ReactiveFlags.IS_REACTIVE]);
}

也修改/src/vue/ref.ts,在RefImpl類增加一個只讀屬性__v_isRef,再暴露一個isRef的方法:

export const isRef = (value: any) => {
  return !!(value && value.__v_isRef);
};

export class RefImpl {
  public dep: Set<ReactiveEffect> = new Set;
  public _value;
  public readonly __v_isRef = true;
  constructor(public rawValue: any) {
    this._value = toReactive(rawValue);
  }
  get value() {
    trackEffects(this.dep);
    return this._value;
  }
  set value(newValue: any) {
    if (newValue !== this.rawValue) {
      this._value = toReactive(newValue);
      this.rawValue = newValue;
      triggerEffect(this.dep);
    }
  }
}

接下來我們就能開始著手製作watch了。

watch

我們可以將watch拆成以下幾個步驟:

  1. 緩存要監視的對象的get
  2. 在effect中觸發緩存的get
  3. 監視的對象的set觸發時調用自訂的schedule

為了實現這三個步驟,我們建立/src/vue/watch.ts文件,並寫上以下程式碼:

import { ReactiveEffect } from "./effect";
import { isReactive } from "./reactive";
import { RefImpl, isRef } from './ref';

const traversal = (value: Record<string, any>) => {
  if (!value || typeof value !== 'object') return value;
  for (const key in value) {
    traversal(value[key]);
  }
  return value;
};

export const watch = (source: unknown, cb: Function) => {
  let getter;
  if (isReactive(source)) getter = () => traversal(source);
  else if (isRef(source)) getter = () => (<RefImpl>source).value;
  else if (typeof source === 'function') getter = source;
  else return;

  let oldValue: unknown;
  const job = () => {
    const newValue = effect.run();
    cb(newValue, oldValue);
    oldValue = newValue;
  };

  const effect = new ReactiveEffect(getter, job);
  oldValue = effect.run();
};

1. 緩存要監視的對象的get

只要我們做出一個返回值是監視對象的屬性的方法,我們調用這個方法時就能觸發監視對象的get,也就是說我們可以在activeEffect是watch的effect的時候觸發這個get。這句話目前看起來可能比較抽象,總之我們暫且先記著我們要做出一個返回值是監視對象的get的方法。

所以在watch的方法體中,我們先宣告一個變數getter,給它賦值為回傳監視對象的get的方法:

let getter;
if (isReactive(source)) getter = () => traversal(source);
else if (isRef(source)) getter = () => (<RefImpl>source).value;
else if (typeof source === 'function') getter = source;
else return;

其中traversal是watch.ts中,在watch的上面宣告的方法。它的內容很單純,就是把一個對象或陣列裡的每一個值都遍歷一遍。

const traversal = (value: Record<string, any>) => {
  if (!value || typeof value !== 'object') return value;
  for (const key in value) {
    traversal(value[key]);
  }
  return value;
};

為什麼要這麼做呢?先前我們在reactive的章節有學到,reactive回傳的Proxy會遞歸代理每一個複雜數據類型的屬性,並給簡單數據類型的屬性加上get。所以只要遍歷一個reactive的對象,就能觸發它所有屬性的get,讓所有屬性都收集watch的effect的依賴。

因此getter = () => traversal(source)就是等到調用getter時,挨個兒去觸發source的所有get。

而watch接收ref或() => {響應式對象的屬性}的情況也同理,當source是ref對象時,getter = () => source.value代表調用getter時觸發ref對象的get;當source是返回值為響應式對象的屬性的方法時,getter = source就代表調用getter時觸發響應式變數的屬性所對應的get。

2. 在effect中觸發緩存的get

我們先看job以外的程式碼:

let oldValue: unknown;
// const job = ......

const effect = new ReactiveEffect(getter, job);
oldValue = effect.run();

複習一下/src/vue/effect.ts暴露的ReactiveEffect吧。它的建構子接收兩個參數:fn和schedule。當ReactiveEffect的實例對象調用run方法時會調用它的fn,並回傳fn的返回值:

run() {
  // ......
  activeEffect = this;
  return this.fn();
  // ......
}

也就是說oldValue = effect.run()就是給oldValue賦值為getter的返回值,也就是watch方法被調用時,其監視的目標的值。
這也就是為什麼watch的方法體中需要宣告一個getter賦值為返回監視的對象的get的原因——直到activeEffect是watch的effect時才觸發get進行依賴收集。
而當watch監視的目標改變時,也就是set被觸發時會調用triggerEffect,觸發它收集的所有依賴:

export const triggerEffect = (effects: Dep) => {
  effects.forEach(effect => {
    if (effect !== activeEffect) {
      if (effect.schedule) effect.schedule();
      else effect.run();
    }
  });
};

watch的effect的schedule正是我們傳給ReactiveEffect建構子的第二個參數,job。

3. 監視的對象的set觸發時調用自訂的schedule

現在再連著job的部分一起看watch的方法體:

// 將getter賦值為回傳監視目標的get的方法
// ......

let oldValue: unknown;
const job = () => {
  const newValue = effect.run();
  cb(newValue, oldValue);
  oldValue = newValue;
};

const effect = new ReactiveEffect(getter, job);
oldValue = effect.run();

當watch監視的對象的set觸發時,會再次調用effect.run(),也就是getter(),回傳我們監視的響應式變數的get的方法。其返回值賦值給newValue,並將newValue和oldValue傳給watch接收的回調cb,最後再讓oldValue = newValue(這樣下次set觸發時,傳給cb的oldValue才會是set觸發前的newValue),如此便實現了watch的基本功能。

除去基本功能,watch其實還有一些其他用法,像是它的第一個參數可以接收包含多個get的陣列、第三個參數可以接收一個例如{ immediate: true }的配置項,甚至它的回調也有第三個形參。不過各位可能已經忘記了,其實我們這次鐵人賽的標題叫「淺談vue3源碼,很淺的那種」來著……為了篇幅著想,這些就請自己舉一反三了吧。

絕對不是我自己也不會啊!不是喔!

githubmain分支commit「[Day 07]watch」


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

尚未有邦友留言

立即登入留言