嘿,日安!

Day 7 - 關注點分離: 拆分 track、trigger

16th September 2025
Front-end
Vue3
IT鐵人賽2025
從零到一打造 Vue3 響應式系統
Last updated:16th September 2025
7 Minutes
1269 Words

我們的程式碼已經可以運作,但RefImpl 同時處理資料儲存和鏈表管理,而且不好擴充,所以需要調整一下程式碼。雖然我們前幾章的程式碼已經可以正常運作,但它存在一個很大的問題:RefImpl 這個類別承擔了太多的責任。

它既要負責儲存數值 (_value),又要管理一整套複雜的鏈表操作。

這種設計違反了軟體工程中的 「單一職責原則 (Single Responsibility Principle)」,會使得程式碼難以閱讀、維護和擴充。

default

ref.ts

首先,我們把 RefImpl 中的鏈表操作抽出來,建立兩個獨立函式:

  • trackRef:收集依賴
  • triggerRef:觸發更新
1
class RefImpl {
2
_value;
3
[ReactiveFlags.IS_REF] = true
4
5
subs:Link
6
subsTail:Link
7
8
constructor(value){
9
this._value = value
10
}
11
12
get value(){
13
if(activeSub){
14
trackRef(this)
15
}
8 collapsed lines
16
return this._value
17
}
18
19
set value(newValue ){
20
this._value = newValue
21
(this)
22
}
23
}
1
/*
2
* 這邊的 dep 是 ref
3
* 收集依賴,建立 ref 和 effect 之間的鏈表關係
4
*/
5
export function trackRef(dep){
6
const newLink = {
7
sub: activeSub,
8
nextSub:undefined,
9
prevSub:undefined
10
}
11
12
if(dep.subsTail){
13
dep.subsTail.nextSub = newLink
14
newLink.prevSub = dep.subsTail
15
dep.subsTail = newLink
19 collapsed lines
16
}else {
17
dep.subs = newLink
18
dep.subsTail = newLink
19
}
20
}
21
22
/*
23
* 觸發 ref 關聯的 effect,重新執行
24
*/
25
export function triggerRef(dep){
26
let link = dep.subs
27
let queuedEffect = []
28
29
while (link){
30
queuedEffect.push(link.sub)
31
link = link.nextSub
32
}
33
queuedEffect.forEach(effect => effect())
34
}

