
在實際狀況,effect 函式內部的依賴,常因為條件分支(像是 if...else)而發生變化,這種情況稱為「動態依賴」。
動態依賴會帶來一個問題:在某次執行中不再被使用的舊有依賴,如果沒有被處理好,會殘留在依賴列表中。
後續這個失效依賴的來源被修改時,仍然會觸發 effect 重新執行,這導致不必要的更新或邏輯錯誤。

flag.value 為 true,effect 依賴 flag 和 name,系統建立依賴鏈表 link1(flag) -> link2(name)。flag.value 變成 false,effect 重新執行。effect 進入 else 分支,需要依賴 age。系統會複用 link1(flag),並且幫 age 建立新節點 link3(age)。在沒有清理機制時,舊的 link2(name) 仍然存在在 effect 的依賴鏈表中。此時,如果修改 name.value,因為 link2(name) 的依賴關係還在,effect 會被再次觸發,而當前 effect 的輸出內容實際只與 age 有關。
最直接的方法是在每次 effect 執行前,清空所有依賴再重新收集,但這樣會造成無法複用已有的鏈表節點,效能會較差。
另一個更有效率的方法是,在執行結束後,找出本次沒訪問到的節點,並只清除那一部分。
我們可以在這個情況下加一個判斷,加上判斷之後,讓 effect 從 name 切換到 age 後,depsTail 最後的位置會指向 link3。

當執行完畢後,depsTail 指向 link3,而 link3 存有一個 nextDep 指針,指向舊的 link2(name)。這邊提供了一個可以判斷的依據:
「從 depsTail 指向節點的 nextDep 開始,到鏈表末尾的所有節點,都是本次執行時沒訪問到的依賴。」
以本次案例中:
depsTail 指向link3,link3 此時仍然有 nextDep
就可以清理 link3 的 nextDep,依賴就被清理完成。

還記得我們上次一直觸發按鈕,鏈表上的狀態一直處於:有頭節點deps,並且尾節點depsTail = undefined。
如果 effect 執行時因爲條件判斷而提前 return,沒有訪問任何響應式資料。depsTail 會保持初始 undefined 狀態。
這邊就提供了另一個可以判斷的依據:
「當 effect 執行完畢後,如果 depsTail 是 undefined 並且 deps 頭節點存在,就說明本次執行時沒有訪問任何依賴,應該清除所有舊依賴。」
我們使用 startTrack 和 endTrack 兩個函式來管理 effect 的執行週期。
depsTail 存在,並且 depsTail 的 nextDep 存在,表示包含nextDep的後續鏈表節點應該被移除,傳入clearTracking函式。depsTail = undefined,並且有sub.deps頭節點),此時也應該要被移除,傳入clearTracking函式。//effect.ts
...
...
export class ReactiveEffect {
...
run(){
...
}finally{
endTrack(this)
activeSub = prevSub
}
}
...
}
function endTrack(sub){
const depsTail = sub.depsTail
/**
*
* 狀況一解法: depsTail 存在,並且 depsTail 的 nextDep 存在,表示後續鏈表節點應該移除
*/
if(depsTail){
if(depsTail.nextDep){
clearTracking(depsTail.nextDep)
depsTail.nextDep = undefined
}
// 狀況二:depsTail 不存在,但舊的 deps 頭節點存在,清除所有節點
}else if(sub.deps){
clearTracking(sub.deps)
sub.deps = undefined
}
}

clearTracking 函式的工作是從鏈表中移除一個 link 節點。

