iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
Vue.js

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

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

  • 分享至 

  • xImage
  •  

banner

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

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

可以看下面範例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <style>
      body {
        padding: 150px;
      }
    </style>
  </head>
<body>
  <div id="app"></div>
  <script type="module">
    // import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    import { ref, effect } from '../dist/reactivity.esm.js'

     const count = ref(0)

      effect(() => {
        console.log(count++)
      })
  </script>
</body>
</html>

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

day17-01

問題分析

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

實際上等同:

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

無限循環的流程

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

解決方法

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

day17-03

程式碼實作

  • effect.ts

    //effect.ts
    import { Link, startTrack, endTrack } from './system'
    
    export let activeSub;
    
    export class ReactiveEffect {
    
    ...
      tracking = false // 是否正在收集依賴
    
    ....
    
  • system.ts

    //system.ts
    
    ...
    ...
    
    export function propagate(subs) {
      let link = subs
      let queuedEffect = []
    
      while (link) {
        const sub = link.sub
    
        // 只有不在執行中的才加入隊列
        if(!sub.tracking){
          queuedEffect.push(sub)
        }
        link = link.nextSub
      }
    
      queuedEffect.forEach(effect => effect.notify())
    }
    
    /**
     * 開始追蹤,將 depsTail 設為 undefined
     */
    
    export function startTrack(sub) {
      sub.depsTail = undefined
      sub.tracking = true // 標記為正在執行
    }
    
    /**
     * 結束追蹤,找到需要清理的依賴
     */
    
    export function endTrack(sub) {
      sub.tracking = false // 執行結束,取消標記
     ....
    
    }
    
    //system.ts
    
    ...
    ...
    
    export function propagate(subs) {
      let link = subs
      let queuedEffect = []
    
      while (link) {
        const sub = link.sub
    
        // 只有不在執行中的才加入隊列
        if(!sub.tracking){
          queuedEffect.push(sub)
        }
        link = link.nextSub
      }
    
      queuedEffect.forEach(effect => effect.notify())
    }
    
    /**
     * 開始追蹤,將 depsTail 設為 undefined
     */
    
    export function startTrack(sub) {
      sub.depsTail = undefined
      sub.tracking = true // 標記為正在執行
    }
    
    /**
     * 結束追蹤,找到需要清理的依賴
     */
    
    export function endTrack(sub) {
      sub.tracking = false // 執行結束,取消標記
     ....
    
    }
    

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


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


上一篇
Day 16 - 效能處理:LinkPool
系列文
從零到一打造 Vue3 響應式系統17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Dylan
iT邦研究生 5 級 ‧ 2025-09-26 21:25:39

最後 system.ts 的 code 好像重複貼了兩次🤔

我要留言

立即登入留言