《你不知道的 WeakMap》番外篇

學習時間:2020.05.26
學習章節:《你不知道的 WeakMap》

一、主要知識點

原文主要複習了“JavaScript垃圾回收機制”,“Map/WeakMap區別”和“WeakMap 屬性和方法”。這很好彌補被我忽視的知識點。
另外,我們可以通過原文,以相同方式再去學 Set/WeakSet,效果會更好,本文後面也會介紹到。

總結開始,先看原文大綱:

image.png
在開始介紹 WeakMap 之前,先複習一遍 JavaScript 中垃圾回收機制,這跟後面的 WeakMap/WeakSet 關係較大。

1. 垃圾回收機制

垃圾回收(Garbage Collection,縮寫爲GC)是一種自動的存儲器管理機制。當某個程序佔用的一部分內存空間不再被這個程序訪問時,這個程序會藉助垃圾回收算法向操作系統歸還這部分內存空間。垃圾回收器可以減輕程序員的負擔,也減少程序中的錯誤。垃圾回收最早起源於LISP語言。
目前許多語言如Smalltalk、Java、C#和D語言都支持垃圾回收器,我們熟知的 JavaScript 具有自動垃圾回收機制。


在 JavaScript 中,原始類型的數據被分配到棧空間中,引用類型的數據會被分配到堆空間中。

1.1 棧空間中的垃圾回收

當函數 showName 調用完成後,通過下移 ESP(Extended Stack Pointer)指針,來銷燬 showName 函數,之後調用其他函數時,將覆蓋掉舊內存,存放另一個函數的執行上下文,實現垃圾回收。
image.png
圖片來自《瀏覽器工作原理與實踐》

1.2 堆空間中的垃圾回收

堆中數據垃圾回收策略的基礎是:代際假說(The Generational Hypothesis)。即:

  1. 大部分對象在內存中存在時間極短,很多對象很快就不可訪問。
  2. 不死的對象將活得更久。

這兩個特點不僅僅適用於 JavaScript,同樣適用於大多數的動態語言,如 Java、Python 等。

V8 引擎將堆空間分爲新生代(存放生存時間短的對象)和老生代(存放生存時間長的對象)兩個區域,並使用不同的垃圾回收器。

  • 副垃圾回收器,主要負責新生代的垃圾回收。
  • 主垃圾回收器,主要負責老生代的垃圾回收。


不管是哪種垃圾回收器,都使用相同垃圾回收流程:標記活動對象和非活動對象,回收非活動對象的內存,最後內存整理。
**

1.2.1 副垃圾回收器

使用 Scavenge 算法處理,將新生代空間對半分爲兩個區域,一個對象區域,一個空閒區域。
image.png
圖片來自《瀏覽器工作原理與實踐》

執行流程:

  • 新對象存在在對象區域,當對象區域將要寫滿時,執行一次垃圾回收;
  • 垃圾回收過程中,首先對對象區域中的垃圾做標記,然後副垃圾回收器將存活的對象複製並有序排列到空閒區域,相當於完成內存整理。
  • 複製完成後,將對象區域和空閒區域翻轉,完成垃圾回收操作,這也讓新生代中兩塊區域無限重複使用。

當然,這也存在一些問題:若複製操作的數據較大則影響清理效率。

JavaScript 引擎的解決方式是:將新生代區域設置得比較小,並採用對象晉升策略(經過兩次回收仍存活的對象,會被移動到老生區),避免因爲新生代區域較小引起存活對象裝滿整個區域的問題。

1.2.2 主垃圾回收器

分爲:標記 - 清除(Mark-Sweep)算法,和標記 - 整理(Mark-Compact)算法


a)標記 - 清除(Mark-Sweep)算法
過程:

  • 標記過程:從一組根元素開始遍歷整個元素,能到達的元素爲活動對象,反之爲垃圾數據;
  • 清除過程:清理被標記的數據,併產生大量碎片內存。(缺點:導致大對象無法分配到足夠的連續內存)

image.png
圖片來自《瀏覽器工作原理與實踐》


b)標記 - 整理(Mark-Compact)算法
過程:

  • 標記過程:從一組根元素開始遍歷整個元素,能到達的元素爲活動對象,反之爲垃圾數據;
  • 整理過程:將所有存活的對象,向一段移動,然後清除端邊界以外的內容。

image.png
圖片來自《瀏覽器工作原理與實踐》

1.3 拓展閱讀

1.《圖解Java 垃圾回收機制》
2.《MDN 內存管理》

2. Map VS WeakMap

2.1 Map 和 WeakMap 主要區別

WeakMap 結構與 Map 結構類似,也是用於生成鍵值對的集合。
區別:

  • Map  對象的鍵可以是任何類型,但 WeakMap  對象中的鍵只能是對象引用( null 除外);
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key
  • WeakMap  不能包含無引用的對象,否則會被自動清除出集合(垃圾回收機制);
  • WeakMap  對象沒有 size 屬性,是不可枚舉的,無法獲取集合的大小。
const map = new WeakMap();
const user1 = {name: 'leo'};
const user2 = {name: 'pingan'};
map.set(user1, 'good~');
map.set(user2, 'hello');
map.map(item => console.log(item))
//Uncaught TypeError: map.map is not a function

