打造響應式系統時,容易遇到的狀況,就是 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>
你打開控制台,你會看到:
問題分析
1effect(() => {2 console.log(count++) // 這裡有兩個操作!3})
實際上等同:
1effect(() => {2 console.log(count.value) // 1. 讀取 count(收集依賴)3 count.value++ // 2. 修改 count(觸發更新)4})
- 讀取
count.value
:這會觸發依賴收集,將當前的effect
註冊為count
的訂閱者。 - 修改
count.value++
:這會觸發更新,Vue 的響應式系統會遍歷所有訂閱者,並執行。由於effect
自身就是訂閱者,它會被重新執行,從而形成了自我觸發的無限循環。
無限循環的流程
同一個 effect 在追蹤期間讀了
count
,又立刻寫回 count
,使自己被再度排入執行隊列;這個「讀→寫→再排隊」的節奏每輪都發生一次,因此形成無限迴圈。
解決方法
Vue 3 使用 tracking
標記來防止同一個 effect 在執行期間被重複加入隊列:
程式碼實作
-
effect.ts
effect.ts 1import { Link, startTrack, endTrack } from './system'23export let activeSub;45export class ReactiveEffect {67...8tracking = false // 是否正在收集依賴910.... -
system.ts
system.ts 1...2...34export function propagate(subs) {5let link = subs6let queuedEffect = []78while (link) {9const sub = link.sub1011// 只有不在執行中的才加入隊列12if(!sub.tracking){13queuedEffect.push(sub)25 collapsed lines14}15link = link.nextSub16}1718queuedEffect.forEach(effect => effect.notify())19}2021/**22* 開始追蹤,將 depsTail 設為 undefined23*/2425export function startTrack(sub) {26sub.depsTail = undefined27sub.tracking = true // 標記為正在執行28}2930/**31* 結束追蹤,找到需要清理的依賴32*/3334export function endTrack(sub) {35sub.tracking = false // 執行結束,取消標記36....3738}system.ts 1...2...34export function propagate(subs) {5let link = subs6let queuedEffect = []78while (link) {9const sub = link.sub1011// 只有不在執行中的才加入隊列12if(!sub.tracking){13queuedEffect.push(sub)25 collapsed lines14}15link = link.nextSub16}1718queuedEffect.forEach(effect => effect.notify())19}2021/**22* 開始追蹤,將 depsTail 設為 undefined23*/2425export function startTrack(sub) {26sub.depsTail = undefined27sub.tracking = true // 標記為正在執行28}2930/**31* 結束追蹤,找到需要清理的依賴32*/3334export function endTrack(sub) {35sub.tracking = false // 執行結束,取消標記36....3738}
如果我們沒有 tracking 機制,effect 在讀 count 時會被收集,寫 count 時又觸發自己,接著再執行自己,永遠停不下來。