接著新增一個 system.ts 檔案,存放鏈表相關邏輯,再次拆分:

  • trackRef:收集依賴入口函式,判斷是否有 activeSub,有的話建立鏈表關係。
    • effect(fn) 在呼叫 fn() 前把自己設為 activeSub,在 fn() 結束後清空,所以我們使用 activeSub 來判斷他是不是當前正在執行的 effect(fn)
  • triggerRef:觸發更新入口函式,要找通知曾經訂閱過這個 dep 的所有 effect,因此我們判斷,如果有 dep 的有 subs,他就觸發更新。
  • dep (dependency) = 被依賴的對象(如 refreactive
  • sub (subscriber) = 訂閱者(如 effectwatch
system.ts
1
export interface Link {
2
sub:Function
3
nextSub:Link
4
prevSub:Link
5
}
6
7
/*
8
* 建立鏈表關係
9
* dep 是依賴項,像是ref/computed/reactive
10
* sub 是訂閱者,像是 effect
11
* 當依賴項目變化(ref),需要通知訂閱者(effect)
12
*/
13
export function link(dep, sub){
14
// 建立新的鏈表節點
35 collapsed lines
15
const newLink: Link = {
16
sub, // 指向目前的訂閱者 (activeSub)
17
nextSub: undefined, // 指向下一個節點 (初始化為空)
18
prevSub: undefined // 指向前一個節點 (初始化為空)
19
}
20
21
// 如果 dep 已經有尾端訂閱者 (代表鏈表不是空的)
22
if(dep.subsTail){
23
// 把尾端節點的 next 指向新的節點
24
dep.subsTail.nextSub = newLink
25
// 新節點的 prev 指向原本的尾端
26
newLink.prevSub = dep.subsTail
27
// 更新 dep 的尾端指標為新節點
28
dep.subsTail = newLink
29
} else {
30
// 如果 dep 還沒有任何訂閱者 (第一次建立鏈表)
31
dep.subs = newLink // 鏈表的頭指向新節點
32
dep.subsTail = newLink // 鏈表的尾也指向新節點
33
}
34
}
35
36
/*
37
* 傳播更新的函式
38
*/
39
export function propagate(subs){
40
let link = subs
41
let queuedEffect = []
42
43
while (link){
44
queuedEffect.push(link.sub)
45
link = link.nextSub
46
}
47
48
queuedEffect.forEach(effect => effect())
49
}
ref.ts
1
import { activeSub } from './effect'
2
import { Link, link, propagate } from './system'
3
4
enum ReactiveFlags {
5
IS_REF = '__v_isRef'
6
}
7
8
class RefImpl {
9
_value;
10
[ReactiveFlags.IS_REF] = true
11
12
subs:Link
13
subsTail:Link
14
constructor(value){
42 collapsed lines
15
this._value = value
16
}
17
18
get value(){
19
if(activeSub){
20
trackRef(this)
21
}
22
return this._value
23
}
24
25
set value(newValue ){
26
this._value = newValue
27
triggerRef(this)
28
}
29
}
30
31
export function ref(value){
32
return new RefImpl(value)
33
}
34
35
export function idRef(value){
36
return !!(value && value[ReactiveFlags.IS_REF])
37
}
38
39
/*
40
* 這邊的 dep 是 ref
41
* 收集依賴,建立 ref 和 effect 之間的鏈表關係
42
*/
43
export function trackRef(dep){
44
if(activeSub){
45
link(dep, activeSub)
46
}
47
}
48
49
/*
50
* 觸發 ref 關聯的 effect,重新執行
51
*/
52
export function triggerRef(dep){
53
if(dep.subs){
54
propagate(dep.subs)
55
}
56
}

Effect.ts

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

我們新增一個類別,並且給他一個 run 方法:

effect.ts
1
export let activeSub;
2
3
export class ReactiveEffect {
4
constructor(public fn){
5
6
}
7
8
run(){
9
// 每次執行 fn 之前,把 this 放到 activeSub 上面
10
activeSub = this
11
try{
12
return this.fn()
13
}finally{
14
// 執行完成後,activeSub 清空
12 collapsed lines
15
activeSub = undefined
16
}
17
18
}
19
}
20
21
export function effect(fn){
22
23
const e = new ReactiveEffect(fn)
24
e.run()
25
26
}

為什麼將 effect 更改為 ReactiveEffect 類別?

主要有三大好處:

  1. 狀態封裝: effect 本身其實是有狀態的(例如它依賴了誰、是否正在執行等)。類別是封裝這些狀態和相關行為的最好的辦法。

  2. 功能擴充: effect 成為一個類別後,我們在有需要的時候,可以輕鬆幫它新增更多方法,像是剛剛的 run() 就是一個很好的例子。

  3. 更好的 this 指向: 在 run() 方法中,activeSub 被賦值為 this (也就是 ReactiveEffect 的實例),方便後續我們從 effect 實例上獲取更多需要的資訊。

也因此 effect 從函式變成物件,所以我們要調整一下呼叫方式。

system.ts
1
export interface Link {
2
//由於調整,effect 是物件
3
sub: ReactiveEffect
4
nextSub:Link
5
prevSub:Link
6
}
7
...
8
...
9
export function propagate(subs){
10
....
11
// effect 變成物件,改調用 run 方法
12
queuedEffect.forEach(effect => effect.run())
13
}

回顧我們今天完成的事,我們把 RefImpl 中複雜的依賴追蹤邏輯,拆分到了獨立的 system.ts 模組,並且把 effect 變成一個更好維護的 ReactiveEffect 類別。

現在,我們的響應式核心是一個由 RefImpl(負責資料內容)、ReactiveEffect(負責 Side Effect)、以及 system.ts(連結它們的橋樑)所組成的。

明天我們可以開始處理 effect 相關的新問題了。

Article title:Day 7 - 關注點分離: 拆分 track、trigger
Article author:日安
Release time:16th September 2025