嘿,日安!

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

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

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

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

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

關鍵要素就是:

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

初始化頁面

default

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

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

default

我們現在要做的事是在我們現有的 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

1
export class ReactiveEffect {
2
3
// 依賴項鏈表的頭節點指向 link
4
deps: Link
5
// 依賴項鏈表的尾節點指向 link
6
depsTail: Link
7
8
....
9
10
}

system.ts

system.ts
1
/**
2
* 依賴項
3
*/
4
interface Dep {
5
// 訂閱者鏈表頭節點
6
subs: Link | undefined
7
// 訂閱者鏈表尾節點
8
subsTail: Link | undefined
9
}
10
/**
11
* 訂閱者
12
*/
13
interface Sub{
19 collapsed lines
14
// 訂閱者鏈表頭節點
15
deps: Link | undefined
16
// 訂閱者鏈表尾節點
17
depsTail: Link | undefined
18
}
19
20
export interface Link {
21
// 訂閱者
22
sub: Sub
23
// 下一個訂閱者節點
24
nextSub:Link
25
// 上一個訂閱者節點
26
prevSub:Link
27
//依賴項
28
dep:Dep
29
30
//下一個依賴項節點
31
nextDep: Link | undefined
32
}

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

system.ts
1
export function link(dep, sub){
2
3
const newLink = {
4
sub,
5
dep,// 加上依賴項
6
nextDep:undefined,
7
nextSub:undefined,
8
prevSub:undefined
9
}
10
...
11
...
12
13
/**
14
* 將鏈表節點跟 sub 建立關聯關係
15 collapsed lines
15
* 1.如果有尾節點,表示鏈表現在有無數個節點,在鏈表尾部新增。
16
* 2.如果沒有尾節點,表示是第一次關聯鏈表,第一個節點頭尾相同。
17
*/
18
if(sub.depsTail){
19
sub.depsTail.nextDep = newLink
20
sub.depsTail = newLink
21
}else{
22
sub.deps = newLink
23
sub.depsTail = newLink
24
}
25
26
}
27
28
...
29
...

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

default

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

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

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

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

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

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

實作 effect.ts

1
run(){
2
const prevSub = activeSub
3
activeSub = this
4
5
// 開始執行,讓尾節點變 undefined
6
this.depsTail = undefined
7
8
...
9
...
10
}

實作 system.ts

1
export function link(dep, sub){
2
3
/**
4
* 復用節點
5
* sub.depsTail 是 undefined,並且有 sub.deps 頭節點,表示要復用
6
*/
7
const currentDep = sub.depsTail
8
if(currentDep === undefined && sub.deps){
9
// 頭節點所連接的 ref 與當前要連接的 ref 相等的話
10
// 表示之前收集過依賴,就不收集了
11
if(sub.deps.dep === dep){
12
sub.depsTail = sub.deps //移動尾節點指針,指向剛剛復用的節點
13
return // 直接返回,不新增節點
14
}
15
}
4 collapsed lines
16
...
17
...
18
19
}

完整執行流程

第一次執行

  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

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

Article title:Day 11 - Effect:Link 節點的複用實作
Article author:日安
Release time:20th September 2025