嘿,日安!

Day 4 - 核心概念:收集依賴、觸發更新

13th September 2025
Front-end
Vue3
IT鐵人賽2025
從零到一打造 Vue3 響應式系統
Last updated:14th September 2025
7 Minutes
1280 Words
1
const count = ref(0)
2
3
effect(() => {
4
console.log('count.value ==>', count.value);
5
})
6
7
setTimeout(() => {
8
count.value++
9
}, 1000)

昨天我們的目標是讓一段簡單的 refeffect 程式碼能夠自動響應。

  1. 進入頁面輸出 count.value ==> 0
  2. 一秒後自動輸出 count.value ==> 1

然而,我們初次實作遇到問題:無法正確取值(undefined),也無法在值變更後觸發更新。

為了解決這個問題,我們要去思考 ref 需要做的事:

  1. 當取得值,ref 要怎麼知道誰在讀取?
  2. 觸發更新之後,ref 要怎麼知道要通知誰?

讓 Ref 知道誰在讀取

1
// 原本的程式碼
2
class RefImpl {
3
_value;
4
constructor(value){
5
this._value = value
6
}
7
}

現在要加入 getter 和 setter,讓 count.value 能正常運作:

1
class RefImpl {
2
_value;
3
4
constructor(value){
5
this._value = value
6
}
7
8
// 新增 getter:讀取 value 時觸發
9
get value(){
10
console.log('有人讀取了 value!')
11
return this._value
12
}
13
14
// 新增 setter:設定 value 時觸發
15
set value(newValue){
4 collapsed lines
16
console.log('有人修改了 value!')
17
this._value = newValue
18
}
19
}

default

現在看起來 count.value 可以正常返回值,但這個時候還是不知道讀取誰、通知誰。

Effect 函式

1
export function effect(fn){
2
fn()
3
}

這時候我們需要儲存當前執行的 effect 函式。

effect.ts
1
// 用來保存目前現在正在執行的 effect 函式
2
export let activeSub;
3
4
export function effect(fn){
5
activeSub = fn
6
activeSub()
7
activeSub = undefined
8
}

這個新版的 effect 函式做了三件事:

  1. 註冊 Side Effect : 在執行傳入的函式 fn之前,先將它賦值給全域變數activeSub
  2. 執行 Side Effect: 立即執行 fn()。如果在執行過程中讀取了某個 ref.value,這個 ref就能透過activeSub 知道是誰在讀取它。
  3. 清除 Side Effect: 執行完畢後,必須將 activeSub清空 (設為undefined)。這非常重要,它能確保只有在 effect的執行期間,讀取ref的行為才會被視為依賴收集。

收集依賴實作

現在我們要讓 ref 能夠:

  1. 在被讀取時,記錄是誰在讀取(依賴收集)
  2. 在被修改時,通知所有讀取者(觸發更新)

我們可以在 getter 在讀取值的時候,判斷activeSub是否存在,來確認當下情況是不是要收集依賴。

ref.ts
1
import { activeSub } from './effect'
2
3
class RefImpl {
4
_value;
5
subs; // 新增:用來儲存訂閱者
6
7
constructor(value){
8
this._value = value
9
}
10
11
// 新增 getter:讀取 value 時觸發
12
get value(){
13
// 依賴收集:如果有 activeSub,就記錄下來
14
if(activeSub){
13 collapsed lines
15
this.subs = activeSub
16
}
17
return this._value
18
}
19
20
// 新增 setter:設定 value 時觸發
21
set value(newValue){
22
// 觸發更新:如果有訂閱者,就執行它
23
if(this.subs){
24
this.subs() // 重新執行 effect
25
} // 可簡寫 this.subs?.()
26
}
27
}

為了方便在後續的系統中判斷一個變數是否為ref物件,我們可以新增一個輔助函式 isRef 和一個內部標記:

