我們的程式碼已經可以運作,但RefImpl
同時處理資料儲存和鏈表管理,而且不好擴充,所以需要調整一下程式碼。雖然我們前幾章的程式碼已經可以正常運作,但它存在一個很大的問題:RefImpl 這個類別承擔了太多的責任。
它既要負責儲存數值 (_value
),又要管理一整套複雜的鏈表操作。
這種設計違反了軟體工程中的 「單一職責原則 (Single Responsibility Principle)」,會使得程式碼難以閱讀、維護和擴充。
ref.ts
首先,我們把 RefImpl 中的鏈表操作抽出來,建立兩個獨立函式:
- trackRef:收集依賴
- triggerRef:觸發更新
1class RefImpl {2 _value;3 [ReactiveFlags.IS_REF] = true4
5 subs:Link6 subsTail:Link7
8 constructor(value){9 this._value = value10 }11
12 get value(){13 if(activeSub){14 trackRef(this)15 }8 collapsed lines
16 return this._value17 }18
19 set value(newValue ){20 this._value = newValue21 (this)22 }23}
1/*2 * 這邊的 dep 是 ref3 * 收集依賴,建立 ref 和 effect 之間的鏈表關係4 */5export function trackRef(dep){6 const newLink = {7 sub: activeSub,8 nextSub:undefined,9 prevSub:undefined10 }11
12 if(dep.subsTail){13 dep.subsTail.nextSub = newLink14 newLink.prevSub = dep.subsTail15 dep.subsTail = newLink19 collapsed lines
16 }else {17 dep.subs = newLink18 dep.subsTail = newLink19 }20}21
22/*23 * 觸發 ref 關聯的 effect,重新執行24 */25export function triggerRef(dep){26 let link = dep.subs27 let queuedEffect = []28
29 while (link){30 queuedEffect.push(link.sub)31 link = link.nextSub32 }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) = 被依賴的對象(如ref
、reactive
)sub
(subscriber) = 訂閱者(如effect
、watch
)
1export interface Link {2 sub:Function3 nextSub:Link4 prevSub:Link5}6
7/*8 * 建立鏈表關係9 * dep 是依賴項,像是ref/computed/reactive10 * sub 是訂閱者,像是 effect11 * 當依賴項目變化(ref),需要通知訂閱者(effect)12 */13export 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 = newLink25 // 新節點的 prev 指向原本的尾端26 newLink.prevSub = dep.subsTail27 // 更新 dep 的尾端指標為新節點28 dep.subsTail = newLink29 } else {30 // 如果 dep 還沒有任何訂閱者 (第一次建立鏈表)31 dep.subs = newLink // 鏈表的頭指向新節點32 dep.subsTail = newLink // 鏈表的尾也指向新節點33 }34}35
36/*37 * 傳播更新的函式38 */39export function propagate(subs){40 let link = subs41 let queuedEffect = []42
43 while (link){44 queuedEffect.push(link.sub)45 link = link.nextSub46 }47
48 queuedEffect.forEach(effect => effect())49}
1import { activeSub } from './effect'2import { Link, link, propagate } from './system'3
4enum ReactiveFlags {5 IS_REF = '__v_isRef'6}7
8class RefImpl {9 _value;10 [ReactiveFlags.IS_REF] = true11
12 subs:Link13 subsTail:Link14 constructor(value){42 collapsed lines
15 this._value = value16 }17
18 get value(){19 if(activeSub){20 trackRef(this)21 }22 return this._value23 }24
25 set value(newValue ){26 this._value = newValue27 triggerRef(this)28 }29}30
31export function ref(value){32 return new RefImpl(value)33}34
35export function idRef(value){36 return !!(value && value[ReactiveFlags.IS_REF])37}38
39/*40 * 這邊的 dep 是 ref41 * 收集依賴,建立 ref 和 effect 之間的鏈表關係42 */43export function trackRef(dep){44 if(activeSub){45 link(dep, activeSub)46 }47}48
49/*50 * 觸發 ref 關聯的 effect,重新執行51 */52export function triggerRef(dep){53 if(dep.subs){54 propagate(dep.subs)55 }56}
Effect.ts
1// 用來保存目前現在正在執行的 effect 函式2export let activeSub;3
4export function effect(fn){5 activeSub = fn6 activeSub()7 activeSub = undefined8}
我們新增一個類別,並且給他一個 run
方法:
1export let activeSub;2
3export class ReactiveEffect {4 constructor(public fn){5
6 }7
8 run(){9 // 每次執行 fn 之前,把 this 放到 activeSub 上面10 activeSub = this11 try{12 return this.fn()13 }finally{14 // 執行完成後,activeSub 清空12 collapsed lines
15 activeSub = undefined16 }17
18 }19}20
21export function effect(fn){22
23 const e = new ReactiveEffect(fn)24 e.run()25
26}
為什麼將 effect
更改為 ReactiveEffect
類別?
主要有三大好處:
-
狀態封裝:
effect
本身其實是有狀態的(例如它依賴了誰、是否正在執行等)。類別是封裝這些狀態和相關行為的最好的辦法。 -
功能擴充:
effect
成為一個類別後,我們在有需要的時候,可以輕鬆幫它新增更多方法,像是剛剛的run()
就是一個很好的例子。 -
更好的 this 指向: 在
run()
方法中,activeSub
被賦值為this
(也就是ReactiveEffect
的實例),方便後續我們從effect
實例上獲取更多需要的資訊。
也因此 effect 從函式變成物件,所以我們要調整一下呼叫方式。
1export interface Link {2 //由於調整,effect 是物件3 sub: ReactiveEffect4 nextSub:Link5 prevSub:Link6}7...8...9export 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
相關的新問題了。