iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Vue.js

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

Day 11 - Effect:Link 節點的複用實作

  • 分享至 

  • xImage
  •  

banner

昨天我們發現了 Effect 的問題:當 effect 被重複觸發時,它會不斷重新收集依賴,導致依賴鏈表指數級增長。

要讓 effect 記住它「訂閱過誰」,最直接的方法就是讓它自己也有一個參照列表。因此,我們分為兩大步:

  • 建立反向依賴鏈表:建立一個新的鏈表,讓 effect 知道自己已經訂閱過哪些 ref,只要 effect 知道自己訂閱過哪些依賴就可以避免新增多餘的鏈表節點,形成了一個雙向的追蹤關係。
  • 實現節點複用機制:下次再次觸發更新之後,就可以藉由查找訂閱過的依賴判斷。如果第一次執行收集過依賴,重復使用之前的鏈表節點,不建立新的節點。如果沒有收集過,就建立一個全新的鏈表節點。

關鍵要素就是:

  1. 需要建立一個新的鏈表讓 effect 紀錄曾經收集過的依賴,這個鏈表我們叫deps
  2. 需要一個判斷 effect 是否是第一次收集依賴的方法。

初始化頁面

day11-01

之前的步驟,剛進入頁面之後, effect 收集依賴,ref 的頭節點 subs 以及尾節點 subsTail 指向 linklinksub 指向 effect

步驟一:建立反向依賴鏈表

day11-02

我們現在要做的事是在我們現有的 Ref -> Link -> Effect 關係上,新增一條從 Effect 出發的反向依賴連結。

之前提到過一個鏈表的必要元素分別是:

  • 頭節點
  • 尾節點
  • 彼此建立的關聯

如上圖,目前頁面上只有一個依賴flag.value,我們可以讓這個鏈表的頭節點 deps 跟尾節點 depsTail 指向 linklinkdep 指向依賴,我們就可以透過關係鏈找到 effect 訂閱過的依賴。

因此我們可以知道三個關鍵的角色。

三個關鍵角色

Effect

  • effect.deps 鏈表:通過 link,記錄 effect 依賴了哪些 ref
  • effect.depsTail:記錄鏈表尾部,目的在可以快速增加新的鏈表節點

Ref(flag)

  • flag.subs 鏈表:通過 link,記錄有哪些 effect 訂閱了此 ref
  • flag.subsTail:記錄鏈表尾部,目的在可以快速增加新的鏈表節點

Link:雙向橋樑節點

Link 是連接 EffectRef 的橋樑,同時存在於兩個鏈表中。

核心屬性:

  • link.sub:指向發起的訂閱者 (effect)
  • link.dep:指向被訂閱的 ref

在 Effect 鏈表中的位置:

  • link.nextDep/prevDep:指向 effect.deps 鏈表的下/上一個節點

在 Ref 鏈表中的位置:

  • link.nextSub/prevSub:指向 ref.subs 鏈表的下/上一個節點

透過上面的方法,我們可以知道三件事:

  1. 雙向查詢:通過 Link 可以找到 effectref
  2. 雙鏈表成員Link 同時是兩個鏈表的成員
    • effect.deps 鏈表的一個節點
    • ref.subs 鏈表的一個節點
  3. 關係管理:一個 Link 代表一個訂閱關係

首先我們更新 effect.ts system.ts 來實作這個新的資料結構。

定義型別

effect.ts

export class ReactiveEffect { 

  // 依賴項鏈表的頭節點指向 link
  deps: Link
  // 依賴項鏈表的尾節點指向 link
  depsTail: Link
  
  ....
  
}

system.ts

//system.ts

/**
 * 依賴項
 */
interface Dep {
  // 訂閱者鏈表頭節點
  subs: Link | undefined
  // 訂閱者鏈表尾節點
  subsTail: Link | undefined
}
/**
 * 訂閱者
 */
interface Sub{
  // 訂閱者鏈表頭節點
  deps: Link | undefined
  // 訂閱者鏈表尾節點
  depsTail: Link | undefined
}