2.2 Map 缺點和 WeakMap 優點

1.賦值和搜索操作都是 O(n) 的時間複雜度,因爲這兩個操作都需要遍歷全部整個數組來進行匹配。
2.可能會導致內存泄漏,因爲數組會一直引用着每個鍵和值。


相比之下, WeakMap  持有的是每個鍵對象的 “弱引用”,這意味着在沒有其他引用存在時垃圾回收能正確進行。 原生 WeakMap 的結構是特殊且有效的,其用於映射的 key 只有在其沒有被回收時纔是有效的。

2.3 Map 和 WeakMap 垃圾回收對比

當數據量越大,則垃圾回收效果越明顯。
通過命令行執行 node --expose-gc weakmap.js 查看對比效果。
其中 --expose-gc 參數表示允許手動執行垃圾回收機制。

// weakmap.js
const objNum = 10 * 1024 * 1024;
const useType = 1; // 修改 useType 值來測試Map和WeakMap
const curType = useType == 1 ?"【Map】" : "【WeakMap】";
let arr = new Array(objNum);

function usageSize() {
    const used = process.memoryUsage().heapUsed;
    return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}

if (useType == 1) {
    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    const map = new Map();
    map.set(arr, 1);

    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    arr = null;
    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    console.log("=====")
} else {
    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    const map = new WeakMap();

    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    arr = null;
    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    console.log("=====")
}

3. WeakMap介紹和應用

3.1 WeakMap 介紹

WeakMap 對象是一組鍵/值對的集合,其中的鍵是 弱引用 的。
WeakMap 的 key 只能是 Object 類型。
** 原始數據類型是不能作爲 key 的(比如 Symbol)。
WeakMap只有四個方法可用:get()set()has()delete()

具體屬性和方法介紹,可查看 《MDN WeakMap》。

3.2 WeakMap 應用

原文中介紹了“通過 WeakMap 緩存計算結果”和“在 WeakMap 中保留私有數據”兩種應用場景。
另外還有一種比較常見的場景:以 DOM節點作爲鍵名的場景


場景1:當我們想要爲DOM添加數據時,可使用 WeakMap
好處在於,當DOM元素移除時,對應 WeakMap 記錄也會自動移除:

<div id="WeakMap"></div>
const wm = new WeakMap();
const weakMap = document.getElementById('WeakMap');
wm.set(weakMap, 'some information');
wm.get(weakMap) //"some information"

場景2:當我們想要爲DOM元素添加事件監聽時,可使用 WeakMap

<button id="button1">按鈕1</button>
<button id="button2">按鈕2</button>
const button1 = document.getElementById('button1');
const button2 = document.getElementById('button2');
const handler1 = () => {  console.log("button1 被點擊") };
const handler2 = () => {  console.log("button2 被點擊") };

// 代碼1
button1.addEventListener('click', handler1, false);
button2.addEventListener('click', handler2, false);
 
// 代碼2
const listener = new WeakMap();
 
listener.set(button1, handler1);
listener.set(button2, handler2);
 
button1.addEventListener('click', listener.get(button1), false);
button2.addEventListener('click', listener.get(button2), false);

代碼2比起代碼1的好處是:由於監聽函數是放在 WeakMap 裏面,
則一旦 DOM 對象button1 / button2消失,與它綁定的監聽函數handler1和handler2 也會自動消失。

二、拓展知識

1. 拓展 Set/WeakSet

1.1 Set 和 WeakSet 主要區別

WeakSet  結構與 Set 類似,也是不重複的值的集合。
區別:

  • WeakSet 的成員只能是對象,而不能是其他類型的值;
const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set
  • WeakSet  中的對象都是弱引用,即垃圾回收機制不考慮 WeakSet  對該對象的引用;
  • WeakSet  對象沒有 size 屬性,是不可枚舉的,無法獲取集合的大小。

1.2 Set/WeakSet 垃圾回收對比

通過命令行執行 node --expose-gc weakset.js 查看對比效果。

// weakset.js
const objNum = 5000 * 1024;
const useType = 1;
const curType = useType == 1 ?"【Set】" : "【WeakSet】";
let obj = [];
for (let k = 0; k < objNum; k++) {
    obj[k] = {}
}

function usageSize() {
    const used = process.memoryUsage().heapUsed;
    return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}

if (useType == 1) {
    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    const sets = new Set([...obj]);

    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    obj = null;
    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    console.log("=====")
} else {
    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    const sets = new WeakSet(obj);

    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    obj = null;
    global.gc();
    console.log(objNum + '個' + curType + '佔用內存:' + usageSize());

    console.log("=====")
}

三、總結

本文首先複習了《你不知道的 WeakMap》中核心知識點,重新回顧了“垃圾回收機制”,“Map VS WeakMap”和“WeakMap 介紹和應用”,最後延伸複習了“Set/WeakSet”相關知識點。
在實際業務開發中,最好也能考慮垃圾回收機制的合理使用,這也是提升產品性能的一個非常常用的方式。

語雀知識庫:Cute-FrontEnd

公衆號:前端自習課

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章