preface
Vue3 beta version has been released for nearly two months, I believe that more or less everyone has learned some new features of VUE3, and some people are not moving to learn. In my opinion, technology must change constantly, new technology can improve productivity, and outdated technology must be eliminated. Five years ago you could get a decent job with a shuttle. Now few companies would ask for it. Just two days ago, The University of Utah also published an article about the production process of VUE3. If you are interested, you can click the link to view it. The article is in English, and those who are not good at English can read it with the help of the translation plug-in. Well, without further ado, the topic of this article is the responsiveness of handwriting VU3.
Example of vue3 code
Before writing the code, let’s take a look at how to use vue3. We can go to github.com/vuejs/vue-n… After you clone the code and use NPM install && NPM run dev, it will generate a package -> vue -> dist -> vue.global.js file so that we can use vue3. Create a new index.html file in the vue folder.
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Vue3 sample</title>
</head>
<body>
<div id="app"></div>
<button id="btn">button</button>
<script src="./dist/vue.global.js"></script>
<script>
const { reactive, computed, watchEffect } = Vue;
const app = document.querySelector('#app');
const btn = document.querySelector('#btn');
const year = new Date().getFullYear();
let person = reactive({
name: 'Fireworks Render farewell'.age: 23
});
let birthYear = computed((a)= > year - person.age);
watchEffect((a)= > {
app.innerHTML = I called ` < div >${person.name}This year,${person.age}Age, birth year is${birthYear.value}</div>`;
});
btn.addEventListener('click', () => {
person.age += 1;
});
</script>
</body>
</html>
Copy the code
As you can see, clicking the button once at a time triggers Person. age += 1; Then watchEffect automatically executes and the calculated properties are updated accordingly, and now our goal is clear: Reactive, watchEffect, computed.
Reactive method
We know that VUe3 is responsive based on proxy. If you are not familiar with proxy, please refer to Teacher Ruan Yifeng’s ES6 tutorial: es6.ruanyifeng.com/#docs/proxy Reflect is also a new API provided by ES6. For details, please refer to the ES6 tutorial by Yifeng Ruan: es6.ruanyifeng.com/#docs/refle… In short, it provides a new API for manipulating objects, putting methods that belong to the language inside the Object into Reflect, and changing the old Object methods to return false. Let’s look at the code that proxies the get, set, and del operations on the object.
function isObject(target) {
return typeof target === 'object'&& target ! = =null;
}
function reactive() {
// Determine whether the object is a proxy
if(! isObject(target)) {return target;
}
const baseHandler = {
set(target, key, value, receiver) { // receiver: It always points to the object where the original read operation is located, usually the Proxy instance
trigger(); // Triggers a view update
return Reflect.set(target, key, value, receiver);
},
get(target, key, receiver) {
return Reflect.get(target, key, value, receiver);
},
del(target, key) {
return Reflect.deleteProperty(target, key); }};let observed = new Proxy(target, baseHandler);
return observed;
}
Copy the code
Add update restrictions
This code looks fine, but when you add or remove elements from an array, you can listen for changes in the array itself, as well as changes in the length property, as shown in the following figure:
So we should only trigger an update when we add a new property, we added hasOwnProperty to compare the old value with the new value, and only update the view when we change the property of our own object or change the property of our own object and the value is different.
set(target, key, value, receiver) {
const oldValue = target[key];
if(! target.hasOwnProperty(key) || oldValue ! == value) {// Add attribute or set attribute old value does not equal new value
trigger(target, key); // Trigger the view update function
}
return Reflect.set(target, key, value, receiver);
}
Copy the code
Deep-level object listening
If the value of the object’s property is still the object, it has not been proxied. When we manipulate the object, set will not be triggered and the view will not be updated. The diagram below:
So how do we go about deep agency?
Let’s take a look at the operation person.hair.push(4). When we fetch Person.hair, we will call the Person get method to get the value of the property hair, so we can determine whether it is an object after it gets the value, and then conduct in-depth monitoring.
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
return isObject(res) ? reactive(res) : res;
},
Copy the code
Cached proxied objects
Reactive resets the proxy when the propped object is executed. This should be avoided by caching the propped object using a HashMap, so that when the propped object is re-propped, the result can be returned.
- Perform multiple proxy examples
let obj = {
name: 'Fireworks Render farewell'.age: 23.hair: [1.2.3]}let person = reactive(obj);
person = reactive(obj);
person = reactive(obj);
Copy the code
- define
hashmap
Caching proxy object
We use WeakMap to cache the proxy object, which is a weak reference object and does not cause a memory leak. Es6.ruanyifeng.com/#docs/set-m…
const toProxy = new WeakMap(a);// The object after the proxy
const toRaw = new WeakMap(a);// The object before the proxy
function reactive(target) {
// Determine whether the object is a proxy
if(! isObject(target)) {return target;
}
let proxy = toProxy.get(target); // The current object is in the proxy table
if (proxy) {
return proxy;
}
if (toRaw.has(target)) { // The current object is proxied
return target;
}
let observed = new Proxy(target, baseHandler);
toProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
let obj = {
name: 'Fireworks Render farewell'.age: 23.hair: [1.2.3]}let person = reactive(obj);
person = reactive(obj); // The proxy returns the data retrieved from the cache
Copy the code
So the Reactive method is basically done.
Collect dependencies and update automatically
Let’s take a look at how we rendered the DOM earlier.
watchEffect((a)= > {
app.innerHTML = I called ` < div >${person.name}This year,${person.age}Age, birth year is${birthYear.value}</div>`;
});
Copy the code
After the watchEffect function is initialized once by default, the DOM data is rendered, and the dependent data changes are automatically executed again, which automatically updates the content of the DOM. This is called collecting dependencies, reactive updates.
So where do we do dependency collection, and when do we notify dependency updates?
- When we use data for presentation, it triggers the creation
proxy
The object’sget
Method, at which point we can collect dependencies. - It also triggers ours when the data changes
set
Method, we are inset
Notification dependency update in. This is actually a design pattern called publish and subscribe.
We collect dependencies in GET:
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key); // Collect dependencies and effect on the stack if the key on the target changes
return isObject(res) ? reactive(res) : res;
}
Copy the code
Notifying dependency updates in a set:
set(target, key, value, receiver) {
if (target.hasOwnProperty(key)) {
trigger(target, key); // Trigger the update
}
return Reflect.set(target, key, value, receiver);
}
Copy the code
You can see that we implemented a track method in GET to collect dependencies, and trigger triggered updates in set. Now that we know how it works, let’s see how to implement the watchEffect method.
WatchEffect method
The function we pass into the watchEffect method is the dependency we want to collect, and we store the collected dependency on a stack. A stack is an advanced data structure. Let’s look at the following code:
let effectStack = []; // Storage depends on data effect
function watchEffect(fn, options = {}) {
// Create a responsive effect function, push an effect function to the effectsStack, execute fn
const effect = createReactiveEffect(fn, options);
return effect;
}
function createReactiveEffect(fn) {
const effect = function() {
if(! effectsStack.includes(effect)) {Effecy is already in the stack to prevent repeated addition
try {
effectsStack.push(effect); // Push the current effect onto the stack
return fn(); / / execution fn
} finally {
effectsStack.pop(effect); // Avoid fn execution error, execute in finally, push current effect off stack
}
}
}
effect(); // Execute once by default
}
Copy the code
Associate effects with corresponding object properties
Above, we only collected FN and stored it in effectsStack, but we have not associated FN with the corresponding object properties. Next, we need to implement the track method to associate effect with the corresponding properties.
let targetsMap = new WeakMap(a);function track(target, key) { // If the key in taeget changes, the effect method on the stack is executed
const effect = effectsStack[effectsStack.length - 1];
// The latest effect is created before the association is created
if (effect) {
let depsMap = targetsMap.get(target);
if(! depsMap) {// Set the matching value for the first rendering
targetsMap.set(target, depsMap = new Map());
}
let deps = depsMap.get(key);
if(! deps) {// Set the matching value for the first rendering
depsMap.set(key, deps = new Set());
}
if(! deps.has(effect)) { deps.add(effect);// Add effect to the depsMap of the current targetsMap}}}function trigger(target, key, type) {
// Trigger the update to find dependency effects
let depsMap = targetsMap.get(target);
if (depsMap) {
let deps = depsMap.get(key);
if (deps) {
deps.forEach(effect= >{ effect(); }); }}}Copy the code
The data structure of targetsMap is relatively complex. It is a WeakMap object. The key of targetsMap is our target object. The value of the key corresponding to the Map object is a Set data structure that stores the effect dependencies corresponding to the current target.key. It might be clearer to look at the following code:
let person = reactive({
name: 'Fireworks Render farewell'}); targetsMap = {person: {
'name': [effect]
}
}
/ / {
// target: {
// key: [dep1, dep2]
/ /}
// }
Copy the code
Execute the process
- Collection process: Execution
watchEffect
Methods,fn
That iseffect
A push toeffectStack
Stack, executefn
If thefn
Useful toreactive
Object that triggers the proxy objectget
Method, while we areget
Methodtrack
Method to collect dependencies,track
The method starts witheffectStack
Extract the last one fromeffect
That’s what we just pushed onto the stackeffect
And then determine whether it exists, and if it does, we start fromtargetMap
Extract the correspondingtarget
thedepsMap
If thedepsMap
Does not exist, we manually place the currenttarget
As akey
.depsMap = new Map()
Set as a value totargetMap
And then we go fromdepsMap
To retrieve the current proxy objectkey
Corresponding dependencydeps
If it does not exist, store a new oneSet
Go in and put the correspondingeffect
Added to thedeps
In the. - Update process: Modify the agent after the object, triggered
set
Method, executiontrigger
Method passed intarget
intargetsMap
Found in thedepsMap
Through thekey
indepsMap
To find the correspondingdeps
, loop execution inside the savedeffect
.
The computed method
Before we write about computed, let’s review how it’s used:
let person = reactive({
name: 'Fireworks Render farewell'.age: 23
});
let birthYear = computed((a)= > 2020 - person.age);
person.age += 1;
Copy the code
You can see that computed takes a function and then returns a processed value, and computed recalculates once the dependent data has been modified.
Actually computed it is also a watchEffect function, but it is a special one. In this case, we pass in two parameters, one for computed FN and the other for the watchEffect parameter {lazy: True, computed: true}, we didn’t handle these parameters when we wrote watchEffect earlier, so now we have to.
function computed(fn) {
let computedValue;
const computedEffect = watchEffect(fn, {
lazy: true.computed: true
});
return {
effect: computedEffect,
get value() {
computedValue = computedEffect();
trackChildRun(computedEffect);
returncomputedValue; }}}function trackChildRun(childEffect) {
if(! effectsStack.length)return;
const effect = effectsStack[effectsStack.length - 1];
for (let i = 0; i < childEffect.deps.length; i++) {
const dep = childEffect.deps[i];
if(! dep.has(effect)) { dep.add(effect); effect.deps.push(dep); }}}Copy the code
Modify the watchEffect method to accept an opstion parameter and add a lazy attribute judgment that does not execute the function passed in immediately when lazy is true, because computed methods do not execute immediately.
function watchEffect(fn, options = {}) {
// Create a responsive effect function, push an effect function to the effectsStack, execute fn
const effect = createReactiveEffect(fn, options);
// start: added code
if(! options.lazy) { effect() }// end: added code
return effect;
}
Copy the code
Modify the createReactiveEffect method to add the options parameter, and add dePS to the current effect to collect the dependencies of the computed property, which in our example is the age property, and to save computed and lazy properties.
function createReactiveEffect(fn, options) {
const effect = function() {
// Determine if the effect is already in the stack to avoid repeating recursive loops, such as modifying dependent data in the listener function
if(! effectsStack.includes(effect)) {try {
effectsStack.push(effect); // Push the current effect onto the stack
return fn(); / / execution fn
} finally {
effectsStack.pop(effect); // Avoid fn execution error, execute in finally, push current effect off stack}}}// start: added code
effect.deps = [];
effect.computed = options.computed;
effect.lazy = options.lazy;
// end: added code
return effect;
}
Copy the code
Add the collected set of property dependencies to effect’s DEPS in the track method.
function track(target, key) { // If the key in taeget changes, the effect method on the stack is executed
const effect = effectsStack[effectsStack.length - 1];
// The latest effect is created before the association is created
if (effect) {
let depsMap = targetsMap.get(target);
if(! depsMap) {// Set the matching value for the first rendering
targetsMap.set(target, depsMap = new Map());
}
let deps = depsMap.get(key);
if(! deps) {// Set the matching value for the first rendering
depsMap.set(key, deps = new Set());
}
if(! deps.has(effect)) { deps.add(effect);// start: added code
effect.deps.push(deps); // Mount the dependency set of properties to effect
// end: added code}}}Copy the code
The trigger method uses the computed property previously saved in Effect to distinguish between a computed function and a normal function, and then saves them separately, and then executes the normal effect function first, then executes the computed function.
function trigger(target, key, type) {
// Trigger the update to find dependency effects
let depsMap = targetsMap.get(target);
if (depsMap) {
let effects = new Set(a);let computedRunners = new Set(a);let deps = depsMap.get(key);
if (deps) {
deps.forEach(effect= > {
if (effect.computed) {
computedRunners.add(effect);
} else{ effects.add(effect); }}); }if ((type === 'ADD' || type === 'DELETE') && Array.isArray(target)) {
const iterationKey = 'length';
const deps = depsMap.get(iterationKey);
if (deps) {
deps.forEach(effect= > {
effects.add(effect);
});
}
}
computedRunners.forEach(computed= > computed());
effects.forEach(effect= >effect()); }}Copy the code
conclusioncomputed
Execute the process
Let’s analyze the execution flow based on the following code.
const value = reactive({ count: 0 });
const cValue = computed((a)= > value.count + 1);
let dummy;
watchEffect((a)= > {
dummy = cValue.value;
console.log(dummy)
});
value.count = 1;
Copy the code
Step 1: Turn the count object into a responsive object.
The second step: After executing computed, watchEffect is executed inside computed, and lazy and computed attributes are passed in. Since lazy is passed in as true, the generated effect is not executed immediately. For differentiation, this effect is collectively called computed effect. The fn passed in is called computed FN, that is, no data is added to the stack, and the cValue holds an object containing the computed effect and get methods.
Step 3: Execute the watchEffect method: This is the most critical step. Execute the watchEffect method, and since it has no lazy property, immediately execute the Effect method, add the current effect to the effectsStack, and execute fn.
Step 4: Perform fn, which gets the value of cValue and triggers a computed GET method, and perform effect saved in step 2.
Step 5: Compute effect, add compute Effect to effectsStack (effectsStack is [Normal Effect, compute effect]), and compute FN.
Step 5: Perform calculation of FN, which depends on the responsive object value. At this time, read the count attribute of value and trigger the GET method of value object, in which track method is executed to collect dependencies.
Step 6: Initialize targetsMap and depsMap. Then save the calculation effect to the dePS corresponding to count and dePS to the calculation effect dePS. Next step: In this way, a bidirectional collection relationship is formed. Counting effect saves all the dependencies of count, and count also stores the dependencies of counting effect. After performing the next step of track method, the obtained value of value.count is returned and stored in computedValue. And then we go ahead and execute.
Step 6: When we run trackChildRun and calculate fn, we push effect off the stack. At this point, the top of the stack is normal effect. First, we get the bottom element of the stack, which is the rest of the normal effect. When we executed track in the previous step, we saved the dependency set corresponding to the count attribute in the DEPS of calculating effect. At this time, there is only one element in the DEPS [calculating effect]. Now add the general effect to the DEP. So depsMap is {count: [calculate effect, normal effect]}.
Step 7: Execute value.count = 1, trigger the set method, execute trigger method, and obtain the DEPS corresponding to count, namely [calculation effect, general effect]. Then compute effect and normal effect are executed.
Thank you
Thank you for reading this article and giving it a thumbs up before you go. (^o^)/~