export interface Link {
  // 訂閱者
  sub: Sub
  // 下一個訂閱者節點
  nextSub:Link
  // 上一個訂閱者節點
  prevSub:Link
  //依賴項
  dep:Dep

  //下一個依賴項節點
  nextDep: Link | undefined
}

增加 link 判斷

接著,修改 link 函式,在建立節點時,將它加入 subdeps 鏈表。

//system.ts
export function link(dep, sub){

    const newLink = {
      sub,
      dep,// 加上依賴項
      nextDep:undefined,
      nextSub:undefined,
      prevSub:undefined
    }
    ...
    ...
    
    /**
     * 將鏈表節點跟 sub 建立關聯關係
     * 1.如果有尾節點,表示鏈表現在有無數個節點,在鏈表尾部新增。
     * 2.如果沒有尾節點,表示是第一次關聯鏈表,第一個節點頭尾相同。 
     */
    if(sub.depsTail){
      sub.depsTail.nextDep = newLink
      sub.depsTail = newLink
    }else{
      sub.deps = newLink
      sub.depsTail = newLink
    }
  
}

...
...

步驟二:實現節點複用機制

day11-03

每次 effect 重新執行時,如何判斷是「第一次執行」還是「重新執行」?

我們可以利用頭節點deps與尾節點depsTail 來設定三種狀態:

  • 初始狀態:當從未執行過收集依賴effectdep 鏈表是沒有頭節點deps也沒有尾節點depsTail
  • 執行時:正在重新執行中,需要復用節點:將尾節點depsTail設定成undefined
  • 執行完成:鏈表更新完成:頭尾節點都是Link

當 effect 開始重新執行時,我們將 depsTail 設為 undefined,但保留 deps 頭節點。這樣做的目的是:

  1. 標記重新執行的狀態,讓 link 函式可以知道需要復用節點
  2. deps 鏈表仍然包含之前收集的所有依賴
  3. depsTail 會在復用過程中遍歷移動

所以往後我們判斷是否是第一次依賴收集:只要有頭節點deps,但是尾節點是undefined,那我們就可以知道它曾經執行過。

實作 effect.ts

run(){
    const prevSub = activeSub
    activeSub = this

    // 開始執行,讓尾節點變 undefined
    this.depsTail = undefined
    
    ...
    ...
  }

實作 system.ts

export function link(dep, sub){

/**
 * 復用節點
 * sub.depsTail 是 undefined,並且有 sub.deps 頭節點,表示要復用
 */
  const currentDep = sub.depsTail
  if(currentDep === undefined && sub.deps){
    // 頭節點所連接的 ref 與當前要連接的 ref 相等的話
    // 表示之前收集過依賴,就不收集了
    if(sub.deps.dep === dep){
      sub.depsTail = sub.deps //移動尾節點指針,指向剛剛復用的節點
      return  // 直接返回,不新增節點
    }
  }
  ...
  ...
  
}

完整執行流程

第一次執行

  1. effect 初始化:deps = undefined, depsTail = undefined
  2. 執行 run()depsTail = undefined
  3. 讀取 ref.value
  4. link() 開始判斷:沒有 deps → 建立新鏈表節點
  5. 執行結束:deps = Link1, depsTail = Link1

第二次執行(點擊按鈕)

  1. 執行前:deps = Link1, depsTail = Link1
  2. 執行 run()depsTail = undefined
  3. 讀取 ref.value
  4. link() 開始判斷:
    • 條件:depsTail = undefineddeps 存在 、deps.dep === 當前 dep
    • 設定好depsTail尾節點,return 不建立新的節點。
  5. 執行結束:deps = Link1, depsTail = Link1

透過執行順序可以更好解決這個問題,修正程式碼之後,就沒有指數觸發現象。


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


上一篇
Day 10 - Effect:為何會被指數級觸發?
系列文
從零到一打造 Vue3 響應式系統11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Dylan
iT邦研究生 5 級 ‧ 2025-09-20 22:20:01

有點分不清 sub 和 dep /images/emoticon/emoticon06.gif

我要留言

立即登入留言