昨天,我們完成了「依賴清理」機制,讓 effect
能夠正確處理動態變化的依賴關係。然而,這也帶來了一個新的效能問題:當依賴頻繁變化時,系統需要不斷地建立和刪除 Link 節點,每次建立依賴關係都會觸發記憶體分配,頻繁的分配/釋放會導致:
我們可以透過物件池(Object Pool)的設計模式來解決這個問題。
物件池(Object Pool)是一種設計模式,用於管理和重複使用物件,避免頻繁新增和刪除物件帶來的效能耗損。
與其在需要時新增、在用完時刪除,不如將可重複使用的物件統一管理起來,實現循環利用。
這個物件池就像一個「倉庫」,預先存放一批可以重複使用的物件。當需要物件時從池中取出,使用完畢後放回到池中,而不是刪除。
這樣可以達到:
LinkPool 採用單向鏈表結構,並且依照後進先出 (LIFO) 的原則 。主要是因為新增、刪除節點都只需要用到頭節點的操作,時間複雜度為 O(1),效率比較高。
我們接下來的執行步驟如下:
可以觀察一下他們的鏈表關係以及 LinkPool 的使用。
linkPool
池是空的,什麼都還沒跑。沒有回收節點可用。
透過endTrack(sub)
判定有「尾段過期」→ 呼叫 clearTracking(Link2)
透過endTrack(sub)
判定有「尾段過期」→ 呼叫 clearTracking(Link1)
執行 link(dep, sub)
,這次 if (linkPool)
為 true,走重用分支。
//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
}
let linkPool: Link
export function link(dep, sub) {
const currentDep = sub.depsTail
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
if (nextDep && nextDep.dep === dep) {
sub.depsTail = nextDep
return
}
let newLink
/**
* 查看 linkPool 是否存在,如果存在,表示有復用節點
*/
if(linkPool){
newLink = linkPool
linkPool = linkPool.nextDep
newLink.nextDep = nextDep
newLink.dep = dep
newLink.sub = sub
}else{
/**
* 如果 linkPool 不存在,表示沒有復用節點,那就新建一個節點
*/
newLink = {
sub,
dep,
nextDep,
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
}
}
export function propagate(subs) {
let link = subs
let queuedEffect = []
while (link) {
queuedEffect.push(link.sub)
link = link.nextSub
}
queuedEffect.forEach(effect => effect.notify())
}
export function startTrack(sub) {
sub.depsTail = undefined
}
export function endTrack(sub) {
const depsTail = sub.depsTail
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
if (prevSub) {
prevSub.nextSub = nextSub
link.nextSub = undefined
} else {
dep.subs = nextSub
}
if (nextSub) {
nextSub.prevSub = prevSub
link.prevSub = undefined
} else {
dep.subsTail = prevSub
}
link.dep = undefined
link.sub = undefined
/**
* 把不要的節點放回 linkPool 去復用
*/
link.nextDep = linkPool
linkPool = link
link = nextDep
}
}
透過對 link 和 clearTracking 函式的修改,我們完成了 LinkPool 機制。這看起來是一個很小的修改,但實際上是對響應式系統底層的重要效能優化。Link 節點的生命週期從「用完後刪除」變成了「循環再生」,從根本上解決因動態依賴而產生的頻繁記憶體分配與回收問題。
同步更新《嘿,日安!》技術部落格