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