JS 實戰 (3) —— 記憶體管理的隱形冠軍:WeakMap 與響應式緩存
在開發大型單頁應用 (SPA) 時,除了效能,最讓工程師頭痛的就是 「記憶體洩漏 (Memory Leak)」。
你可能已經知道 Vue 3 使用了全新的響應式引擎,但你可能不知道,這個引擎的底層有一位「隱形冠軍」在默默支撐著全站的記憶體安危——它就是 WeakMap。
一、 回顧:垃圾回收 (GC) 的基本法則
JavaScript 是一門具備自動垃圾回收機制的語言。最核心的法則只有一條:「只要這個物件沒人要了(不可達),我就把它回收掉」。
但什麼叫「有人要」?這就引出了 強引用 (Strong Reference) 的概念。
1. 強引用的陷阱
一般的 Map 或 Set 都是強引用。
javascript
let user = { name: "Linyi" };
const cache = new Map();
cache.set(user, "Some User Data");
user = null; // 我們以為這會讓物件被回收
// ❌ 錯了!cache 內部依然持有對該物件的強引用。
// 只要 cache 存在,原本那個物件就永遠不會被回收。這就是記憶體洩漏的典型場景:隨著時間推移,你的緩存 (Cache) 裡堆滿了死掉的物件,直到瀏覽器崩潰。
二、 弱引用 (Weak Reference):優雅的放手
WeakMap 與 WeakSet 的出現就是為了打破僵局。
1. 什麼是 WeakMap?
你可以把它想像成一個「單向存取」的地圖:
- Key 必須是物件。
- 弱引用的力量:WeakMap 對 Key 的引用不計入 GC 的回收判斷程式。
javascript
let node = { id: 1 };
const wm = new WeakMap();
wm.set(node, "Related Metadata");
node = null; // 當 node 在外部沒有其他引用時...
// ✅ GC 會在下次執行時自動把該物件回收,即便 wm 裡還有它。三、 深度解析:為什麼 Vue 3 非它不可?
在第一篇中我們提到,Vue 3 的響應式核心是一個全域的 targetMap。它記錄了「哪個物件」的「哪個屬性」被「哪個 Effect」監聽著。
1. 龐大的依賴地圖
想像你的 App 裡有幾千個元件,每個元件都有幾百個資料點。
javascript
const targetMap = new WeakMap(); // (Object) -> (Map) -> (Set)如果 targetMap 使用普通的 Map:
- 當你銷毀 (Destroy) 一個元件時,該元件內部的數據物件(Target)依然被
targetMap死死抓著。 - 即使該元件已經在画面上消失,它的響應式資料、關聯的 Effects 通通都不會被釋放。
- 元件切換幾次後,記憶體就會瞬間爆表。
因為使用了 WeakMap,當元件銷毀、資料物件外部引用消失時,Vue 3 核心數據地圖也會自動「瘦身」,無需手動清理,這讓框架變得既強大又安全。
四、 效能建議:緩存 (Cache) 的正確姿勢
如果你正在開發一個具備「資料快取」功能的 Service,請遵循以下原則:
- 關聯元數據:如果資料是跟著某個 DOM 元素或物件走的,請用
WeakMap。 - 避免死緩存:不要讓全域變數 (Global scope) 長期持有一個普通的
Map,除非你有明確的清理 (Pruning) 機制。
五、 關鍵區別總結
| 特性 | Map | WeakMap |
|---|---|---|
| Key 型別 | 任意型別 | 必須是物件 |
| 可遍歷性 | 可遍歷 (keys, values, entries) | 不可遍歷 |
| GC 行為 | 持有強引用,阻礙回收 | 持有弱引用,不影響回收 |
| 應用場景 | 通用的鍵值對存儲 | 關聯元數據、依賴收集、私有屬性模擬 |
️ 進階挑戰
- 實作挑戰:試著利用
WeakMap為一個現有的類別實作「真・私有屬性」。外層無論如何都存取不到,且當實例銷毀時,私有屬性資料也會自動回收。 - 深度思考:為什麼
WeakMap是不可遍歷的 (No.size, No.keys())?這與垃圾回收機制有什麼底層關聯?
延伸閱讀與資源
- MDN: WeakMap
- V8 Blog: Memory Management and Trash Collection
- Vue.js: Reactivity Transform & Memory Management