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/reactivity
和vue2
Responsive distinction- In the use of
Object.defineProperty
Problems 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 in
vue
In the use of - The functional approach
reactive
/effect
/computed
And other methods, more flexible - Raw data is isolated from response data and can also pass
toRaw
To get the raw data invue2
Is directly in the raw data hijacking operations - 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 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 the
hash
operation - Elements of the
delete
operation - Elements of the
ownKeys
operation
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