background

We all know that Vue3 rewrites reactive code to use proxies to hijack data operations and separate out @vue/ reActivity libraries, not limited to vue being available in any JS code

However, because of the use of Proxy, Proxy cannot be compatible with Polyfill, which results in that it cannot be used in environments that do not support Proxy. This is also part of the reason why VUe3 does not support IE11

Content of this article: Rewrote the @vue/reactivity hijacking part to make it compatible with environments that don’t support Proxy

Some contents can be obtained through this article:

  • Response principle
  • @vue/reactivityvue2Responsive distinction
  • In the use ofObject.definePropertyProblems and solutions encountered in rewriting
  • Code implementation
  • Application scenarios and restrictions

Reactivity is primarily a defobserver.ts file

responsive

Before we begin, let’s take a quick look at the responsiveness of @vue/reactivity

The first was the hijacking of a piece of data

Collect dependencies when GET gets the data, and record which method it was called from, assuming it was called by method effect1

When a set sets the data, the get method is used to trigger the effect1 function to listen

Effect, on the other hand, is a wrapper method that sets the execution stack to itself before and after the call to collect dependencies during the execution of the function

The difference between

The biggest difference between VUE3 and VUE2 is that it uses a Proxy

Proxy can have more comprehensive Proxy interception than Object.defineProperty:

(While Proxy brings more comprehensive functionality, it also brings performance; Proxy is actually much slower than Object.defineProperty.)

Reflections on ES6 Proxy performance

  • Get /set hijacking of unknown attributes

    const obj = reactive({});
    effect(() = > {
      console.log(obj.name);
    });
    obj.name = 111;
    Copy the code

    This point in Vue2 must be assigned using the set method

  • Array element subscript changes, you can directly use the subscript to operate on the array, directly modify the array length

    const arr = reactive([]);
    effect(() = > {
      console.log(arr[0]);
    });
    arr[0] = 111;
    Copy the code
  • Support for delete obj[key] attribute deletion

    const obj = reactive({
      name: 111}); effect(() = > {
      console.log(obj.name);
    });
    delete obj.name;
    Copy the code
  • Whether there is support for has for the key in obj attribute

    const obj = reactive({});
    effect(() = > {
      console.log("name" in obj);
    });
    obj.name = 111;
    Copy the code
  • Support for for(let key in obj){} properties to be traversed by ownKeys

    const obj = reactive({});
    effect(() = > {
      for (const key in obj) {
        console.log(key); }}); obj.name =111;
    Copy the code
  • Support for Map, Set, WeakMap and WeakSet

These are the capabilities that Proxy brings, as well as some new concepts or changes in usage

  • Independent subcontracting, not only can invueIn the use of
  • The functional approachreactive/effect/computedAnd other methods, more flexible
  • Raw data is isolated from response data and can also passtoRawTo get the raw data invue2Is directly in the raw data hijacking operations
  • More comprehensive functionsreactive/readonly/shallowReactive/shallowReadonly/ref/effectScope, read-only, shallow, basic type of hijacking, scope

So if we want to use Object.defineProperty, can we do this? What are the problems?

Problems and Solutions

Let’s ignore the differences between Proxy and Object.defineProperty functionality for now

Since we’re writing @vue/reactivity rather than vue2 based, we need to address some new conceptual differences, such as raw data and response data isolation

@vue/reactivity: There is a WeakMap between the original data and the response data. When you get an object type data, you still take the original data. You just judge if there is a corresponding response data to get. If not, generate a corresponding reactive data save and fetch

This controls the get level so that responder data is always responder and original data is always raw (unless a responder is directly assigned to a property in the original object).

Then the source code of vue2 cannot be used directly

Write a minimum-implementation code to verify the logic as described above:

const proxyMap = new WeakMap(a);function reactive(target) {
  // If the original object already exists, the cache is returned
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }

  const proxy = {};

  for (const key in target) {
    proxyKey(proxy, target, key);
  }

  proxyMap.set(target, proxy);

  return proxy;
}

function proxyKey(proxy, target, key) {
  Object.defineProperty(proxy, key, {
    enumerable: true.configurable: true.get: function () {
      console.log("get", key);
      const res = target[key];
      if (typeof res === "object") {
        return reactive(res);
      }
      return res;
    },
    set: function (value) {
      console.log("set", key, value); target[key] = value; }}); }Copy the code

Try this in the online sample

This allows us to isolate the raw data from the response data, regardless of the depth of the data hierarchy

Now we’re left with the question, what about arrays?

Arrays are retrieved by subscripts, not quite the same as objects’ properties. How do you isolate this

That’s how you hijack array subscripts in the same way that objects do

const target = [{ deep: { name: 1}}];const proxy = [];

for (let key in target) {
  proxyKey(proxy, target, key);
}
Copy the code

Try this in the online sample

I’m just adding an isArray judgment to the code above

