Why is WeakMap used as a "cache" in Vue 3 responsive source code?

Posted by nephish on Mon, 13 Dec 2021 11:52:54 +0100


Why is WeakMap used as a "cache" in Vue 3 responsive source code?

When reading the code of Vue 3's responsive principle, I saw that during responsive processing, a "cache" was created for each object using WeakMap. The code is as follows:

// Pay attention to the following code!
const reactiveMap = new WeakMap();

// The core hijacking method handles the logic of get and set
const mutableHandlers = {
    get,
    set
}

function reactive(target: object) {
    return createReactiveObject(target, mutableHandlers, reactiveMap);
}

/**
 * @description Create a responsive object 
 * @param {Object} target Target object to be proxied
 * @param {Function} baseHandlers Different processing functions corresponding to each method
 * @param {Object} proxyMap WeakMap object
 */
function createReactiveObject(target, baseHandlers, proxyMap) {
    // Detect whether the target is an object. It is not returned directly. No proxy is performed
    if (!isObject(target)) {
        return target
    }
    const existsProxy = proxyMap.get(target);
    // If the object has been proxied, it will be returned directly without repeated proxies
    if (existsProxy) {
        return existsProxy
    }
    // If it has not been represented, a proxy object is created
    const proxy = new Proxy(target,baseHandlers);
    // Cache to avoid duplicate agents, that is, to avoid the occurrence of reactive(reactive(Object))
    proxyMap.set(target,proxy); 
    return proxy
}

As can be seen from the above code, the function of the WeakMap cache is to prevent objects from being repeatedly represented.

Why does Vue 3 use WeakMap to cache proxy objects? Why not use other methods for caching, such as Map?

What is WeakMap

A WeakMap object is a collection of key value pairs whose keys are weakly referenced. The key must be an object, and the value can be arbitrary.

grammar

new WeakMap([iterable])

Iteratable is an array (binary array) or other iteratable object whose elements are key value pairs. Each key value pair will be added to the new WeakMap.

method

There are four methods for WeakMap: get, set, has and delete. Let's take a look at their general usage:

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); // Value can be any value, including an object or a function
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // Keys and values can be any object, or even another WeakMap object

wm1.get(o2); // "azerty"
wm2.get(o2); // undefined, there is no o2 key in wm2
wm2.get(o3); // Undefined, the value is undefined

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (even if the value is undefined)

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

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

Why use WeakMap instead of Map

In JavaScript, The map API can share two arrays through four API methods (one storage key and one storage value). In this way, when setting the value of this map, the key and value will be added to the end of the two arrays at the same time. Thus, the indexes of the key and value correspond in the two arrays. When taking values from the map, you need to traverse all the keys, and then use the index to retrieve the corresponding values from the array of stored values.

However, such an implementation will have two major disadvantages. First, the time complexity of assignment and search operations is O(n) (n is the number of key value pairs), because both operations need to traverse the entire array for matching.

Another disadvantage is that it may lead to memory leakage, because the array will always refer to each key and value. This reference makes it impossible for the garbage collection algorithm to recycle them, even if no other references exist.

let jser = { name: "dachui" };

let array = [ jser ];

jser = null; // Override reference

In the above code, we put an object into an array. As long as the array exists, the object also exists, even if there is no other reference to the object.

let jser = { name: "dachui" };

let map = new Map();
map.set(jser, "");

jser = null; // Override reference

Similarly, if we use an object as the key of a regular Map, the object will exist when the Map exists. It will occupy memory and will not be recycled by the garbage collection mechanism.

In contrast, the native WeakMap holds weak references to each key object, which means that garbage collection can be carried out correctly when no other references exist.

Because of this weak reference, the keys of WeakMap are not enumerable (no method can give all keys). If the key is enumerable, its list will be affected by the garbage collection mechanism, resulting in uncertain results. Therefore, if you want a list of key values for this type of object, you should use Map.

To sum up, we can draw the following conclusion: the object pointed to by the key of WeakMap is not included in the garbage collection mechanism.

Therefore, if you want to add data to the object without interfering with the garbage collection mechanism, you can use WeakMap.

You should know that the reason why Vue 3 uses WeakMap as a buffer is to correctly garbage collect the data that is no longer used.

What is a weak reference

Wikipedia gives the answer to "weak references":

In computer programming, weak reference, as opposed to strong reference, refers to a reference that cannot ensure that the object it refers to will not be recycled by the garbage collector. If an object is only referenced by a weak reference, it is considered inaccessible (or weakly accessible) and may therefore be recycled at any time.

Why do weak references occur

So why do weak references occur? What other problems can weak references solve besides the above problems? To answer these questions, we first need to understand how V8 engine performs garbage collection.

