关于前端:如何理解WeakMap

31次阅读

共计 5865 个字符,预计需要花费 15 分钟才能阅读完成。

在学习缓存函数时,最初提到了WeakMap 形式缓存(对入参类型为对象做缓存,并且当对象在 WeakMap 中的 key 没有援用时不便浏览器垃圾回收)

If our parameter were an object (rather than a string, like it is above), we could use WeakMap instead of Map in modern browsers. The benefit of WeakMap is that it would automatically“clean up”the entries when our object key is no longer accessible.

而且 JavaScript 既然曾经有了 Map 类型的数据结构,为什么还有一种叫做 WeakMap 类型的数据结构呢?它和垃圾回收有什么关系?

WeakMap 很早之前就遇到过,然而没有零碎学习过,明天就来对它一探到底。

  • 初识 WeakMap

    • WeakMap 的 key 为什么是弱援用的?
    • WeakMap 与 Map 最大的不同
  • 新增 WeakMap 类型是为什么?
  • WeakMap 的实例办法
  • WeakMap 最简应用形式
  • WeakMap 存储公有数据
  • 领有 clear 形式的 WeakMap 类
  • WeakMap 式主动垃圾回收缓存函数
  • 参考资料

初识 WeakMap

  • WeakMap 对象是一组键值对的汇合,其中 key 是弱援用的
  • WeakMap 的key 必须是对象类型,value 能够是任意类型

WeakMap 的 key 为什么是弱援用的?

弱援用的意义:如果是作为 key 的对象没有 任何中央援用它 的话,垃圾收集器 (GC) 会将其标记为指标并且进行 垃圾回收

WeakMap 的 key 和 value 能够是哪些类型

key:必须是任意 object 类型(对象、数组、Map、WeakMap 等等)
value:any(任意类型,所以也包含 undefined,null)

WeakMap 与 Map 最大的不同

WeakMap 的 key 是不可枚举的,而 Map 是可枚举的。
不可枚举就意味着获取不到 WeakMap 的 key 列表。

设计为不可枚举的起因是因为:如果枚举 WeakMap 的 key,那就须要依赖垃圾回收器 (GC) 的状态,从而引入不确定性。

新增 WeakMap 类型是为什么?

map API 在 js 中能够通过共享 4 个 API(get,set,has,delete)的两个数组来实现:一个存储 key,一个存储 value。在这个 map 上设置元素同步推入一个 key 和一个 value 到数组尾部。作为后果,key 和 value 的索引会和两个数组绑定起来。从 map 获取一个值得话,会遍历所有 key 去找到一个匹配的,而后应用这个匹配到的 index 从 values 数组中查问到对应的值。

这样实现的话会有 2 个次要的弊病:

  1. 首先是 set 和 search 的工夫复杂度是 O(n),n 是 map 中 key 数组的数量,因为都须要遍历列表去查找到须要的值
  2. 其次是会造成 内存透露 ,因为数组须要 无期限地去确保每个 key 和每个 value 的援用。这些援用会导致阻止 key 被垃圾回收掉,即便这个对象没有任何中央再援用到了,key 对应的 value 也同样会被阻止垃圾回收。

相比之下,原生的 WeakMap 会放弃对 key 的“弱”援用。原生的 WeakMap 不会阻止垃圾回收,最终会移除对 key 对象的援用。“弱”援用同样能够让 value 很好地垃圾回收。WeakMap 特地实用于 key 映射的信息只有不被垃圾回收时才有价值的场景,换句话说就是WeakMap 实用于动静垃圾回收 key 的场景。

因为援用是弱的,所以 WeakMap 的键是不能枚举的。没有办法去获取 key 的列表。如果枚举 WeakMap 的 key,那就须要依赖垃圾回收器 (GC) 的状态,从而引入不确定性。如果必须要有 key 的话,应该去应用Map

WeakMap 的基本概念

语法

new WeakMap()
new WeakMap(iterable)