And it also determines the behind us to always maintain this array mapping, but also simple, push in the array/unshift/pop/shift/splice length change to add or delete the subscript to establish a mapping

const instrumentations = {}; // store the override method

["push"."pop"."shift"."unshift"."splice"].forEach((key) = > {
  instrumentations[key] = function (. args) {
    const oldLen = target.length;
    constres = target[key](... args);const newLen = target.length;
    // Added/deleted elements
    if(oldLen ! == newLen) {if (oldLen < newLen) {
        for (let i = oldLen; i < newLen; i++) {
          proxyKey(this, target, i); }}else if (oldLen > newLen) {
        for (let i = newLen; i < oldLen; i++) {
          delete this[i]; }}this.length = newLen;
    }

    return res;
  };
});
Copy the code

There is no need to change the old mapping, just map the new subscript and delete the deleted subscript

The downside of this is that if you override the array method and put some properties in it, it’s not going to be reactive

Such as:

class SubArray extends Array {
  lastPushed: undefined;

  push(item: T) {
    this.lastPushed = item;
    return super.push(item); }}const subArray = new SubArray(4.5.6);
const observed = reactive(subArray);
observed.push(7);
Copy the code

The lastPushed here cannot be monitored, because this is the original object. There is a solution to record the response data before push and judge and trigger when set modifies the metadata. We are still considering whether to use this

// When the push method is hijacked
enableTriggering()
constres = target[key](... args); resetTriggering()// When declaring
{
  push(item: T) {
    set(this.'lastPushed', item)
    return super.push(item); }}Copy the code

implementation

Call track in get hijacking to collect dependencies

Trigger is triggered during an operation such as a set or push

Anyone who has used VUe2 should be aware of the defect in defineProperty. You can’t listen for attribute deletion and setting of unknown attributes, so there is a difference between existing and unknown attributes

In fact, the above example could have been modified slightly to support the hijacking of existing attributes

const obj = reactive({
  name: 1}); effect(() = > {
  console.log(obj.name);
});

obj.name = 2;
Copy the code

The next implementation is to fix the defineProperty and Proxy differences

Here are some differences:

  • Array index changes
  • The hijacking of the unknown
  • Elements of thehashoperation
  • Elements of thedeleteoperation
  • Elements of theownKeysoperation

Array subscript changes

Arrays are a bit special because when we call unshift to insert an element at the beginning of the array, we need trigger to notify the array of each change. This is fully supported in Proxy and requires no extra code, but using defineProperty requires us to be compatible to calculate any subscript changes

The same goes for splice, shift, pop, push, etc., which subscripts change and then notify them

There is another disadvantage: array changes to length are not listened for, because the length attribute cannot be re-set

In the future, we might consider using objects instead of arrays, but we won’t be able to use array. isArray:

const target = [1.2];

const proxy = Object.create(target);

for (const k in target) {
  proxyKey(proxy, target, k);
}
proxyKey(proxy, target, "length");
Copy the code

Other operating

The rest are defineProperty bugs that we can only support by adding additional methods

So we added set, get, has, del, and ownKeys methods

(Click the method to view the source code implementation)

use
const obj = reactive({});

effect(() = > {
  console.log(has(obj, "name")); // Determine unknown attributes
});

effect(() = > {
  console.log(get(obj, "name")); // Get an unknown attribute
});

effect(() = > {
  for (const k in ownKeys(obj)) {
    // Iterate over unknown attributes
    console.log("The key.", k); }}); set(obj,"name".11111); // Set unknown properties

del(obj, "name"); // Delete attributes
Copy the code

Obj is an empty object. What attributes will be added in the future

Like set and del are bugs in VUe2 that are compatible with defineProperty

Set instead of get del instead of delete obj.name Syntax has instead of ‘name’ in obj check whether ownKeys exist instead of for(const k in obj) {} and other traversal operations, when the object/array will be traversed with ownKeys wrapped

Application scenarios and restrictions

At present, this feature is mainly positioned as a non-VUE environment and does not support Proxy

Other syntaxes are compatible with polyfill

Since the old vue2 syntax does not need to be changed, if you want to use the new syntax in VUe2, you can also use composition-API to make it compatible

Why do we need to do this? There are still some users in our application (applet) whose environment does not support Proxy, but still want to use @vue/reactivity syntax

As we can see from the examples above, the restrictions are high and the costs of flexibility are high

If you want to be flexible, you must use the method wrapper. If not, the usage is similar to that of vue2. All attributes are defined when they are initialized

const data = reactive({
  list: [].form: {
    title: "",}});Copy the code

There is a mental cost to using and setting a property that is unknown and wrapped in a method

Rough point to all Settings wrapped in method, such code can not see where

And with the barrel effect, once wrapping is used, it doesn’t seem necessary to automatically switch to Proxy hijacking in higher releases

The alternative is to handle it at compile time, with get on all fetch and set on all set syntax, but the cost of this is undoubtedly very high, and some JS syntax is too flexible to support