由於 link 節點同時存在於 dep 的訂閱者列表 (dep.subs) 和 effect 的依賴列表 (effect.deps) 這兩個雙向鏈表中,移除操作需要更新其在 dep.subs 列表中的 prevSub 和 nextSub 指針,然後再沿著 effect.deps 列表的 nextDep 指針繼續處理下一個待清理的節點。
/**
* 清理依賴函式鏈表
*/
function clearTracking(link: Link){
while(link){
const { prevSub, nextSub, dep, nextDep} = link
/**
* 1. 如果上一個節點有 sub,那就把 nextSub 的下一個節點指向當前節點的下一個節點
* 2. 如果沒有 sub,表示屬於頭節點,那就把 dep.subs 指向當前節點的下一個節點
*/
if(prevSub){
prevSub.nextSub = nextSub
link.nextSub = undefined
}else{
dep.subs = nextSub
}
/**
* 1. 如果下一個節點有 sub,那就把 nextSub 的上一個節點指向當前節點的上一個節點
* 2. 如果下一個節點沒有 sub,表示屬於尾節點,那就把 dep.subsTail 指向當前節點的上一個節點
*/
if(nextSub){
nextSub.prevSub = prevSub
link.prevSub = undefined
}else{
dep.subsTail = prevSub
}
link.dep = link.sub = undefined
link.nextDep = undefined
link = nextDep
}
}
...
...
system.ts 調整export function link(dep, sub){
/**
* 復用節點
* sub.depsTail 是 undefined,並且有 sub.deps 頭節點,表示要復用
*/
const currentDep = sub.depsTail // = link1
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
// nextDep = link1.nextDep = link2
if(nextDep && nextDep.dep === dep){
// link2.dep (name) === age ? → false! 不能復用,需要建立新 link
sub.depsTail = nextDep
return
}
const newLink = {
sub,
dep,
nextDep, // 讓link3的 nextDep 變成 link2
nextSub:undefined,
prevSub:undefined
}
if(dep.subsTail){
dep.subsTail.nextSub = newLink
newLink.prevSub = dep.subsTail
dep.subsTail = newLink
}else {
dep.subs = newLink
dep.subsTail = newLink
}
if(sub.depsTail){
sub.depsTail.nextDep = newLink
sub.depsTail = newLink
}else{
sub.deps = newLink
sub.depsTail = newLink
}
}
system.ts//system.ts
/**
* 依賴項
*/
interface Dep {
// 訂閱者鏈表頭節點
subs: Link | undefined
// 訂閱者鏈表尾節點
subsTail: Link | undefined
}
/**
* 訂閱者
*/
interface Sub{
// 訂閱者鏈表頭節點
deps: Link | undefined
// 訂閱者鏈表尾節點
depsTail: Link | undefined
}
export interface Link {
// 訂閱者
sub: Sub
// 下一個訂閱者節點
nextSub:Link
// 上一個訂閱者節點
prevSub:Link
//依賴項
dep:Dep
//下一個依賴項節點
nextDep: Link | undefined
}
export function link(dep, sub){
/**
* 復用節點
* sub.depsTail 是 undefined,並且有 sub.deps 頭節點,表示要復用
*/
const currentDep = sub.depsTail
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
// 如果 nextDep.dep 等於我當前要收集的 dep
if(nextDep && nextDep.dep === dep){
sub.depsTail = nextDep // 移動指針
return
}
const newLink = {
sub,
dep,
nextDep,// 讓link3的 nextDep 變成 link2
nextSub:undefined,
prevSub:undefined
}
// 將鏈表節點跟 dep 建立關聯關係
if(dep.subsTail){
dep.subsTail.nextSub = newLink
newLink.prevSub = dep.subsTail
dep.subsTail = newLink
}else {
dep.subs = newLink
dep.subsTail = newLink
}
/**
* 將鏈表節點跟 sub 建立關聯關係
* 1.如果有尾節點,表示鏈表現在有無數個節點,在鏈表尾部新增。
* 2.如果沒有尾節點,表示是第一次關聯鏈表,第一個節點頭尾相同。
*/
if(sub.depsTail){
sub.depsTail.nextDep = newLink
sub.depsTail = newLink
}else{
sub.deps = newLink
sub.depsTail = newLink
}
}
export function propagate(subs){
let link = subs
let queuedEffect = []
while (link){
queuedEffect.push(link.sub)
link = link.nextSub
}
queuedEffect.forEach(effect => effect.notify())
}
/**
* 開始追蹤,將 depsTail 設為 undefined
*/
export function startTrack(sub){
sub.depsTail = undefined
}
/**
* 結束追蹤,找到需要清理的依賴
*/
export function endTrack(sub){
const depsTail = sub.depsTail
/**
* 1. depsTail 存在,並且 depsTail 的 nextDep 存在,表示後續鏈表節點應該移除
* 2. 觸發更新完全沒讀到任何依賴(depsTail undefined,並且有頭節點),
* 那就把所有節點清除,否則 effect 函式會繼續被那些不相干的依賴觸發。
*/
if(depsTail){
if(depsTail.nextDep){
clearTracking(depsTail.nextDep)
depsTail.nextDep = undefined
}
}else if(sub.deps){
clearTracking(sub.deps)
sub.deps = undefined
}
}
/**
* 清理依賴函式鏈表
*/
function clearTracking(link: Link){
while(link){
const { prevSub, nextSub, dep, nextDep} = link
/**
* 1. 如果上一個節點有 sub,那就把 nextSub 的下一個節點指向當前節點的下一個節點
* 2. 如果沒有 sub,表示屬於頭節點,那就把 dep.subs 指向當前節點的下一個節點
*/
if(prevSub){// 如果我有上一個節點
prevSub.nextSub = nextSub
link.nextSub = undefined
}else{// 我沒有上一個節點,我是要被刪除的頭節點
dep.subs = nextSub
}
/**
* 1. 如果下一個節點有 sub,那就把 nextSub 的上一個節點指向當前節點的上一個節點
* 2. 如果下一個節點沒有 sub,表示屬於尾節點,那就把 dep.subsTail 指向當前節點的上一個節點
*/
if(nextSub){// 如果我有下一個節點
nextSub.prevSub = prevSub
link.prevSub = undefined
}else{ // 我沒有下一個節點,我是要被刪除的尾節點
dep.subsTail = prevSub
}
// 清空引用
link.dep = undefined
link.sub = undefined
link.nextDep = undefined
// 處理下一個要移除的節點
link = nextDep
}
}
//effect.ts
import { Link, startTrack, endTrack } from './system'
export let activeSub;
export class ReactiveEffect {
// 依賴項鏈表的頭節點指向 link
deps: Link
// 依賴項鏈表的尾節點指向 link
depsTail: Link
constructor(public fn){
}
run(){
const prevSub = activeSub
activeSub = this
startTrack(this)
try{
return this.fn()
}finally{
endTrack(this)
activeSub = prevSub
}
}
notify(){
this.scheduler()
}
scheduler(){
this.run()
}
}
export function effect(fn, options){
const e = new ReactiveEffect(fn)
Object.assign(e, options)
e.run()
const runner = e.run.bind(e)
runner.effect = e
return runner
}

