打造響應式系統時,容易遇到的狀況,就是 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>
你打開控制台,你會看到:
effect(() => {
console.log(count++) // 這裡有兩個操作!
})
實際上等同:
effect(() => {
console.log(count.value) // 1. 讀取 count(收集依賴)
count.value++ // 2. 修改 count(觸發更新)
})
count.value
:這會觸發依賴收集,將當前的 effect
註冊為 count
的訂閱者。count.value++
:這會觸發更新,Vue 的響應式系統會遍歷所有訂閱者,並執行。由於 effect
自身就是訂閱者,它會被重新執行,從而形成了自我觸發的無限循環。
同一個 effect 在追蹤期間讀了 count
,又立刻寫回 count
,使自己被再度排入執行隊列;這個「讀→寫→再排隊」的節奏每輪都發生一次,因此形成無限迴圈。
Vue 3 使用 tracking
標記來防止同一個 effect 在執行期間被重複加入隊列:
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 時又觸發自己,接著再執行自己,永遠停不下來。
同步更新《嘿,日安!》技術部落格