嘿,日安!

Day 17 - 效能處理:無限循環

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

打造響應式系統時,容易遇到的狀況,就是 effect 在執行期間同時「讀取」又「寫入」同一個依賴,這會造成自我觸發(self-trigger)。

effect 為了讀值而被追蹤進依賴,但它在同一次執行中又改了這個值,導致立刻再次觸發自己,形成無限迴圈。

可以看下面範例

1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
<meta charset="UTF-8" />
5
<title>Document</title>
6
<style>
7
body {
8
padding: 150px;
9
}
10
</style>
11
</head>
12
<body>
13
<div id="app"></div>
14
<script type="module">
15
// import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
10 collapsed lines
16
import { ref, effect } from '../dist/reactivity.esm.js'
17
18
const count = ref(0)
19
20
effect(() => {
21
console.log(count++)
22
})
23
</script>
24
</body>
25
</html>

你打開控制台,你會看到:

default

問題分析

1
effect(() => {
2
console.log(count++) // 這裡有兩個操作!
3
})

實際上等同:

1
effect(() => {
2
console.log(count.value) // 1. 讀取 count(收集依賴)
3
count.value++ // 2. 修改 count(觸發更新)
4
})
  • 讀取 count.value:這會觸發依賴收集,將當前的 effect 註冊為 count 的訂閱者。
  • 修改 count.value++:這會觸發更新,Vue 的響應式系統會遍歷所有訂閱者,並執行。由於 effect 自身就是訂閱者,它會被重新執行,從而形成了自我觸發的無限循環。

無限循環的流程

default 同一個 effect 在追蹤期間讀了 count,又立刻寫回 count,使自己被再度排入執行隊列;這個「讀→寫→再排隊」的節奏每輪都發生一次,因此形成無限迴圈。

解決方法

Vue 3 使用 tracking 標記來防止同一個 effect 在執行期間被重複加入隊列:

default

程式碼實作

  • effect.ts

    effect.ts
    1
    import { Link, startTrack, endTrack } from './system'
    2
    3
    export let activeSub;
    4
    5
    export class ReactiveEffect {
    6
    7
    ...
    8
    tracking = false // 是否正在收集依賴
    9
    10
    ....
  • system.ts

    system.ts
    1
    ...
    2
    ...
    3
    4
    export function propagate(subs) {
    5
    let link = subs
    6
    let queuedEffect = []
    7
    8
    while (link) {
    9
    const sub = link.sub
    10
    11
    // 只有不在執行中的才加入隊列
    12
    if(!sub.tracking){
    13
    queuedEffect.push(sub)
    25 collapsed lines
    14
    }
    15
    link = link.nextSub
    16
    }
    17
    18
    queuedEffect.forEach(effect => effect.notify())
    19
    }
    20
    21
    /**
    22
    * 開始追蹤,將 depsTail 設為 undefined
    23
    */
    24
    25
    export function startTrack(sub) {
    26
    sub.depsTail = undefined
    27
    sub.tracking = true // 標記為正在執行
    28
    }
    29
    30
    /**
    31
    * 結束追蹤,找到需要清理的依賴
    32
    */
    33
    34
    export function endTrack(sub) {
    35
    sub.tracking = false // 執行結束,取消標記
    36
    ....
    37
    38
    }
    system.ts
    1
    ...
    2
    ...
    3
    4
    export function propagate(subs) {
    5
    let link = subs
    6
    let queuedEffect = []
    7
    8
    while (link) {
    9
    const sub = link.sub
    10
    11
    // 只有不在執行中的才加入隊列
    12
    if(!sub.tracking){
    13
    queuedEffect.push(sub)
    25 collapsed lines
    14
    }
    15
    link = link.nextSub
    16
    }
    17
    18
    queuedEffect.forEach(effect => effect.notify())
    19
    }
    20
    21
    /**
    22
    * 開始追蹤,將 depsTail 設為 undefined
    23
    */
    24
    25
    export function startTrack(sub) {
    26
    sub.depsTail = undefined
    27
    sub.tracking = true // 標記為正在執行
    28
    }
    29
    30
    /**
    31
    * 結束追蹤,找到需要清理的依賴
    32
    */
    33
    34
    export function endTrack(sub) {
    35
    sub.tracking = false // 執行結束,取消標記
    36
    ....
    37
    38
    }

如果我們沒有 tracking 機制,effect 在讀 count 時會被收集,寫 count 時又觸發自己,接著再執行自己,永遠停不下來。

Article title:Day 17 - 效能處理:無限循環
Article author:日安
Release time:26th September 2025