flag.value 是 true。
effect 執行後,依賴了 flag 和 name。
effect 內部形成了一個依賴鏈表: link1(flag) -> link2(name)。
此時 effect 物件的狀態是:
effect.deps 指向 link1 (頭節點)effect.depsTail 指向 link2 (尾節點)effect 執行(flag.value 變成 false)flag.value 變成 false 時,會觸發 effect.run()。startTrack(this) 執行run 方法的一開始,startTrack 會先將 effect.depsTail 重設為 undefined。
effect.deps 仍然指向 link1。effect.depsTail 現在是 undefined。track 被呼叫,接著進入 link(flag的dep, effect) 函式。effect 的狀態變成:
effect.deps 指向 link1。effect.depsTail 指向 link1。else ,讀取到 age.valuetrack 再次被呼叫,進入 link(age的dep, effect) 函式。function link(dep, sub){
// 找到下一個可能的復用節點
const currentDep = sub.depsTail // 現在是 link1
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
// 因為 currentDep 是 link1,所以 nextDep = link1.nextDep,也就是 link2(name)
// 判斷是否可復用
// nextDep (link2) 存在,但 link2 的 dep 是 name,可是我們現在要收集的是 age,條件不成立。
if(nextDep && nextDep.dep === dep){
sub.depsTail = nextDep
return
}
// 由於條件不成立,建立一個新的 link 節點 (link3)
const newLink = { // 這個節點就是 link3
sub,
dep,
// newLink 的 nextDep 被賦值為我們剛剛計算出的 nextDep 變數,也就是 link2(name)
nextDep,
nextSub:undefined,
prevSub:undefined
}
}
age 建立新的節點 link3 時,我們把「上一個節點 (link1) 的下一個節點 (link2)」這個資訊,預先存入了 link3 的 nextDep 屬性中。所以我們可以看到:
effect 依賴了 flag 和 age。link1 被復用,link3 是新建的。effect.depsTail 指向 link3。link3.nextDep 指向此次未訪問的 link2。失效的依賴是要實現響應式系統時需要處理的一個問題。這次我們利用 deps 鏈表和 depsTail 指標,在 effect 執行完畢後,能夠確認並移除不再使用的依賴項目。
同步更新《嘿,日安!》技術部落格
請問大大這系列的 code 有 github repo 可以參考嗎?
因不知道哪裡跟錯了,在狀況一我的 link3 的 nextDep 是 null,跟大大文中說的不一樣 🥲
我現在 code 超亂 可能要再找時間開一下 Repo
如果你有 Repo 的話可以貼上來,我這邊看一下
我後來仔細看發現是個誤會😅 真抱歉 ![]()
👍