1const count = ref(0)2
3effect(() => {4 console.log('count.value ==>', count.value);5})6
7setTimeout(() => {8 count.value++9}, 1000)昨天我們的目標是讓一段簡單的 ref 和 effect 程式碼能夠自動響應。
- 進入頁面輸出
count.value ==> 0 - 一秒後自動輸出
count.value ==> 1
然而,我們初次實作遇到問題:無法正確取值(undefined),也無法在值變更後觸發更新。
為了解決這個問題,我們要去思考 ref 需要做的事:
- 當取得值,
ref要怎麼知道誰在讀取? - 觸發更新之後,
ref要怎麼知道要通知誰?
讓 Ref 知道誰在讀取
1// 原本的程式碼2class RefImpl {3 _value;4 constructor(value){5 this._value = value6 }7}現在要加入 getter 和 setter,讓 count.value 能正常運作:
1class RefImpl {2 _value;3
4 constructor(value){5 this._value = value6 }7
8 // 新增 getter:讀取 value 時觸發9 get value(){10 console.log('有人讀取了 value!')11 return this._value12 }13
14 // 新增 setter:設定 value 時觸發15 set value(newValue){4 collapsed lines
16 console.log('有人修改了 value!')17 this._value = newValue18 }19}
現在看起來 count.value 可以正常返回值,但這個時候還是不知道讀取誰、通知誰。
Effect 函式
1export function effect(fn){2 fn()3}這時候我們需要儲存當前執行的 effect 函式。
1// 用來保存目前現在正在執行的 effect 函式2export let activeSub;3
4export function effect(fn){5 activeSub = fn6 activeSub()7 activeSub = undefined8}這個新版的 effect 函式做了三件事:
- 註冊 Side Effect : 在執行傳入的函式
fn之前,先將它賦值給全域變數activeSub。 - 執行 Side Effect: 立即執行
fn()。如果在執行過程中讀取了某個ref的.value,這個ref就能透過activeSub知道是誰在讀取它。 - 清除 Side Effect: 執行完畢後,必須將
activeSub清空 (設為undefined)。這非常重要,它能確保只有在effect的執行期間,讀取ref的行為才會被視為依賴收集。
收集依賴實作
現在我們要讓 ref 能夠:
- 在被讀取時,記錄是誰在讀取(依賴收集)
- 在被修改時,通知所有讀取者(觸發更新)
我們可以在 getter 在讀取值的時候,判斷activeSub是否存在,來確認當下情況是不是要收集依賴。
1import { activeSub } from './effect'2
3class RefImpl {4 _value;5 subs; // 新增:用來儲存訂閱者6
7 constructor(value){8 this._value = value9 }10
11 // 新增 getter:讀取 value 時觸發12 get value(){13 // 依賴收集:如果有 activeSub,就記錄下來14 if(activeSub){13 collapsed lines
15 this.subs = activeSub16 }17 return this._value18 }19
20 // 新增 setter:設定 value 時觸發21 set value(newValue){22 // 觸發更新:如果有訂閱者,就執行它23 if(this.subs){24 this.subs() // 重新執行 effect25 } // 可簡寫 this.subs?.()26 }27}為了方便在後續的系統中判斷一個變數是否為ref物件,我們可以新增一個輔助函式 isRef 和一個內部標記:
1enum ReactiveFlags {2 IS_REF = '__v_isRef'3}4
5class RefImpl {6 _value;7 subs; // 新增:用來儲存訂閱者8 [ReactiveFlags.IS_REF] = true9
10 ...11}12
13export function isRef(value){14 return !!(value && value[ReactiveFlags.IS_REF])15}現在,讓我們將所有部分串連起來,完整地模擬執行流程。
完整執行流程
頁面初始化與依賴收集
剛開始進入頁面。
1import { ref, effect } from '../dist/reactivity.esm.js'2
3const count = ref(0)程式執行:const count = ref(0)
- 執行
ref(0),建立一個RefImpl實例。 - 此時
count實例的內部狀態為:_value: 0- 沒有任何訂閱者:
subs: undefined - 帶有一個內部標記:
__v_isRef: true
呼叫 effect 函式,並傳入匿名函式 fn 作為參數。
1effect(() => {2 console.log('effect', count.value)3})進入 effect 函式內部
1export let activeSub;2
3export function effect(fn){4 activeSub = fn5 activeSub()6 activeSub = undefined7}- 設定
activeSub:activeSub被賦值為fn:activeSub = fn。 - 立刻執行
fn()- 執行
console.log('effect', count.value) - 觸發了
count實例的get value()。 - 進入
getter內部:
-
if(activeSub)條件成立,activeSub正是我們的fn。1if(activeSub){2this.subs = activeSub3}
- 執行「收集依賴」:
this.subs = activeSub。 - 現在
count實例透過subs屬性,記住了是fn在依賴它。 getter回傳this._value(也就是 0)。console.log輸出:effect 0。
- 執行
activeSub = undefined(執行完成後清空,沒有 effect 在執行)。
此時
count.subs就是傳入effect的函式。- 依賴關係:
count→effect(fn)。
一秒之後
set value(newValue)被呼叫,this._value = 1。this.subs?.()若有訂閱者就呼叫(這裡就是前面存起來的effect函式)- 觸發更新
effect函式再次執行console.log('effect', count.value)→ 讀 getter → 看見沒有activeSub,所以不會收集依賴。- 這會直接執行 effect 函式本體,不是再經過
effect(fn)的包裝流程,所以第二次之後執行 effect 時activeSub是undefined。 console.log輸出:effect 1。
這樣我們就完成響應式依賴收集的最小可行版本。
完整程式碼
ref.ts
1import { activeSub } from './effect'2
3enum ReactiveFlags {4 IS_REF = '__v_isRef'5}6
7class RefImpl {8 _value; // 保存實際數值9 // ref 標記,證實是個 ref10 [ReactiveFlags.IS_REF] = true11
12 subs13 constructor(value){14 this._value = value15 }26 collapsed lines
16
17 // 收集依賴18 get value(){19 // 當有人訪問的時候,可以取得 activeSub20 if(activeSub){21 //當有 activeSub 儲存值,以便更新後觸發22 this.subs = activeSub23 }24 return this._value25 }26
27 // 觸發更新28 set value(newValue){29 this._value = newValue30 // 通知 effect 重新執行,取得最新的 value31 this.subs?.()32 }33}34
35export function ref(value){36 return new RefImpl(value)37}38
39export function isRef(value){40 return !!(value && value[ReactiveFlags.IS_REF])41}effect.ts
1// 用來保存目前現在正在執行的 effect 函式2export let activeSub;3
4export function effect(fn){5 activeSub = fn6 activeSub()7 activeSub = undefined8}