16

如何理解WeakMap?.png

When learning the cache function , I finally mentioned the WeakMap cache (cache the input parameter type as an object, and facilitate browser garbage collection when the object's key in WeakMap is not referenced)

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.

And since JavaScript already has a Map type data structure, why is there a data structure called WeakMap type? What does it have to do with garbage collection?

WeakMap has been encountered a long time ago, but has not been systematically studied, so let's find out about it today.

  • Getting to know WeakMap

    • Why is the key of WeakMap weakly referenced?
    • The biggest difference between WeakMap and Map
  • Why is the new WeakMap type added?
  • Instance methods of WeakMap
  • The easiest way to use WeakMap
  • WeakMap stores private data
  • WeakMap class with clear method
  • WeakMap-style automatic garbage collection cache function
  • References

Getting to know WeakMap

  • A WeakMap object is a collection of key-value pairs where keys are weakly referenced
  • 162259355884f0 key of must be object type , value can be any type

Why is the key of WeakMap weakly referenced?

The meaning of weak reference: If the object as the key does not reference it , the garbage collector (GC) will mark it as a target and perform garbage

What types can the key and value of WeakMap be?

key: must be any object type (object, array, Map, WeakMap, etc.)
value: any (any type, so it also includes undefined, null)

The biggest difference between WeakMap and Map

The key of WeakMap is not enumerable, while Map is enumerable.
Non-enumerable means that the key list of WeakMap cannot be obtained.

The reason why it is designed to be non-enumerable is because: if you enumerate the keys of WeakMap, you need to rely on the state of the garbage collector (GC), thereby introducing uncertainty.

Why is the new WeakMap type added?

The map API can be implemented in js by sharing two arrays of 4 APIs (get, set, has, delete): one to store the key and one to store the value. Setting elements on this map synchronously pushes a key and a value to the end of the array. As a result, the indices of key and value are bound to the two arrays. If you get a value from the map, it will traverse all the keys to find a matching one, and then use the matching index to query the corresponding value from the values array.

There are 2 main drawbacks to this implementation:

  1. The first is that the time complexity of set and search is O(n), n is the number of key arrays in the map, because both need to traverse the list to find the required value
  2. Secondly, it will cause memory leak , because the array needs indefinitely to ensure that each key and each value reference . These references will prevent the key from being garbage collected, even if the object is no longer referenced anywhere, the value corresponding to the key will also be blocked from garbage collection.

In contrast, the native WeakMap will keep a "weak" reference to the key. The native WeakMap does not prevent garbage collection and will eventually remove the reference to the key object. "Weak" references also allow values to be garbage collected nicely. WeakMap is especially suitable for scenarios where the information of key mapping is only valuable when it is not garbage collected. In other words, WeakMap is suitable for scenarios where keys are dynamically garbage collected.

Because references are weak, WeakMap keys are not enumerable. There is no way to get a list of keys. If you enumerate the keys of WeakMap, you need to rely on the state of the garbage collector (GC), thus introducing uncertainty. If you must have a key, you should use Map .

Basic Concepts of WeakMap

grammar

new WeakMap()
new WeakMap(iterable)

Where iterable is an array or any iterable object, which needs to have key-value pairs (usually a two-dimensional array). null will be treated as undefined.

iterable is a two-dimensional array
const iterable = [
    [{foo:1}, 1], 
    [[1,2,3], 2], 
    [window, 3]
]
const iwm = new WeakMap(iterable)
// WeakMap {{…} => 1, Window => 3, Array(3) => 2}

instance method

WeakMap.prototype.delete(key)

Delete any value associated with the key. WeakMap.prototype.has(key) returns false after deletion.

WeakMap.prototype.get(key)

Returns the value associated with key, or undefined if there is no associated value.

WeakMap.prototype.has(key)

Returns the result of whether the key exists on the WeakMap.

WeakMap.prototype.set(key, value)

Set the specified value for the corresponding key on the WeakMap object. and returns the WeakMap object

The easiest way to use 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 stores private data

The data and methods on the instance and prototype chain are public, so the implementation details can be hidden through private variables of type 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;

WeakMap class with clear method

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-style automatic garbage collection cache function

are many ways implement the cache function, such as single cache, Map-style full cache, LRU least recent cache, and so on.
So why do you need a WeakMap-style cache function? This is because the input parameter is a cache of object type and is convenient for browser garbage collection.

Cache function implementation

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]"}

How to reflect the garbage collection characteristics of WeakMap?

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

At this time, sometimes the print result of the last weakmap is as follows:

WeakMap:
0: {#document => "[object HTMLDocument]"}
Why do you say "sometimes"?

Because the garbage collection may not be completed when printing, although it will bring uncertainty, it is certain that, assuming that the object is no longer referenced, the key in the WeakMap will be automatically garbage collected by the browser.

Why is only document saved in weakmap?

This is because [1,2,3], function(){}, new WeakMap(), new Map(), new Set() are not referenced any more, and because they are used as keys of WeakMap, so It will be automatically garbage collected by the browser.

How to prevent the key from being garbage collected?

Holds a variable reference to it.

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)

At this point the print result is:

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

This is because [1,2,3], new Map() is continuously referenced by the variables retainArray and retainMap, so it will not be garbage collected. And function(){}, new WeakMap(), new Set() are no longer referenced, and because they are used as the key of WeakMap, they will be automatically garbage collected by the browser.

What about manually triggering garbage collection?

There is a button to manually trigger garbage collection with the help of the Chrome DevTools memory panel tool.
image

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

For example, in the above example, a delay of 5 seconds is set: as long as the "garbage collection button" is manually triggered within 5 seconds after the code runs, you can accurately see that the WeakMap key has been garbage collected.

Of course, the time of 5 seconds can be adjusted artificially, to ensure that you can trigger the garbage collection of WeakMap before the code in setTimeout runs, and you can adjust it appropriately.

Looking forward to communicating with you and making progress together:

  • WeChat public account: Dada big front end / excellent_developers
  • Front-end Q&A mutual aid planet: t.zsxq.com/yBA2Biq

References:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Keyed_collections#weakmap_object
https://fitzgeraldnick.com/2014/01/13/hiding-implementation-details-with-e6-weakmaps.html
https://whatthefuck.is/memoization
https://github.com/reactjs/react-basic


趁你还年轻
4.1k 声望4.1k 粉丝