昨天,我們完成了「依賴清理」機制,讓 effect
能夠正確處理動態變化的依賴關係。然而,這也帶來了一個新的效能問題:當依賴頻繁變化時,系統需要不斷地建立和刪除 Link 節點,每次建立依賴關係都會觸發記憶體分配,頻繁的分配/釋放會導致:
- 垃圾回收壓力增大:GC 執行得越頻繁,就越可能造成應用程式的短暫卡頓。
- 記憶體碎片化:頻繁處理和釋放小塊記憶體,可能導致記憶體空間中出現大量不連續的記憶體碎片。
- 效能下降:記憶體管理本身的成本
我們可以透過物件池(Object Pool)的設計模式來解決這個問題。
Object Pool 設計模式
物件池(Object Pool)是一種設計模式,用於管理和重複使用物件,避免頻繁新增和刪除物件帶來的效能耗損。
與其在需要時新增、在用完時刪除,不如將可重複使用的物件統一管理起來,實現循環利用。 這個物件池就像一個「倉庫」,預先存放一批可以重複使用的物件。當需要物件時從池中取出,使用完畢後放回到池中,而不是刪除。
這樣可以達到:
- 重複使用已分配的記憶體:避免了大量的記憶體分配操作
- 減少垃圾回收次數:降低對主執行緒的干擾
Link Pool
LinkPool 採用單向鏈表結構,並且依照後進先出 (LIFO) 的原則 。主要是因為新增、刪除節點都只需要用到頭節點的操作,時間複雜度為 O(1),效率比較高。
Link Pool 生命週期
我們接下來的執行步驟如下:
- LinkPool 未使用
- 移除 Link2 節點
- 移除 Link1 節點
- 復用在 linkPool 的節點
可以觀察一下他們的鏈表關係以及 LinkPool 的使用。
初始化
linkPool
池是空的,什麼都還沒跑。沒有回收節點可用。
移除 Link2
透過endTrack(sub)
判定有「尾段過期」→ 呼叫 clearTracking(Link2)
移除 Link1
透過endTrack(sub)
判定有「尾段過期」→ 呼叫 clearTracking(Link1)
加入 Link1
執行 link(dep, sub)
,這次 if (linkPool)
為 true,走重用分支。
LinkPool 程式碼實作
1interface Dep {2 subs: Link | undefined3 subsTail: Link | undefined4}5
6interface Sub {7 deps: Link | undefined8 depsTail: Link | undefined9}10
11export interface Link {12 sub: Sub13 nextSub: Link122 collapsed lines
14 prevSub: Link15 dep: Dep16
17 nextDep: Link | undefined18}19
20let linkPool: Link21
22export function link(dep, sub) {23
24 const currentDep = sub.depsTail25 const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep26 if (nextDep && nextDep.dep === dep) {27 sub.depsTail = nextDep28 return29 }30
31 let newLink32
33 /**34 * 查看 linkPool 是否存在,如果存在,表示有復用節點35 */36
37 if(linkPool){38 newLink = linkPool39 linkPool = linkPool.nextDep40 newLink.nextDep = nextDep41 newLink.dep = dep42 newLink.sub = sub43 }else{44 /**45 * 如果 linkPool 不存在,表示沒有復用節點,那就新建一個節點46 */47 newLink = {48 sub,49 dep,50 nextDep,51 nextSub: undefined,52 prevSub: undefined53 }54 }55
56 if (dep.subsTail) {57 dep.subsTail.nextSub = newLink58 newLink.prevSub = dep.subsTail59 dep.subsTail = newLink60 } else {61 dep.subs = newLink62 dep.subsTail = newLink63 }64
65 if (sub.depsTail) {66 sub.depsTail.nextDep = newLink67 sub.depsTail = newLink68 } else {69 sub.deps = newLink70 sub.depsTail = newLink71 }72
73}74
75export function propagate(subs) {76 let link = subs77 let queuedEffect = []78
79 while (link) {80 queuedEffect.push(link.sub)81 link = link.nextSub82 }83
84 queuedEffect.forEach(effect => effect.notify())85}86
87export function startTrack(sub) {88 sub.depsTail = undefined89}90
91export function endTrack(sub) {92 const depsTail = sub.depsTail93
94 if (depsTail) {95 if (depsTail.nextDep) {96 clearTracking(depsTail.nextDep)97 depsTail.nextDep = undefined98 }99 } else if (sub.deps) {100 clearTracking(sub.deps)101 sub.deps = undefined102 }103
104}105
106function clearTracking(link: Link) {107 while (link) {108 const { prevSub, nextSub, dep, nextDep } = link109
110 if (prevSub) {111 prevSub.nextSub = nextSub112 link.nextSub = undefined113 } else {114 dep.subs = nextSub115 }116
117 if (nextSub) {118 nextSub.prevSub = prevSub119 link.prevSub = undefined120 } else {121 dep.subsTail = prevSub122 }123
124 link.dep = undefined125 link.sub = undefined126
127 /**128 * 把不要的節點放回 linkPool 去復用129 */130 link.nextDep = linkPool131 linkPool = link132
133 link = nextDep134 }135}
透過對 link 和 clearTracking 函式的修改,我們完成了 LinkPool 機制。這看起來是一個很小的修改,但實際上是對響應式系統底層的重要效能優化。Link 節點的生命週期從「用完後刪除」變成了「循環再生」,從根本上解決因動態依賴而產生的頻繁記憶體分配與回收問題。