其中 iterable 是数组或者任意能够迭代的对象,须要领有 key-value 对(个别是一个二维数组)。null 会被当做 undefined。

iterable 为二维数组
const iterable = [[{foo:1}, 1], 
    [[1,2,3], 2], 
    [window, 3]
]
const iwm = new WeakMap(iterable)
// WeakMap {{…} => 1, Window => 3, Array(3) => 2}

实例办法

WeakMap.prototype.delete(key)

删除 key 关联的任意值。删除后 WeakMap.prototype.has(key) 返回 false。

WeakMap.prototype.get(key)

返回与 key 关联的值,假如不存在关联值得话返回 undefined。

WeakMap.prototype.has(key)

返回 key 在 WeakMap 上是否存在的后果。

WeakMap.prototype.set(key, value)

在 WeakMap 对象上为对应 key 设置指定的 value。并且返回 WeakMap 对象

WeakMap 最简应用形式

const wm1 = new WeakMap(),
      wm2 = new WeakMap(),
      wm3 = new WeakMap();
const o1 = {},
      o2 = function() {},
      o3 = window;

wm1.set(o1, 37);
wm1.set(o2, 'azerty');
wm2.set(o1, o2); // WeakMap 的值能够是任意类型,包含 object 和 function
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // key 和 value 能够是任意对象。包含 WeakMap!wm1.get(o2); // "azerty"
wm2.get(o2); // undefined, wm2 上没有 o2 这个 key
wm2.get(o3); // undefined, 因为这是设置的值

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即便 value 是 undefined)

wm3.set(o1, 37);
wm3.get(o1); // 37

wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false

WeakMap 存储公有数据

实例和原型链上的数据和办法是公开的,所以能够通过 WeakMap 类型的公有变量去暗藏实现细节。

const privates = new WeakMap();

