在實際狀況,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
}
失效的依賴是要實現響應式系統時需要處理的一個問題。這次我們利用 deps
鏈表和 depsTail
指標,在 effect
執行完畢後,能夠確認並移除不再使用的依賴項目。
同步更新《嘿,日安!》技術部落格
請問大大這系列的 code 有 github repo 可以參考嗎?
因不知道哪裡跟錯了,在狀況一我的 link3
的 nextDep
是 null,跟大大文中說的不一樣 🥲