Reverse operation, I let vue/reactivity support non Proxy environment

Posted by jrinco11 on Tue, 14 Dec 2021 07:29:20 +0100

background

As we all know, vue3 rewrites the responsive code, uses Proxy to hijack data operations, and separates a separate library @ vue/reactivity, which is not limited to Vue. It can be used in any js code

However, because Proxy is used, Proxy cannot be compatible with polyfill, which makes it impossible to use in environments that do not support Proxy, which is also part of the reason why vue3 does not support ie11

Content of this article: the hijacking part of @ vue/reactivity is rewritten to be compatible with the environment that does not support Proxy

Some contents can be found in this article:

  • Responsive principle
  • @The difference between vue/reactivity and vue2 responsiveness
  • Problems and solutions in rewriting
  • code implementation
  • Application scenarios and limitations

Source address: reactivity It mainly refers to defobserver TS file

Responsive

Before we start, let's have a simple understanding of the response of @ vue/reactivity

The first is to hijack a data

When get ting data, collect dependencies and record the method in which you called it, assuming it was called by method effect1

When setting data in set, get the method recorded in get to trigger the effect1 function to achieve the purpose of monitoring

effect is a wrapper method that sets the execution stack to itself before and after the call to collect dependencies during function execution

difference

The biggest difference between vue3 and vue2 is the use of Proxy

Proxy can be better than object Defineproperty has more comprehensive proxy interception:

  • get/set hijacking of unknown property

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

    In Vue2, you must use the set method to assign values

  • When the subscript of an array element changes, you can directly use the subscript to operate the array and directly modify the array length

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

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

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

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

These are the functions brought by Proxy, as well as some new concepts or changes in usage

  • Independent subcontracting can not only be used in vue
  • Functional methods, such as reactive / effective / computed, are more flexible
  • The original data is isolated from the response data. The original data can also be obtained through toRaw. In vue2, hijacking is directly performed in the original data
  • More comprehensive functions: reactive/readonly/shallowReactive/shallowReadonly/ref/effectScope, read-only, shallow, basic type hijacking, scope

So if we want to use object Defineproperty, can you complete the above functions? What problems will you encounter?

Problems and Solutions

Let's ignore Proxy and object Functional differences in defineproperty

Because we want to write @ vue/reactivity instead of vue2, we need to solve some new conceptual differences, such as the isolation of raw data and response data

@vue/reactivity: there is a weak type reference (WeakMap) between the original data and the response data. When you get an object type data, you still get the original data. Just judge that if there is a corresponding response data, you can get it. If there is no response data, you can generate a corresponding response data, save and obtain it

In this way, it is controlled at the get level. What is obtained through the response data is always the response, and what is obtained through the original object is always the original data (unless a response is directly assigned to an attribute in the original object)

Then the source code of vue2 can't be used directly

According to the logic mentioned above, write a minimum implementation code to verify the logic:

const proxyMap = new WeakMap();
function reactive(target) {
  // If the corresponding response object already exists for the current original object, 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;
    },
  });
}

<!-- This example tries in codepen -->

Try in the online example

In this way, we can isolate the original data from the response data, and no matter how deep the data level is

Now we still face a problem, what about arrays?

The array is obtained by subscript, which is different from the attribute of the object. How to isolate it

That is to hijack array subscripts in the same way as objects

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

const proxy = [];

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

Try in the online example

Is to add an isArray judgment in the above code

This also determines that we need to maintain the array mapping all the time. In fact, it is also simple. When the length of the array push/unshift/pop/shift/splice changes, we need to re-establish the mapping for the newly added or deleted subscripts

const instrumentations = {}; // Method of storing rewrites

["push", "pop", "shift", "unshift", "splice"].forEach((key) => {
  instrumentations[key] = function (...args) {
    const oldLen = target.length;
    const res = target[key](...args);
    const newLen = target.length;
    // Added / deleted element
    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;
  };
});

The old mapping does not need to be changed, but only maps the new subscript and deletes the deleted subscript

The disadvantage of this is that if you override the array method and set some properties in it, it can not become a response

For example:

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);

lastPushed here cannot be monitored because this is the original object
One solution is to record the response data before push ing, judge and trigger it when the set modifies the metadata, and consider whether to use it in this way

// When hijacking the push method
enableTriggering()
const res = target[key](...args);
resetTriggering()

// At the time of the statement
{
  push(item: T) {
    set(this, 'lastPushed', item)
    return super.push(item);
  }
}

realization

Call track in get to collect dependencies

Trigger trigger during set or push operation

Anyone who has used vue2 should know the defect of defineProperty and cannot listen to the deletion of properties and the setting of unknown properties. Therefore, there is a difference between existing properties and unknown properties

In fact, the above example can be slightly improved to support the hijacking of existing properties

const obj = reactive({
  name: 1,
});

effect(() => {
  console.log(obj.name);
});

obj.name = 2;

Next, we will fix the difference between defineProperty and Proxy in implementation

The following differences:

  • Array subscript change
  • Hijacking of unknown elements
  • hash operation of element
  • delete operation of element
  • ownKeys operation for element

Subscript change of array

The array is a little special. When we call unshift to insert elements at the beginning of the array, we need a trigger to notify the array of every change. This is fully supported in the Proxy. There is no need to write redundant code, but using defineProperty requires us to calculate which subscript changes are compatible

During split, shift, pop, push and other operations, you also need to calculate which subscripts have changed, and then notify them

There is another disadvantage: changing the length of the array will not be monitored because the length attribute cannot be reset

In the future, we may consider replacing arrays with objects, but we can't use array Isarray to judge:

const target = [1, 2];

const proxy = Object.create(target);

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

Other operations

The remaining hard injuries of defineProperty can only be supported by adding additional methods

So we added set,get,has,del,ownKeys method

(you can click the method to view the source code implementation)

use
const obj = reactive({});

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

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

effect(() => {
  for (const k in ownKeys(obj)) {
    // Traverse unknown properties
    console.log("key: ", k);
  }
});

set(obj, "name", 11111); // Set unknown property

del(obj, "name"); // Delete attribute

obj is originally an empty object. I don't know what attributes will be added in the future

For example, set and del exist in vue2, which are compatible with the defects of defineProperty

set overrides the setting of an unknown property
get replaces the acquisition of unknown properties
Delete replaces delete obj Name delete syntax
has replaces' name 'in obj to judge whether it exists
ownKeys replaces traversal operations such as for(const k in obj) {}. When traversing objects / arrays, ownKeys should be used to wrap them

Application scenarios and limitations

At present, this function is mainly positioned as non vue environment and does not support Proxy

Other syntax uses polyfill compatibility

Because the old version of vue2 syntax does not need to be changed, if you want to use the new syntax in vue2, you can also use the composition API for compatibility

The reason for doing this is that our application (applet) actually does not support Proxy in some users' environments, but still wants to use the syntax @ vue/reactivity

As for the examples used above, we should also know that the restrictions are very large and the cost of flexibility is very high

If you want to be flexible, you must wrap it with methods. If it is not flexible, the usage is not much different from vue2. Define all properties during initialization

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

This method brings a kind of mental loss. When using and setting, we should consider whether this attribute is an unknown attribute and whether it should be wrapped with methods

Brutally wrap all settings with methods, so the code won't look good

Moreover, according to the barrel effect, once the packaging method is used, it seems unnecessary to automatically switch to Proxy hijacking in the high version

Another solution is to process it at compile time, put get method on all acquisition and set method on all setting syntax, but the cost is undoubtedly very large, and some js syntax is too flexible to support

Topics: Javascript Vue