function Public() {
  const me = {// Private data goes here};
  privates.set(this, me);
}

Public.prototype.method = function () {const me = privates.get(this);
  // Do stuff with private data in `me`...
};

module.exports = Public;

领有 clear 形式的 WeakMap 类

class ClearableWeakMap {constructor(init) {this._wm = new WeakMap(init);
  }
  clear() {this._wm = new WeakMap();
  }
  delete(k) {return this._wm.delete(k);
  }
  get(k) {return this._wm.get(k);
  }
  has(k) {return this._wm.has(k);
  }
  set(k, v) {this._wm.set(k, v);
    return this;
  }
}
const key1 = {foo:1};
const key2 = [1,2,3];
const key3 = window;
const cwm = new ClearableWeakMap([[key1, 1], 
    [key2, 2], 
    [key3, 3]
])
cwm.has(key1) // true
console.log(cwm);// ClearableWeakMap {_wm: WeakMap {Window => 3, {…} => 1, Array(3) => 2}}
cwm.clear(); // 垃圾回收以后 WeakMap,并且宣称新的空 WeakMap
cwm.has(key1) // false
console.log(cwm);// ClearableWeakMap {_wm: WeakMap {}}

WeakMap 式主动垃圾回收缓存函数

实现缓存函数的形式有很多种,比方单次缓存,Map 式全量缓存,LRU 最近起码缓存等等。
那么为什么还须要 WeakMap 式的缓存函数呢?这是因为入参为对象类型的缓存且不便浏览器垃圾回收。

缓存函数实现

function memoizeWeakMap(fn) {const wm = new WeakMap();
  return function (arg) {if (wm.has(arg)) {return wm.get(arg);
    }
    const cachedArg = arg;
    const cachedResult = fn(arg);
    wm.set(cachedArg, cachedResult)
    console.log('weakmap object', wm)
    return cachedResult;
  };
}

let testFn = (bar) => {return Object.prototype.toString.call(bar)}; // 这里须要革新一下,革新完返回传入对象的类型

let memoizeWeakMapFn = memoizeWeakMap(testFn);

memoizeWeakMapFn(document) // weakmap 对 document 生成缓存
memoizeWeakMapFn([1,2,3]) // weakmap 对 [1,2,3] 生成缓存
memoizeWeakMapFn(function(){}) // weakmap 对 function(){}生成缓存

memoizeWeakMapFn(new WeakMap())  // weakmap 对 WeakMap 实例生成缓存
memoizeWeakMapFn(new Map()) // weakmap 对 Map 实例生成缓存
memoizeWeakMapFn(new Set())  // weakmap 对 Set 实例生成缓存

WeakMap:0: {Array(3) => "[object Array]"}
1: {function(){} => "[object Function]"}
2: {WeakMap => "[object WeakMap]"}
3: {Map(0) => "[object Map]"}
4: {#document => "[object HTMLDocument]"}
5: {Set(0) => "[object Set]"}

如何体现出 WeakMap 的垃圾回收个性呢

// 疏忽局部代码同上
setTimeout(()=>{memoizeWeakMapFn(document)    
},5000)

此时有时最初一次 weakmap 的打印后果如下:

WeakMap:0: {#document => "[object HTMLDocument]"}
为什么说是“有时”?

因为打印时垃圾回收可能并没有执行实现,尽管会带来不确定性,然而能够确定的是,假如对象没有再被援用,WeakMap 中的 key 会被浏览器主动垃圾回收掉。

为什么 weakmap 中仅保留了 document?

这是因为 [1,2,3], function(){},new WeakMap(),new Map(),new Set() 在前面都没有再持续援用了,而且因为它们作为了 WeakMap 的 key,所以会被浏览器主动垃圾回收掉。

如何不让 key 被垃圾回收掉呢?

放弃一个变量对它的援用。

let memoizeWeakMapFn = memoizeWeakMap(testFn);
let retainArray = [1,2,3]; // 放弃援用防止被垃圾回收
let retainMap = new Map(); // 放弃援用防止被垃圾回收

memoizeWeakMapFn(document) // weakmap 对 document 生成缓存
memoizeWeakMapFn(retainArray) // weakmap 对 [1,2,3] 生成缓存
memoizeWeakMapFn(function(){}) // weakmap 对 function(){}生成缓存

memoizeWeakMapFn(new WeakMap())  // weakmap 对 WeakMap 实例生成缓存
memoizeWeakMapFn(retainMap) // weakmap 对 Map 实例生成缓存
memoizeWeakMapFn(new Set())  // weakmap 对 Set 实例生成缓存

setTimeout(()=>{memoizeWeakMapFn(document)    
},5000)

此时打印后果为:

WeakMap:0: {#document => "[object HTMLDocument]"}
1: {Map(0) => "[object Map]"}
2: {Array(3) => "[object Array]"}

这是因为 [1,2,3], new Map() 被变量 retainArray 和 retainMap 继续援用着,所以不会被垃圾回收。而 function(){},new WeakMap(),new Set()都没有再持续援用了,而且因为它们作为了 WeakMap 的 key,所以会被浏览器主动垃圾回收掉。

如果手动触发垃圾回收呢?

能够借助 Chrome DevTools 的 memory 面板工具,有一个手动触发垃圾回收的按钮。

// ...
setTimeout(()=>{memoizeWeakMapFn(document)    
},5000)

比方在下面的例子中,设置了一个 5 秒的延时:只有代码运行后的 5 秒内,去手动触发“垃圾回收按钮”,就能够很准确地看到 WeakMap 的 key 被垃圾回收了。

当然 5 秒这个工夫是能够人为调整的,保障本人能在 setTimeout 内的代码运行前触发对 WeakMap 的垃圾回收即可,能够适当调大。

参考资料:
https://developer.mozilla.org…
https://developer.mozilla.org…
https://fitzgeraldnick.com/20…
https://whatthefuck.is/memoiz…
https://github.com/reactjs/re…

正文完
 0