iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
Vue.js

從零到一打造 Vue3 響應式系統系列 第 16

Day 16 - 效能處理:LinkPool

  • 分享至 

  • xImage
  •  

banner
昨天,我們完成了「依賴清理」機制,讓 effect 能夠正確處理動態變化的依賴關係。然而,這也帶來了一個新的效能問題:當依賴頻繁變化時,系統需要不斷地建立和刪除 Link 節點,每次建立依賴關係都會觸發記憶體分配,頻繁的分配/釋放會導致:

  • 垃圾回收壓力增大:GC 執行得越頻繁,就越可能造成應用程式的短暫卡頓。
  • 記憶體碎片化:頻繁處理和釋放小塊記憶體,可能導致記憶體空間中出現大量不連續的記憶體碎片。
  • 效能下降:記憶體管理本身的成本

我們可以透過物件池(Object Pool)的設計模式來解決這個問題。

Object Pool 設計模式

物件池(Object Pool)是一種設計模式,用於管理和重複使用物件,避免頻繁新增和刪除物件帶來的效能耗損。

與其在需要時新增、在用完時刪除,不如將可重複使用的物件統一管理起來,實現循環利用。
這個物件池就像一個「倉庫」,預先存放一批可以重複使用的物件。當需要物件時從池中取出,使用完畢後放回到池中,而不是刪除。

這樣可以達到:

  • 重複使用已分配的記憶體:避免了大量的記憶體分配操作
  • 減少垃圾回收次數:降低對主執行緒的干擾

Link Pool

LinkPool 採用單向鏈表結構,並且依照後進先出 (LIFO) 的原則 。主要是因為新增、刪除節點都只需要用到頭節點的操作,時間複雜度為 O(1),效率比較高。

Link Pool 生命週期

我們接下來的執行步驟如下:

  • LinkPool 未使用
  • 移除 Link2 節點
  • 移除 Link1 節點
  • 復用在 linkPool 的節點

可以觀察一下他們的鏈表關係以及 LinkPool 的使用。

初始化

day16-01

linkPool 池是空的,什麼都還沒跑。沒有回收節點可用。

移除 Link2

day16-02

透過endTrack(sub) 判定有「尾段過期」→ 呼叫 clearTracking(Link2)

移除 Link1

day16-03

透過endTrack(sub) 判定有「尾段過期」→ 呼叫 clearTracking(Link1)

加入 Link1

day16-04

執行 link(dep, sub),這次 if (linkPool)true,走重用分支。

LinkPool 程式碼實作

//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 節點的生命週期從「用完後刪除」變成了「循環再生」,從根本上解決因動態依賴而產生的頻繁記憶體分配與回收問題。


同步更新《嘿,日安!》技術部落格


上一篇
Day 15 - Effect:依賴清理實作方案
下一篇
Day 17 - 效能處理:無限循環
系列文
從零到一打造 Vue3 響應式系統17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言