1
enum ReactiveFlags {
2
IS_REF = '__v_isRef'
3
}
4
5
class RefImpl {
6
_value;
7
subs; // 新增:用來儲存訂閱者
8
[ReactiveFlags.IS_REF] = true
9
10
...
11
}
12
13
export function isRef(value){
14
return !!(value && value[ReactiveFlags.IS_REF])
15
}

現在,讓我們將所有部分串連起來,完整地模擬執行流程。


完整流執行流程

頁面初始化與依賴收集

剛開始進入頁面。

1
import { ref, effect } from '../dist/reactivity.esm.js'
2
3
const count = ref(0)

程式執行:const count = ref(0)

  • 執行 ref(0),建立一個 RefImpl 實例。
  • 此時 count 實例的內部狀態為:
    • _value: 0
    • 沒有任何訂閱者:subs: undefined
    • 帶有一個內部標記:__v_isRef: true

呼叫 effect 函式,並傳入匿名函式 fn 作為參數。

1
effect(() => {
2
console.log('effect', count.value)
3
})

進入 effect 函式內部

1
export let activeSub;
2
3
export function effect(fn){
4
activeSub = fn
5
activeSub()
6
activeSub = undefined
7
}
  1. 設定 activeSub: activeSub 被賦值為 fnactiveSub = fn
  2. 立刻執行 fn()
    1. 執行 console.log('effect', count.value)
    2. 觸發了 count 實例的 get value()
    3. 進入 getter 內部:
    • if(activeSub) 條件成立,activeSub 正是我們的 fn

      1
      if(activeSub){
      2
      this.subs = activeSub
      3
      }
    1. 執行「收集依賴」:this.subs = activeSub
    2. 現在 count 實例透過 subs 屬性,記住了是 fn 在依賴它。
    3. getter 回傳 this._value(也就是 0)。
    4. console.log 輸出:effect 0
  3. activeSub = undefined(執行完成後清空,沒有 effect 在執行)。

此時

  1. count.subs 就是傳入 effect 的函式。
  2. 依賴關係:counteffect(fn)

一秒之後

  • set value(newValue) 被呼叫,this._value = 1
  • this.subs?.() 若有訂閱者就呼叫(這裡就是前面存起來的 effect 函式)
  • 觸發更新 effect 函式再次執行
    • console.log('effect', count.value) → 讀 getter → 看見沒有 activeSub,所以不會收集依賴。
    • 這會直接執行 effect 函式本體,不是再經過 effect(fn) 的包裝流程,所以第二次之後執行 effect 時 activeSubundefined
    • console.log 輸出:effect 1

這樣我們就完成響應式依賴收集的最小可行版本。


完整程式碼

ref.ts

1
import { activeSub } from './effect'
2
3
enum ReactiveFlags {
4
IS_REF = '__v_isRef'
5
}
6
7
class RefImpl {
8
_value; // 保存實際數值
9
// ref 標記,證實是個 ref
10
[ReactiveFlags.IS_REF] = true
11
12
subs
13
constructor(value){
14
this._value = value
15
}
26 collapsed lines
16
17
// 收集依賴
18
get value(){
19
// 當有人訪問的時候,可以取得 activeSub
20
if(activeSub){
21
//當有 activeSub 儲存值,以便更新後觸發
22
this.subs = activeSub
23
}
24
return this._value
25
}
26
27
// 觸發更新
28
set value(newValue){
29
this._value = newValue
30
// 通知 effect 重新執行,取得最新的 value
31
this.subs?.()
32
}
33
}
34
35
export function ref(value){
36
return new RefImpl(value)
37
}
38
39
export function isRef(value){
40
return !!(value && value[ReactiveFlags.IS_REF])
41
}

effect.ts

1
// 用來保存目前現在正在執行的 effect 函式
2
export let activeSub;
3
4
export function effect(fn){
5
activeSub = fn
6
activeSub()
7
activeSub = undefined
8
}
Article title:Day 4 - 核心概念:收集依賴、觸發更新
Article author:日安
Release time:13th September 2025