For JSer, memory management is automatic and invisible. All this is due to the V8 engine silently helping us find and clean up the memory we don't need.

So what happens when we no longer need something, and how does the V8 engine find and clean it up?

Nowadays, there are two garbage collection methods commonly used by major browsers. One is "reference counting" and the other is "tag clearing". Let's take a look:

Mark clear

Mark and sweep is called Mark and sweep. It determines whether an object is alive based on accessibility. It periodically performs the following garbage collection steps:

  1. The garbage collector finds all the roots and marks (remembers) them.
  2. It then traverses and marks all references from them. All traversed objects will be remembered to avoid traversing the same object again in the future.
  3. ... do this until all reachable (from the root) references are accessed.
  4. Objects that are not marked are deleted.

We can also imagine this process as a huge paint bucket overflowing from the root, flowing through all references and marking all reachable objects, and then removing unmarked objects.

Reference count

The most basic form of reference counting is to associate each managed object with a reference counter, which records the number of times the object is currently referenced. Whenever a new reference is created to point to the object, the counter will increase by 1, and the counter will decrease by 1 whenever the reference to the object fails. When the value of the counter drops to 0, the object is considered dead.

difference

The biggest difference between reference counting and mark clearing based on accessibility is that the former only needs local information, while the latter needs global information.

In the reference count, each counter only records the local information of its corresponding object - the number of references, without (and does not need) the life and death information of a global object graph.

Because only local information is maintained, dead objects can be identified and released without scanning the global object graph. However, due to the lack of global object graph information, the circular reference cannot be handled.

Therefore, more advanced reference counting implementations introduce the concept of weak references to break some known circular references.

WeakMap application

Store DOM nodes

A typical case of a WeakMap application is to use the DOM node as the key name. Here is an example.

const myWeakmap = newWeakMap();
myWeakmap.set(
  document.getElementById('logo'),
  { timesClicked: 0 },
);
document.getElementById('logo').addEventListener('click', () => {
  const logoData = myWeakmap.get(document.getElementById('logo'));
  logoData.timesClicked++;
}, false);

In the above code, document Getelementbyid ('logo ') is a DOM node that updates the status whenever a click event occurs. We put this state as a value in the WeakMap, and the corresponding key is the node object. Once the DOM node is deleted, the state will disappear automatically, and there is no risk of memory leakage.

Data cache

The answer lies in the puzzle. The question we raised at the beginning of the article is the answer here. When Vue 3 implements the principle of responsiveness, it uses WeakMap as the "cache" of responsive objects.

On this point, the usage is also very simple. When we need to associate objects and data, such as storing some properties without modifying the original object or storing some calculated values according to the object, and do not want to manually manage these memory problems, we can use WeakMap.

Private properties in deployment classes

Another use of WeakMap is to deploy private properties in classes.

It is worth mentioning that the principle of private property implemented in TypeScript is to use WeakMap.

Private attributes should not be accessed by the outside world or shared by multiple instances. It is not reliable to use underscores to mark private attributes and methods in JavaScript.

The following three methods are used:

  • Version 1: closure
const testFn = (function () {
  let data;

  class Test {
    constructor(val) {
      data = val
    }
    getData() {
      return data;
    }
  }
  return Test;
})();

let test1 = new testFn(3);
let test2 = new testFn(4);
console.log(test1.getData()); // 4
console.log(test2.getData()); // 4

You can see that the final output is 4, and multiple instances share private properties, so version 1 does not comply.

  • Version 2: Symbol
const testFn = (function () {
  let data = Symbol('data')

  class Test {
    constructor(val) {
      this[data] = val
    }
    getData() {
      return this[data]
    }
  }
  return Test;
})();

let test1 = new testFn(3);
let test2 = new testFn(4);
console.log(test1.getData()); // 3
console.log(test2.getData()); // 4

console.log(test1[Object.getOwnPropertySymbols(test1)[0]]); // 3
console.log(test2[Object.getOwnPropertySymbols(test2)[0]]); // 4

Using Symbol, we realized and correctly output 3 and 4, but we found that we can get private properties directly outside without using getData method, so this method does not meet our requirements.

  • Version 3: WeakMap
const testFn = (function () {
  let data = new WeakMap()

  class Test {
    constructor(val) {
      data.set(this, val)
    }
    getData() {
      return data.get(this)
    }
  }
  return Test;
})();

let test1 = new testFn(3);
let test2 = new testFn(4);
console.log(test1.getData()); // 3
console.log(test2.getData()); // 4

As above, perfect solution~~

reference resources

More attention, please pay attention to our official account "100 bottles of technology". There are occasional benefits!

Topics: Vue.js