趁着清闲,在家学习 vue-next 源码,注意到其对 WeakMap
的应用;大家应该都知道,新版本 Vue 与旧版本相比,实现机制从 defineProperty
转变为 Proxy
,却可能未曾注意到细致末节的差异。
ES6 之前,前端使用 Map 时,一般都是通过对象来模拟;对象的 key
值只能是字符串,即使传入的 key
值不是字符串形式,也会被转为字符串,而 Map 没有这种限制:
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
来开启手动清理):
// 打印方法格式化
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 的话,运行结果为:
初始化:
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 数组到底占用了多少内存?
在浏览器中,可以通过开发者工具来查看使用 WeakMap 和 Map 时对内存的影响:
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 执行结果:
Map 执行结果:
在前端项目中对 WeakMap 的应用场景,可以参见阮一峰老师 WeakMap-的用途。
参考:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。