無用知識集:JavaScript 中的 WeakMap

趁着悠閒,在家學習 vue-next 源碼,注意到其對 WeakMap應用;你們應該都知道,新版本 Vue 與舊版本相比,實現機制從 defineProperty 轉變爲 Proxy,卻可能不曾注意到細緻末節的差別。前端

ES6 以前,前端使用 Map 時,通常都是經過對象來模擬;對象的 key 值只能是字符串,即便傳入的 key 值不是字符串形式,也會被轉爲字符串,而 Map 沒有這種限制:vue

let o = {};
let o1 = {toString(){ return 1; }};
let o2 = {};
o[o1] = 1;
o[o2] = 2;
o; // {1: 1, [object Object]: 2}

WeakMap 與 Map 的區別

理論上的區別

  • WeakMap 的 key 只能是對象類型(null除了 typeof 的時候被當成對象的 bug,任什麼時候候都不算對象):
const m1 = new Map();
const wm = new WeakMap();

const k1 = { foo: 1 };
const k2 = 'k2';

m1.set(k1, 'v1');
m1.set(k2, 'v2')
wm.set(k1, 'v1');
// wm.set(k2, 'v2'); // TypeError: Invalid value used as weak map key
  • WeakMap 的 key 不計入垃圾回收機制。WeakMap 的 key 所引用的對象都是弱引用,即垃圾回收機制不將該引用考慮在內;一旦 key 不被其餘地方引用,那麼就會被回收。正是由於 WeakMap 鍵名的不肯定性,它沒有 keys()values()entries() 方法,也沒有 size 屬性;此外,WeakMap 尚未 clear() 方法(在最初的時候,是有這個方法的)。

實操

前面說了,WeakMap 相對於 Map 的一個重要區別就是其垃圾回收機制;空口無憑,下面用一段代碼來展現其回收效果(由於瀏覽器環境下,沒法經過代碼控制垃圾回收,因此如下代碼經過 Node.js 運行,node --expose-gc 來開啓手動清理):node

// 打印方法格式化
function format(value) {
    return `${(value / 1024 / 1024).toFixed(2)} M`
}
function print(m) {
    console.log(`HeapTotal: ${format(m.heapTotal)}`);
    console.log(`HeapUsed: ${format(m.heapUsed)}`);
}

// 手動垃圾回收
global.gc();
console.log('初始化:');
print(process.memoryUsage()); // node中查看內存狀態的方法,咱們目前只須要關注 heapTotal,heapUsed
let vm = new WeakMap();
let key = new Array(20 * 1024 * 1024);
vm.set(key, 'foo');
global.gc();
console.log('回收前:');
print(process.memoryUsage());

key = null; // key 置爲 null 以後,只被 vm 鍵名引用
global.gc();
console.log('回收後:');
print(process.memoryUsage());
// 上面代碼的運行結果:
// node --expose-gc weakmap-test.js
// 初始化:
// HeapTotal: 4.52 M
// HeapUsed: 1.86 M
// 回收前:
// HeapTotal: 166.52 M
// HeapUsed: 161.71 M
// 回收後:
// HeapTotal: 10.52 M
// HeapUsed: 1.71 M

若是將上述代碼中的 WeakMap 替換爲 Map 的話,運行結果爲:react

初始化:
HeapTotal: 4.52 M
HeapUsed: 1.86 M
回收前:
HeapTotal: 166.52 M
HeapUsed: 161.71 M
回收後:
HeapTotal: 170.52 M
HeapUsed: 161.71 M

使用 Map 時,因爲數組佔用的內存未被回收,HeapUsed 在垃圾回收先後無差別;而使用 WeakMap 時,在垃圾回收後,內存恢復。另外,咱們在代碼中建立了一個長度爲 20M 的空數組,但其佔用的內存卻約爲 160M,也就是說,每一個空元素佔用的內存大小爲 8 個字節。若是有興趣的話,能夠查看在 Chrome 中 JavaScript 數組到底佔用了多少內存?git

在瀏覽器中,能夠經過開發者工具來查看使用 WeakMap 和 Map 時對內存的影響:es6

const map = new WeakMap(); // 將 WeakMap 替換爲 Map 後,再次點擊 Take Heap Snapshot,耗時明顯增長;而且從圖中能夠看出,array 佔用的內存,並無被回收。
(function () {
  // 當即執行函數,執行完以後,除了 map/weakmap 以外,無其餘方式能夠訪問到;當其被做爲 map 的 key 時,內存不會回收;而做爲 WeakMap 的 key 時,執行完成後會被回收;
  const arr1 = new Array(20 * 1024 * 1024);
  const arr2 = new Array(20 * 1024 * 1024);
  const arr3 = new Array(20 * 1024 * 1024);
  const arr4 = new Array(20 * 1024 * 1024);
  const arr5 = new Array(20 * 1024 * 1024);
  const arr6 = new Array(20 * 1024 * 1024);
  const arr7 = new Array(20 * 1024 * 1024);
  const arr8 = new Array(20 * 1024 * 1024);
  const arr9 = new Array(20 * 1024 * 1024);
  const arr10 = new Array(20 * 1024 * 1024);
  const arr11 = new Array(20 * 1024 * 1024);
  const arr12 = new Array(20 * 1024 * 1024);
  const arr13 = new Array(20 * 1024 * 1024);
  const arr14 = new Array(20 * 1024 * 1024);
  const arr15 = new Array(20 * 1024 * 1024);
  const arr16 = new Array(20 * 1024 * 1024);
  const arr17 = new Array(20 * 1024 * 1024);
  const arr18 = new Array(20 * 1024 * 1024);
  map.set(arr1, 'arr1');
  map.set(arr2, 'arr2');
  map.set(arr3, 'arr3');
  map.set(arr4, 'arr4');
  map.set(arr5, 'arr5');
  map.set(arr6, 'arr6');
  map.set(arr7, 'arr7');
  map.set(arr8, 'arr8');
  map.set(arr9, 'arr9');
  map.set(arr10, 'arr10');
  map.set(arr11, 'arr11');
  map.set(arr12, 'arr12');
  map.set(arr13, 'arr13');
  map.set(arr14, 'arr14');
  map.set(arr15, 'arr15');
  map.set(arr16, 'arr16');
  map.set(arr17, 'arr17');
  map.set(arr18, 'arr18');
})();

WeakMap 執行結果:
weak-map.png
Map 執行結果:
map-memory.pnggithub

在前端項目中對 WeakMap 的應用場景,能夠參見阮一峯老師 WeakMap-的用途web

參考:chrome

相關文章
相關標籤/搜索