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
andvue2
- 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
get/set
hijacking of unknown attributesconst 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 deletionconst 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 traversedownKeys
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 -->
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);
}
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 attributesget
replaces the acquisition of unknown attributesdel
replaces delete obj.name
delete syntaxhas
replaces 'name' in obj
determine whether it existsownKeys
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。