「學到會為止就能學會。」——自尤雨溪望
上回書說道,今天我們終於要正式進入到Vue3源碼的環節。不過在此之前,我必須要先聲明,源碼的學習是艱難的,要理解它的原理、吸收它的思想,絕非一朝一夕所能企及,途中勢必也會遇到一些挫折吧。
但這世上所有知識都是學到會為止就能學會,只要你持之以恆,有朝一日金石必將可鏤,所以請不斷提醒自己想變強的理由,並答應我堅持下去。
那麼進入正題——雖然我很想這麼說,不過在真的開始接觸源碼前,我想先演示一段效能巨差,但與源碼相比極好理解的數據驅動視圖給各位看。請先在/src/index.ts寫下以下程式碼:
import './assets/css/style.less';
interface Lyrics {
value: string;
}
const lyrics = new Proxy({
value: '再多一眼看一眼就會爆炸'
}, {
get: (target: Lyrics, key: keyof Lyrics) => target[key],
set: (target: Lyrics, key: keyof Lyrics, value: string) => {
target[key] = value;
render();
return true;
}
});
const render = () => {
document.body.innerHTML = `
<h1>${lyrics.value}</h1>
`;
};
render();
setTimeout(() => lyrics.value = '再近一點靠近點快被融化', 3000);
首先我們宣告了一個有屬性value為字串的interface Lyrics,並且宣告一個叫lyrics的Proxy對象。
interface Lyrics {
value: string;
}
const lyrics = new Proxy({
value: '再多一眼看一眼就會爆炸'
}, {
get: (target: Lyrics, key: keyof Lyrics) => target[key],
set: (target: Lyrics, key: keyof Lyrics, value: string) => {
target[key] = value;
render();
return true;
}
});
【補充】
給對ES6還沒有那麼熟悉的小夥伴解釋一下Proxy對象,所謂Proxy對象於建構子接收兩個參數,分別是要代理的對象本體,以及一個包含get和set的配置項。這個Proxy對象的功能很單純,就只是在訪問它的時候觸發get,給它的屬性賦值時觸發set而已。
以上述程式碼為例,當你訪問lyrics的value屬性時,會觸發get,並返回lyrics.value;當給lyrics.value賦值時,會先改變lyrics的value屬性,並觸發render函數,return true表示賦值成功。
而在set中調用的render方法,就是給body的內容換成${lyrics.value}。
const render = () => {
document.body.innerHTML = `
<h1>${lyrics.value}</h1>
`;
};
render();
最後我們再於3秒後,將lyrics的value屬性修改為'再近一點靠近點快被融化'。
setTimeout(() => lyrics.value = '再近一點靠近點快被融化', 3000);
因為給Proxy對象的屬性賦值,因此會觸發Proxy對象的set方法
,調用上面宣告的render函數,便實現了當Proxy對象的屬性改變時,更新畫面的效果。
當然如果咱完全不考慮效能,此時可以說是已經實現了數據驅動視圖。但在實際開發上,通常我們不會一整個頁面就只有一個響應式變數,我們的頁面上應該充斥著各式各樣的ref和reactive,而且它們應該散落在各個組件之中,若每當任何一個響應式對象的值改變就重新渲染整個頁面,未免太過浪費效能。
因此尤雨溪的團隊採取了一個策略,他們將渲染每個組件的函數拆開,記錄每個渲染組件的函數包含哪些響應式變數,也記錄每個響應式變數出現在哪些渲染組件的函數。如此一來便能實現當某個響應式變數更新時,僅將有提及它的組件重新渲染。
為了實現數據的雙向綁定,尤雨溪團隊在源碼中寫下了這趟千里之行的第一步——effect函數。
何謂effect函數呢?請先暫且將其理解成渲染函數
,意即將我們所寫的template渲染到畫面的那個函數。
實際上effect函數的功能不止於此,但為了避免各位混亂,這部分我將留到講解computed及watch時再說。
effect函數有兩大重點:
在它的get方法中記錄當前正在執行的effect函數
,將一個effect函數對於一個響應式變數的依賴收集起來。當它的set方法被觸發
,便遍歷調用每一個該響應式變數收集的effect函數
,重新渲染每個提及這個響應式變數的組件。讓我們新建文件/src/vue/effect.ts,接下來我們將在這個檔案中完成effect函數的依賴收集。
先明確一下我們的目標,以及達成目標的步驟吧。
我們的目標是記錄每個響應式變數被哪些effect函數提及,為此我們需要執行以下步驟:
首先我們先在/src/vue/effect.ts寫下以下程式碼:
let activeEffect: ReactiveEffect | undefined;
class ReactiveEffect {
parent?: ReactiveEffect;
constructor(public fn: Function) { }
run() {
try {
this.parent = activeEffect;
activeEffect = this;
return this.fn();
} finally {
activeEffect = this.parent;
}
}
}
首先activeEffect用於記錄當前正在執行的ReactvieEffect對象
,稍後會提到。
然後我們宣告一個類ReactiveEffect,它有parent及fn兩個屬性,以及run方法。
【補充】
在ts,如果在類的建構子所接收的參數加上public關鍵詞修飾,便能自動將這個參數接收到的值變成實例對象的屬性。
例如假設我們有個方法f,當我們將其傳入ReactiveEffect的建構子,便會自動給這個ReactiveEffect對象加上f屬性。const render = () => {/* do something... */} const r = new ReactiveEffect(render) r.fn // 能訪問到上面宣告的方法render
讓我們將焦點放到ReactvieEffect對象的run方法吧。
run() {
try {
this.parent = activeEffect;
activeEffect = this;
return this.fn();
} finally {
activeEffect = this.parent;
}
}
首先effect函數是會彼此嵌套的,我們可以想成是一個頁面裡面可能包含一些組件,組件裡又包含一些子組件,我們經常會遇到一個父組件渲染到一半,就必須先渲染子組件,等子組件渲染完才能回去繼續渲染父組件的情況。像這種先進後出的情況,可以使用棧或鏈表實現,尤雨溪的團隊選擇了後者。
this.parent = activeEffect;
activeEffect = this;
當run方法被調用時,先讓其parent屬性指向當前正在執行的ReactvieEffect對象activeEffect
(可想成是子組件準備開始渲染時,先將parent指向其父組件),再將當前正在執行的ReactvieEffect對象activeEffect指向自己
。
然後執行被傳入ReactvieEffect建構子的方法,可想成是開始渲染子組件。
return this.fn();
當方法被執行完畢,再回去繼續執行this.parent所儲存的ReactvieEffect對象
(可想成當子組件渲染完畢,再回去渲染父組件剩下的部分)。
activeEffect = this.parent;
如此一來,我們便實現了當前正在執行的ReactvieEffect對象的記錄。
最後我們再暴露一個能夠創建ReactiveEffect並調用run方法的方法即可。
export default <T = any>(fn: () => T) => {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
今天我們完成了依賴收集的第一步,用activeEffect記錄當前正在作用的ReactiveEffect對象,並暴露可產生ReactiveEffect對象並調用其run方法的effect方法。程式碼可以在我github的main分支的commit「[Day 03]依賴收集 - 1——記錄目前正在執行的effect函數」找到。
畢竟目前的進度還無法讓我們看到效果,或許今天學習的內容會讓你感到枯燥、疲憊,不過明天我們將正式進入到reactive的環節,然後你就會更疲憊(?)