iT邦幫忙

2023 iThome 鐵人賽

DAY 4
0
Vue.js

淺談vue3源碼,很淺的那種系列 第 4

[Day 04]依賴收集 - 2——利用響應式變數的get收集當前effect

  • 分享至 

  • xImage
  •  

「明けない夜に落ちてゆく前に、
僕の手を掴んでほら。」——Ayase

上回書說道,為了記錄每個響應式變數被哪些effect函數提及,我們必須執行以下步驟:

  1. 記錄目前正在執行的effect函數(可先理解為目前正在渲染哪個組件)。 已完成
  2. 當effect函數作用時,觸發其提及的響應式變數的get。
  3. 響應式變數的get被觸發時,收集當前正在作用的effect函數。

我們還在/src/vue/effect.ts暴露了一個方法,其可以記錄當前正在作用的ReactiveEffect對象,並調用其run方法。
接下來我們將來復刻reactive。

reactive

依賴收集的前置準備

首先我們要建立兩個文件,/src/vue/reactive.ts和/src/vue/baseHandler.ts。
baseHandler.ts的部分要做依賴收集,會比較複雜,我們先把依賴收集以外的部分寫好:

import { reactive } from './reactive';

export const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive'
}

export default {
  get: (
    target: Record<string, any>,
    key: string,
    receiver: any
  ) => {
    if (key === ReactiveFlags.IS_REACTIVE) return true;

    const res = Reflect.get(target, key, receiver);
    if (res && typeof res === 'object') return reactive(res);

    return res;
  },
  set: (target: Record<string, any>, key: string, value: any) => {
    target[key] = value;
    return true;
  }
};

先暴露一個枚舉ReactiveFlags,其中有IS_REACTIVE屬性,表示這個對象是否被代理過,稍後會在reactive.ts提到。

enum是ts的關鍵字,其代表枚舉,可在enum中寫入許多key,乍看之下和物件很像,但當ts被編譯為js後,枚舉會被全部替換成key對應的value,並不會在堆內存產生物件
譬如上述的例子ReactiveFlags,在ts被編譯成js後,不會在內存產生一個有IS_REACTIVE屬性的物件,而所有用到ReactiveFlags.IS_REACTIVE的地方都會被替換為'__v_isReactive'(如果枚舉的key沒有值,將會被依序換成0、1、2......類推的數字)。
而這邊之所以使用枚舉,是因為IS_REACTIVE具備可讀性,但這個屬性不像__v_isReactive,撞名的可能性相對較高。因此透過枚舉,實現開發時具可讀性,打包時不易撞名的效果。

至於下方默認暴露的,包含get和set方法的對象,就是接下來要給reactive傳入Proxy的第二個參數的handler了。
基本上目前還沒甚麼重點,重點在之後才會往這裡面寫的依賴收集,唯一值得一提的只有深度代理的部分:

const res = Reflect.get(target, key, receiver);
if (res && typeof res === 'object') return reactive(res);

Reflect.get(target, key, receiver)和target[key]基本無異,就只差在能給之後深度代理的屬性的this指向receiver。
而如果目前訪問到的target[key]是一個物件,便return reactive(target[key]),遞歸調用reactive將對象內的每一個屬性都代理成響應式變數,即可實現深度代理。


相較之下reactive.ts就簡單得多:

import baseHandler, { ReactiveFlags } from './baseHandler';

const reactiveMap = new WeakMap();

export const reactive = (target: Record<string, any>) => {
  if (target && typeof target === 'object') {
    if (target[ReactiveFlags.IS_REACTIVE]) return target;

    const existingProxy = reactiveMap.get(target);
    if (existingProxy) return existingProxy;

    const proxy = new Proxy(target, baseHandler);
    reactiveMap.set(target, proxy);
    return proxy;
  }
};

WeakMap是一種類似物件的結構,其差別在於物件的key必須是string或Symbol,而WeakMap的key必須是物件。
因此可以透過WeakMap,替被代理的原對象及代理後的響應式對象建立映射關係。

以下程式碼都是在檢查reactive接收的對象是否已被代理。已被代理過就直接返回原對象,反正它已經被代理過了,沒必要代理第二次。

if (target[ReactiveFlags.IS_REACTIVE]) return target;

const existingProxy = reactiveMap.get(target);
if (existingProxy) return existingProxy;

確認未被代理,便new一個Proxy對象,將target記錄到reactiveMap中(用於判斷target已被代理過了),最後再return proxy。

const proxy = new Proxy(target, baseHandler);
reactiveMap.set(target, proxy);
return proxy;

