在解決了鏈表節點指數增長的問題後,我們還需要注意依賴的有效性。
effect 的執行路徑可能因為條件判斷或程式邏輯不同而改變,導致這次執行中不再需要某些依賴。
如果這些過期依賴沒有被清理:
- 會造成 記憶體洩漏:不需要的鏈表節點一直被保留。
- 會導致 不必要的更新:effect 雖然已經不依賴某個 ref,但這個 ref 的變化仍然會觸發 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 <button id="flagBtn">update flag</button>15 <button id="nameBtn">update name</button>31 collapsed lines
16 <button id="ageBtn">update age</button>17 <script type="module">18 // import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'19 import { ref, effect } from '../dist/reactivity.esm.js'20
21 const flag = ref(true)22 const name = ref('姓名')23 const age = ref(18)24
25 effect(() => {26 console.count('effect')27
28 if(flag.value){29 app.innerHTML = name.value30 }else{31 app.innerHTML = age.value32 }33 })34
35 flagBtn.onclick = () => {36 flag.value = !flag.value37 }38 nameBtn.onclick = () => {39 name.value = '姓名' + Math.random()40 }41 ageBtn.onclick = () => {42 age.value++43 }44 </script>45</body>46</html>
我們現在有三個變數,flag
、name
、age
- 如果
flag
是 true,那點擊 name會觸發更新,但是點擊 age 不會觸發更新。 - 如果
flag
是 false,那點擊 age 會觸發更新,但是點擊 name 不會觸發更新。
這很合理,因為沒有執行的狀況就不應該觸發。
實際情況
- 初始化:輸出
effect: 1
、flag
是 true。 - 點擊
update flag
:effect 更新,輸出effect: 2
、flag
是 false。 - 點擊
update name
:我們期望他沒有反應,因為「如果flag
是 false,點擊 name 不會觸發更新。」 - 但 effect 仍然更新,console 輸出
effect: 3
。
問題說明
我們先來看一下 effect 以及 name 裡面的內容,執行步驟是初始化後先點擊 update flag
,再點擊 update name
。
檢查 Effect 回傳值
1const e = effect(() => {2 console.count('effect')3 if(flag.value){4 app.innerHTML = name.value5 }else{6 app.innerHTML = age.value7 }8})9...10...11nameBtn.onclick = () => {12 name.value = '姓名' + Math.random()13 console.dir(e)14 console.log(name)15}
我們預期 effect 回傳值中,不會有 name
節點:實際上是正確的,目前 Link 節點是 flag
→ age
。
我們預期 name
回傳值中,不會有頭節點跟尾節點,但實際上輸出結果是自己是正在被訂閱的狀態。
異常情況
我們現在了解異常情況是
effect
裡面訂閱flag
、age
沒有name,
是我們要的正確結果。name
裡面發現自己被effect
訂閱,但鏈表上沒有它節點,它應該要被清除。
Effect 依賴清理圖解問題說明
頁面初始化
遇到 flag
收集依賴,建立鏈表節點。
遇到 name
,建立鏈表節點。
點擊按鈕,觸發更新
點擊按鈕,effect 重新執行,run
函式讓depsTail
指向undefined
。
接著link
函式建立鏈表節點,先檢查是否可以復用,頭節點desp
存在depsTail == undefined
,可以復用。
發現可以復用,depsTail
指向 link1,此時 flag
是 false。
由於 flag
是 false,接下來遇到 age
,之前沒有收集依賴過,於是建立新的鏈表節點。
建立age
鏈表節點之後,depsTail
的 nextDep
以及 depsTail
指向新節點。
此時你看黃底色的地方,發現 effect
已經沒有存 link2,但是 link2 的 sub
仍然指向 effect。
所以才會出現 name
更新,仍然會讓 effect 執行的情況。
場景二:提前返回
1const flag = ref(true)2const name = ref('姓名')3const age = ref(18)4let count = 05
6effect(() => {7 console.count('effect')8
9 if(count>0) return10 count++11
12 if(flag.value){13 app.innerHTML = name.value14 }else{15 app.innerHTML = age.value12 collapsed lines
16 }17})18
19flagBtn.onclick = () => {20 flag.value = !flag.value21}22nameBtn.onclick = () => {23 name.value = '姓名' + Math.random()24}25ageBtn.onclick = () => {26 age.value++27}
到時候修正會一起調整,所以我們先來說第二種需要清除依賴的狀況。
預期:只會觸發兩次。(因為 count>0
會返回,無論如何點擊按鈕就不再觸發)
初始化
- 此時
count
是0
,繼續往下執行count++
,count
現在是1。- 讀到
flag.value
→ 收集依賴:flag
。 flag
是true
,接著讀到name.value
→ 收集依賴:name
。
此時 Link 節點是
flag
→name
;depsTail
指向name
。 effect 被flag
與name
兩個 ref 訂閱。
- 點擊 name 按鈕:
- 觸發 effect 重新
run()
。 depsTail
=undefined
- 進入
effect
函式,馬上遇到if (count > 0) return
被中斷,因此鏈表節點上沒有任何改動。
- 觸發 effect 重新
遇到 return 程式應該會中斷,可是你一直點擊 name
按鈕,console.count('effect')
仍然一直觸發更新。
但遇到 if (count > 0) return
,effect 沒有存取任何依賴, deps 鏈表上應該要是空的,可是鏈表上面有之前建立的鏈表沒有清除。
此時鏈表上的狀態一直處於:有頭節點deps
,並且尾節點depsTail
= undefined
。
明天我們會來談怎麼清除這些依賴。