「我囸你大擊錘沒存到檔!」——自尤雨溪望
大家早安,今天是我第二次寫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所監視的響應式變數更新時會重新觸發回調,執行我們客製化定義的行為。
這句話當中有幾個重點:
由於這次響應式變數更新時並不是重新渲染組件,而是執行客製化定義的行為,因此我們會需要改造一下/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拆成以下幾個步驟:
為了實現這三個步驟,我們建立/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();
};
只要我們做出一個返回值是監視對象的屬性的方法,我們調用這個方法時就能觸發監視對象的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。
我們先看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。
現在再連著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」