我們的程式碼已經可以運作,但RefImpl
同時處理資料儲存和鏈表管理,而且不好擴充,所以需要調整一下程式碼。雖然我們前幾章的程式碼已經可以正常運作,但它存在一個很大的問題:RefImpl
這個類別承擔太多的責任。
它既要負責儲存數值 (_value
),又要管理一整套複雜的鏈表操作。
這種設計違反了軟體工程中的 「單一職責原則 (Single Responsibility Principle)」,會使得程式碼難以閱讀、維護和擴充。
首先,我們把 RefImpl 中的鏈表操作抽出來,建立兩個獨立函式:
class RefImpl {
_value;
[ReactiveFlags.IS_REF] = true
subs:Link
subsTail:Link
constructor(value){
this._value = value
}
get value(){
if(activeSub){
trackRef(this)
}
return this._value
}
set value(newValue ){
this._value = newValue
triggerRef(this)
}
}
/*
* 這邊的 dep 是 ref
* 收集依賴,建立 ref 和 effect 之間的鏈表關係
*/
export function trackRef(dep){
const newLink = {
sub: activeSub,
nextSub:undefined,
prevSub:undefined
}
if(dep.subsTail){
dep.subsTail.nextSub = newLink
newLink.prevSub = dep.subsTail
dep.subsTail = newLink
}else {
dep.subs = newLink
dep.subsTail = newLink
}
}
/*
* 觸發 ref 關聯的 effect,重新執行
*/
export function triggerRef(dep){
let link = dep.subs
let queuedEffect = []
while (link){
queuedEffect.push(link.sub)
link = link.nextSub
}
queuedEffect.forEach(effect => effect())
}
接著新增一個 system.ts
檔案,存放鏈表相關邏輯,再次拆分:
activeSub
,有的話建立鏈表關係。
effect(fn)
在呼叫 fn()
前把自己設為 activeSub
,在 fn()
結束後清空,所以我們使用 activeSub 來判斷他是不是當前正在執行的 effect(fn)
。dep
的所有 effect
,因此我們判斷,如果有 dep
的有 subs
,他就觸發更新。dep
(dependency) = 被依賴的對象(如 ref
、reactive
)sub
(subscriber) = 訂閱者(如 effect
、watch
) //system.ts
export interface Link {
sub:Function
nextSub:Link
prevSub:Link
}
/*
* 建立鏈表關係
* dep 是依賴項,像是ref/computed/reactive
* sub 是訂閱者,像是 effect
* 當依賴項目變化(ref),需要通知訂閱者(effect)
*/
export function link(dep, sub){
// 建立新的鏈表節點
const newLink: Link = {
sub, // 指向目前的訂閱者 (activeSub)
nextSub: undefined, // 指向下一個節點 (初始化為空)
prevSub: undefined // 指向前一個節點 (初始化為空)
}
// 如果 dep 已經有尾端訂閱者 (代表鏈表不是空的)
if(dep.subsTail){
// 把尾端節點的 next 指向新的節點
dep.subsTail.nextSub = newLink
// 新節點的 prev 指向原本的尾端
newLink.prevSub = dep.subsTail
// 更新 dep 的尾端指標為新節點
dep.subsTail = newLink
} else {
// 如果 dep 還沒有任何訂閱者 (第一次建立鏈表)
dep.subs = newLink // 鏈表的頭指向新節點
dep.subsTail = newLink // 鏈表的尾也指向新節點
}
}
/*
* 傳播更新的函式
*/
export function propagate(subs){
let link = subs
let queuedEffect = []
while (link){
queuedEffect.push(link.sub)
link = link.nextSub
}
queuedEffect.forEach(effect => effect())
}
//ref.ts
import { activeSub } from './effect'
import { Link, link, propagate } from './system'
enum ReactiveFlags {
IS_REF = '__v_isRef'
}
class RefImpl {
_value;
[ReactiveFlags.IS_REF] = true
subs:Link
subsTail:Link
constructor(value){
this._value = value
}
get value(){
if(activeSub){
trackRef(this)
}
return this._value
}
set value(newValue ){
this._value = newValue
triggerRef(this)
}
}
export function ref(value){
return new RefImpl(value)
}
export function idRef(value){
return !!(value && value[ReactiveFlags.IS_REF])
}
/*
* 這邊的 dep 是 ref
* 收集依賴,建立 ref 和 effect 之間的鏈表關係
*/
export function trackRef(dep){
if(activeSub){
link(dep, activeSub)
}
}
/*
* 觸發 ref 關聯的 effect,重新執行
*/
export function triggerRef(dep){
if(dep.subs){
propagate(dep.subs)
}
}
//effect.ts
// 用來保存目前現在正在執行的 effect 函式
export let activeSub;
export function effect(fn){
activeSub = fn
activeSub()
activeSub = undefined
}
我們新增一個類別,並且給他一個 run
方法:
//effect.ts
export let activeSub;
export class ReactiveEffect {
constructor(public fn){
}
run(){
// 每次執行 fn 之前,把 this 放到 activeSub 上面
activeSub = this
try{
return this.fn()
}finally{
// 執行完成後,activeSub 清空
activeSub = undefined
}
}
}
export function effect(fn){
const e = new ReactiveEffect(fn)
e.run()
}
effect
更改為 ReactiveEffect
類別?主要有三大好處:
狀態封裝: effect
本身其實是有狀態的(例如它依賴了誰、是否正在執行等)。類別是封裝這些狀態和相關行為的最好的辦法。
功能擴充: effect
成為一個類別後,我們在有需要的時候,可以輕鬆幫它新增更多方法,像是剛剛的 run()
就是一個很好的例子。
更好的 this 指向: 在 run()
方法中,activeSub
被賦值為 this
(也就是 ReactiveEffect
的實例),方便後續我們從 effect
實例上獲取更多需要的資訊。
也因此 effect 從函式變成物件,所以我們要調整一下呼叫方式。
//system.ts
export interface Link {
//由於調整,effect 是物件
sub: ReactiveEffect
nextSub:Link
prevSub:Link
}
...
...
export function propagate(subs){
....
// effect 變成物件,改調用 run 方法
queuedEffect.forEach(effect => effect.run())
}
回顧我們今天完成的事,我們把 RefImpl
中複雜的依賴追蹤邏輯,拆分到了獨立的 system.ts
模組,並且把 effect
變成一個更好維護的 ReactiveEffect
類別。
現在,我們的響應式核心是一個由 RefImpl
(負責資料內容)、ReactiveEffect(負責 Side Effect)、以及 system.ts
(連結它們的橋樑)所組成的。
明天我們可以開始處理 effect
相關的新問題了。
同步更新《嘿,日安!》技術部落格
最一開始的 set value(newValue)
少了 triggerRef
😂
set value(newValue ){
this._value = newValue
(this) <--- here
}
感謝 XDDD