iT邦幫忙

2023 iThome 鐵人賽

DAY 3
1
Vue.js

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

[Day 03]依賴收集 - 1——記錄目前正在執行的effect函數

  • 分享至 

  • xImage
  •  

「學到會為止就能學會。」——自尤雨溪望

心理建設

上回書說道,今天我們終於要正式進入到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函數。
何謂effect函數呢?請先暫且將其理解成渲染函數,意即將我們所寫的template渲染到畫面的那個函數。

實際上effect函數的功能不止於此,但為了避免各位混亂,這部分我將留到講解computed及watch時再說。

effect函數有兩大重點:

  1. 依賴收集
    先記錄目前正在執行哪個effect函數(可先理解為正在渲染哪個組件),當effect函數中提及響應式變數,使其get方法被觸發,我們便在它的get方法中記錄當前正在執行的effect函數,將一個effect函數對於一個響應式變數的依賴收集起來。
  2. 驅動視圖
    當一個響應式變數的值產生改變,意即當它的set方法被觸發,便遍歷調用每一個該響應式變數收集的effect函數,重新渲染每個提及這個響應式變數的組件。

讓我們新建文件/src/vue/effect.ts,接下來我們將在這個檔案中完成effect函數的依賴收集。

依賴收集

先明確一下我們的目標,以及達成目標的步驟吧。
我們的目標是記錄每個響應式變數被哪些effect函數提及,為此我們需要執行以下步驟:

  1. 記錄目前正在執行的effect函數(可先理解為目前正在渲染哪個組件)。
  2. 當effect函數作用時,觸發其提及的響應式變數的get。
  3. 響應式變數的get被觸發時,收集當前正在作用的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的環節,然後你就會更疲憊(?)


上一篇
[Day 02]webpack環境搭建
下一篇
[Day 04]依賴收集 - 2——利用響應式變數的get收集當前effect
系列文
淺談vue3源碼,很淺的那種31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言