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}