1
头图

background

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

But precisely because of the use Proxy , Proxy can not use polyfill be compatible, has led to an unsupported Proxy not use environment, which is vue3 does not support ie11 part of the reason

The content of this article: Rewrite the @vue/reactivity to be compatible with the environment that Proxy

Some content can be obtained through this article:

  • Responsive principle
  • The difference between @vue/reactivity and vue2
  • Problems encountered in rewriting with Object.defineProperty
  • Code
  • Application scenarios and limitations

Source code address: reactivity mainly defObserver.ts file

Responsive

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

The first is to hijack a piece of data

Collect dependencies when getting data at get records in which method is called, assuming it is called effect1

When the data is set set , the method gets get is used 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 the execution of the function

the difference

vue3 compared vue2 biggest difference is the use of Proxy

Proxy can have a more comprehensive proxy interception Object.defineProperty

( Proxy brings more comprehensive functions, it also brings performance. Proxy actually much slower Object.defineProperty

on ES6 Proxy performance

  • get/set hijacking of unknown attributes

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

This point must be assigned using the set Vue2

  • Changes in the index of the array elements, you can directly use the index to manipulate the array, directly modify the array length

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

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

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

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

These are Proxy , and there are some new concepts or changes in usage

  • Independent subcontracting, not only can be used in vue
  • Functional method reactive / effect / computed and other methods, more flexible
  • The original data is separated from the response data, and the original data toRaw vue2 , the hijacking operation is performed directly in the original data
  • More comprehensive functions reactive / readonly / shallowReactive / shallowReadonly / ref / effectScope , read-only, shallow, basic type of hijacking, scope

So if we want to use Object.defineProperty , can we complete the above functions? What problems will you encounter?

Problem and solution

Let’s ignore the functional differences between Proxy and Object.defineProperty

Because we want to write @vue/reactivity instead of vue2 , we must first solve some new concept differences, such as original data and response data isolation

@vue/reactivity practice, a weakly typed reference between the original data and the response data ( WeakMap ), in get a object type data when the original data or take, if only the corresponding response to determine what data is present on the pick, If it does not exist, generate a corresponding response data to save and obtain

In this way, at the get level control, what you get through the responsive data is always responsive, and what you get through the original object is always the original data (unless a responsive type is directly assigned to an attribute in the original object)

Then vue2 cannot be used directly

According to the logic mentioned above, write a minimally implemented code to verify the logic:

const proxyMap = new WeakMap();
function reactive(target) {
  // 如果当前原始对象已经存在对应响应对象,则返回缓存
  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 try -->

online example

In this way, we have achieved the isolation of the original data and the response data, and no matter how deep the data level is.

Now we still face a problem, what about arrays?

Arrays are obtained by subscripts, which are not the same as the properties of objects. How to isolate them?

That is to hijack the array subscript in the same way as the

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

const proxy = [];

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

online example

Just add a isArray judgment to the above code

And this also determines that we have to maintain this array mapping in the push . In fact, it is simple. When the length of the array 061c3e89f9ec17 / unshift / pop / shift / splice changes, the new or deleted subscripts are re-mapped.

const instrumentations = {}; // 存放重写的方法

["push", "pop", "shift", "unshift", "splice"].forEach((key) => {
  instrumentations[key] = function (...args) {
    const oldLen = target.length;
    const res = target[key](...args);
    const newLen = target.length;
    // 新增/删除了元素
    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, just map new subscripts and delete subscripts that have been deleted

The disadvantage of this is that if you rewrite the array method and set some properties in it, it will not become reactive

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

The lastPushed here cannot be monitored because this is the original object
One solution is to push the response data before set , judge and trigger when the metadata is modified in 061c3e89f9edb1, and still consider whether to use it in this way

// 在劫持push方法的时候
enableTriggering()
const res = target[key](...args);
resetTriggering()

// 声明的时候
{
  push(item: T) {
    set(this, 'lastPushed', item)
    return super.push(item);
  }
}

accomplish

Call track in the get hijacking to collect dependencies

In set or push time operations such as to trigger trigger

Used vue2 should know defineProperty defects, not listening to delete attributes and settings unknown properties, so there is a existing property and unknown attribute difference

In fact, the above example can be improved a little, and it already supports the existing attribute

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

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

obj.name = 2;

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

The following differences:

  • Array subscript changes
  • Hijacking of Unknown Elements
  • hash operation of the element
  • Element delete operation
  • ownKeys operation of the element

Subscript change of array

The array is a bit special. When we call unshift to insert an element at the beginning of the array, we need trigger to notify each item of the array that has changed. This Proxy . No need to write extra code, but the use of defineProperty requires us to be compatible to calculate Which subscript changes

In splice , shift , pop , push and other operations, it is also necessary to calculate which subscripts have changed and then notify them

There is another disadvantage: the array change length will not be monitored, because the length attribute cannot be restored

In the future, you may consider replacing the array with an object, but this way you 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 rest belong to the defineProperty , and we can only support it by adding additional methods.

So we added a the 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")); // 判断未知属性
});

effect(() => {
  console.log(get(obj, "name")); // 获取未知属性
});

effect(() => {
  for (const k in ownKeys(obj)) {
    // 遍历未知属性
    console.log("key:", k);
  }
});

set(obj, "name", 11111); // 设置未知属性

del(obj, "name"); // 删除属性

obj was originally an empty object, and I don’t know what attributes will be added in the future

Like set and del are in vue2 to be compatible with the defects of defineProperty

set replaces the setting of unknown attributes
get replaces the acquisition of unknown attributes
del replaces delete obj.name delete syntax
has replaces 'name' in obj determine whether it exists
ownKeys replaces for(const k in obj) {} and other traversal operations. When traversing objects/arrays, use ownKeys wrap

Application scenarios and limitations

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

Other syntax is compatible polyfill

Because the old version of vue2 syntax does not need to be changed, if you want to use the new syntax vue2 composition-api to be compatible

The reason for this is that our application (small program) actually still has some users whose environment does not support Proxy , but they still want to use the grammar @vue/reactivity

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

If you want to be flexible, you must use the method to wrap it. If it is not flexible, the usage vue2 all the properties when they are initialized first.

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

This method brings a kind of mental loss. When using and setting, you must consider whether this attribute is an unknown attribute and whether you should use methods to wrap it.

Roughly wrap all settings with methods, so the code is hard to see where it goes

And according to the barrel effect, once the packaging method is used, it seems unnecessary Proxy

Another solution is processed at compile time, to acquire all of the time put get way to put all the settings grammar set method, but this brings the cost is undoubtedly very large, and some flexibility over grammar js Can't support high


李十三
13.6k 声望17.8k 粉丝