至此,前置準備工作便完成了。接下來我們將利用/src/vue/baseHandler.ts中默認暴露的get方法來進行依賴收集。

依賴收集

讓我們先回到/src/vue/effect.ts,在最下面加上以下程式碼吧。

type Dep = Set<ReactiveEffect>;
type DepsMap = Map<string | symbol, Dep>;
type TargetMap = WeakMap<object, DepsMap>;
const targetMap: TargetMap = new WeakMap();
const trackEffects = (dep: Dep) => {
  if (activeEffect && !dep.has(activeEffect)) {
    dep.add(activeEffect);
  }
};
export const track = (target: object, key: string | symbol) => {
  if (!activeEffect) return;

  let depsMap: DepsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, depsMap = new Map());

  let dep: Dep = depsMap.get(key);
  if (!dep) depsMap.set(key, dep = new Set());

  trackEffects(dep);
};

TargetMap的型別很複雜,把它展開來會長這樣:

type TargetMap = WeakMap<
  object,
  Map<
    string | symbol, Set<ReactiveEffect>
  >
>

它代表一個兩層的映射關係,首先將響應式對象觸發get的key映射在一起,再將這個key有提及它的所有ReactiveEffect映射在一起,形成一個 WeakMap{ target: Map{ key: Set{ ...deps } } } 的結構。
目前看起來可能還很抽象,待我們先繼續往下看,把下面的程式碼看完,或許就會比較清晰了。
接著我們先跳過trackEffects,從track開始看起。

export const track = (target: object, key: string | symbol) => {
  if (!activeEffect) return;

  let depsMap: DepsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, depsMap = new Map());

  let dep: Dep = depsMap.get(key);
  if (!dep) depsMap.set(key, dep = new Set());

  trackEffects(dep);
};

我們可以看到track接收一個普通對象和一個對象的屬性,我們之後可以把觸發了get的代理對象及使get觸發的key傳入track方法,進行依賴收集。

track方法接收到觸發get的對象及使get觸發的key後,會先判斷當前是否有正在作用的effect,若沒有也就不需要依賴收集了,直接return。

if (!activeEffect) return;

接著檢查targetMap是否已經收集過target了,若無,意即這個target是第一次在effect中觸發get,就將它收集到targetMap中,給它映射到一個新的Map實例對象。

let depsMap: DepsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, depsMap = new Map());

然後檢查key是否也已被映射在其中,若無,意即這個target的key是第一次觸發target的get,我們同理需要將它收集到targetMap的depsMap之中,給它映射到一個新的Set,之後用這個Set來儲存跟這個提及target[key]的effect

let dep: Dep = depsMap.get(key);
if (!dep) depsMap.set(key, dep = new Set());

如此一來,我們便完成了targetMap的雙層映射關係,讓target映射到key,key映射到儲存effect的Set,也就是上面說的 WeakMap{ target: Map{ key: Set{ ...deps } } } 這種結構。
最後讓我們看看trackEffects:

const trackEffects = (dep: Dep) => {
  if (activeEffect && !dep.has(activeEffect)) {
    dep.add(activeEffect);
  }
};

只要dep這個儲存effect的set不包含當前正在作用的activeEffect,便將其放入set當中
最後,再到/src/vue/baseHandler.ts的get方法中加上track,修改程式碼如下:

get: (
  target: Record<string, any>,
  key: string,
  receiver: any
) => {
  if (key === ReactiveFlags.IS_REACTIVE) return true;

  track(target, key);

  const res = Reflect.get(target, key, receiver);
  if (res && typeof res === 'object') return reactive(res);

  return res;
}

如此一來,當template中有任何reactive對象的任何key被提及,便會觸發這個reactive對象的get,在get中會將代理的對象以及被提及的key傳入track方法中,在track方法建立對象、key及當前正在作用的activeEffect的兩層映射關係,我們就成功實現了一個陽春的依賴收集。

最後為了方便我們之後在/src/index.ts測試reactive,我們創建一個新文件/src/vue/index.ts:

export * from './reactive';

之後的ref、computed、watch也都從這裡導出,在/src/index.ts就只要import { reactive } from './vue'即可引入,之後會寫的ref、computed、watch亦同。

照慣例,這次的程式碼我也會放上我github的main分支的commit「[Day 04]依賴收集 - 2——利用響應式變數的get收集當前effect」。

明天我們再向數據驅動視圖邁進。


上一篇
[Day 03]依賴收集 - 1——記錄目前正在執行的effect函數
下一篇
[Day 05]驅動視圖
系列文
淺談vue3源碼,很淺的那種31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言