iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0
Vue.js

從零到一打造 Vue3 響應式系統系列 第 7

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

  • 分享至 

  • xImage
  •  

banner

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

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

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

day07-01

ref.ts

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

  • trackRef:收集依賴
  • triggerRef:觸發更新
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 檔案,存放鏈表相關邏輯,再次拆分:

  • trackRef:收集依賴入口函式,判斷是否有 activeSub,有的話建立鏈表關係。
    • effect(fn) 在呼叫 fn() 前把自己設為 activeSub,在 fn() 結束後清空,所以我們使用 activeSub 來判斷他是不是當前正在執行的 effect(fn)
  • triggerRef:觸發更新入口函式,要找通知曾經訂閱過這個 dep 的所有 effect,因此我們判斷,如果有 dep 的有 subs,他就觸發更新。
  • dep (dependency) = 被依賴的對象(如 refreactive
  • sub (subscriber) = 訂閱者(如 effectwatch
 //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.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 類別?

主要有三大好處:

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

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

  3. 更好的 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 相關的新問題了。


同步更新《嘿,日安!》技術部落格


上一篇
Day 6 - 首次實作: 鏈表應用
下一篇
Day 8 - Effect: 深入剖析巢狀 effect
系列文
從零到一打造 Vue3 響應式系統10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Dylan
iT邦研究生 5 級 ‧ 2025-09-16 13:59:53

最一開始的 set value(newValue) 少了 triggerRef 😂

set value(newValue ){ 
  this._value = newValue
  (this)  <--- here
}
heyrian iT邦新手 5 級 ‧ 2025-09-16 15:03:04 檢舉

感謝 XDDD

我要留言

立即登入留言