preface
In this article, we will write a written copy of the responsiveness principle in VUE-Next from scratch. For length and ease of understanding, we will only implement the core API and ignore some marginal function points
The apis that this article will implement include
- track
- trigger
- effect
- reactive
- watch
- computed
Project structures,
We built our project with vite, which is hot these days
The version demonstrated in this article
- node
v12.16.1
- npm
v6.14.5
- yarn
v1.22.4
Let’s start by downloading the template
yarn create vite-app vue-next-reactivity
Copy the code
After downloading the template, go to the directory
cd vue-next-reactivity
Copy the code
Then install the dependencies
yarn install
Copy the code
Then we just keep the main.js file in the SRC directory, clear the rest and create the reactivity folder we’ll use
The entire file directory is shown, and the NPM run dev project is launched by typing
Handwritten code
The nature of the responsive principle
Before we start writing, let’s think about what is the responsive principle?
Let’s explain this from the use of VUe-Next
The responses used in Vue-Next fall into three categories
- The template or render
When the variables used in the page change, the page is refreshed automatically
- computed
The calculated properties are automatically changed when the variables used in the calculated properties function are changed
- watch
When the listening value changes, the corresponding callback function is automatically triggered
These three points sum up the essence of the responsive principle
When a value changesautomatic
Trigger the corresponding callback function
The callbacks here are the page refresh function in template, the recalculation function in computed, and the Watch callback, which is a callback anyway
So we’re going to implement the responsive principle and now we’re going to break it down into two questions
- Listen for value changes
- Trigger the corresponding callback function
We solved these two problems, and we wrote the reactive principle
Listen for value changes
Javascript provides two apis for listening for value changes
One is object.defineproperety, used in vue2. X
const obj = {};
let aValue = 1;
Object.defineProperty(obj, 'a', {
enumerable: true.configurable: true,
get() {
console.log('I'm being read.');
return aValue;
},
set(value) {
console.log('I'm set.'); aValue = value; }}); obj.a;// I am read
obj.a = 2; // I am set
Copy the code
Another method is the proxy used in VUe-Next, which is also the method used in this handwriting
This approach addresses the four pain points of Object.defineproperety
- Cannot intercept additions and deletions of attributes on objects
- Cannot intercept calls on arrays
push
pop
shift
unshift
Methods that affect the current array - Intercepting excessive performance overhead for array indexes
- Unable to intercept
Set
Map
Equal set type
Of course, mainly the first two
In vue2. X, the array index must be changed by this.$set. Many students think object.defineproperety can’t intercept an array index
The above 4 points can be perfect proxy solution, now let’s start to write a proxy interception!
The proxy to intercept
We create two files in the reactivity directory we created earlier
Utils.js holds some common methods
React.js stores the proxy interception method
We’ll start by adding the methods we’ll use to determine whether the object is native to utils.js
reactivity/utils.js
// Get the original type
export function toPlain(value) {
return Object.prototype.toString.call(value).slice(8.- 1);
}
// Whether it is a native object
export function isPlainObject(value) {
return toPlain(value) === 'Object';
}
Copy the code
reactivity/reactive.js
import { isPlainObject } from './utils';
// Only arrays and objects can be observed
function canObserve(value) {
return Array.isArray(value) || isPlainObject(value);
}
// Intercept data
export function reactive(value) {
// The value that cannot be listened on is returned directly
if(! canObserve(value)) {return;
}
const observe = new Proxy(value, {
// Intercepts the read
get(target, key, receiver) {
console.log(`${key}'was read);
return Reflect.get(target, key, receiver);
},
// Intercept Settings
set(target, key, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver);
console.log(`${key}Is set to);
returnres; }});// Returns the proxy instance being observed
return observe;
}
Copy the code
reactivity/index.js
Methods derived
export * from './reactive';
Copy the code
main.js
import { reactive } from './reactivity';
const test = reactive({
a: 1});const testArr = reactive([1.2.3]);
/ / 1
test.a; // A is read
test.a = 2; // A is set
/ / 2
test.b; // b is read
/ / 3
testArr[0]; // 0 is read
/ / 4
testArr.pop(); // pop read length read 2 read length set
Copy the code
You can see that we have added a reactive method to proxy intercepts objects and arrays and returns the corresponding proxy instance
1, 2, and 3 are pretty straightforward, so let’s explain number 4
Calling the POP method first triggers a GET intercept, printing that the POP was read
The pop method is then called to read the length of the array and trigger the GET intercept, printing that length was read
The pop method returns the currently deleted value, reads the value with array index 2, triggers a GET intercept, and prints that 2 was read
After pop, the array length is changed, the set intercept is triggered, and the print length is set
You can also try other ways of changing arrays
It can be summed up in one sentence
Length is read and reset when it affects the length of the array itself, and the index of the changed value is read and reset (push unshift)
Add a callback function
We use proxy to implement value interception to solve the first problem we raised
But instead of triggering the callback after the value changes, let’s add the callback
reactivity/reactive.js
import { isPlainObject } from './utils'; / / this column only arrays and objects can be observed the function canObserve (value) {return Array. The isArray (value) | | isPlainObject (value); }+ // hypothetical callback function
+ function notice(key) {
+ console.log(' ${key} was changed and triggered the callback function ');
+}Export function reactive(value) {if (! canObserve(value)) { return; } const observe = new Proxy(value, {// Get (target, key, receiver) {- console.log(' ${key} was read ');return Reflect.get(target, key, receiver); }, // Set (target, key, newValue, receiver) {const res = reflect. set(target, key, newValue, receiver);- console.log(' ${key} is set ');
+ // triggers the hypothetical callback function
+ notice(key);return res; }}); // Proxy instance return observe; }Copy the code
We trigger our hypothetical callback in the most intuitive way possible in a set intercept whose value is changed
main.js
import { reactive } from './reactivity';
const test = reactive({
a: 1.b: 2}); test.a =2; // A is changed and the callback is triggered
test.b = 3; // b is changed and the callback is triggered
Copy the code
You can see that when the value changes, the corresponding log is output
But there are definitely problems with this example, and there are more than one, so let’s update it step by step
Collection of callback functions
The example above a and b are corresponding to a callback function notice, but the actual scenario, a and b may correspond to different callback functions respectively, if we just use a simple global variables to store the callback function, this is clearly not appropriate, if the latter will overwrite the former, then how can I let the callback function and corresponding between each value?
It is easy to think of the key-value object in JS. As the key value of the object, the attributes A and B can distinguish their respective value values
But collecting callback functions with objects is problematic
In the above column, we have a test object that has properties A and B. If we have another object test1 that also has properties A and B, then we are repeating it. This will trigger the repetition problem we mentioned earlier
Test and test1 are the same names in the same execution context, but they are in different execution contexts, which leads to the same duplication problem
To deal with this problem, use the pass-by-reference nature of JS objects
// 1.js
const obj = {
a: 1};// 2.js
const obj = {
a: 1};Copy the code
We define object obj with identical name attribute data structure in two folders, but we know that the two obJ are not equal because their memory points to different addresses
So if we could just use objects as keys, wouldn’t we be able to distinguish objects that look “identical”?
The answer is yes, but we have to change the data structure, because the key of an object in JS cannot be an object
Here, we need to use a new data structure Map and WeakMap of ES6
We illustrate the storage mode of this data structure with examples
Suppose we now have two objects with “the same” data structure, obj, each with its own attributes A and B, and changes to each attribute trigger different callback functions
// 1.js
const obj = {
a: 1.b: 2
};
// 2.js
const obj = {
a: 1.b: 2
};
Copy the code
Map and WeakMap are used for storage, as shown in the figure below
We define the global variable targetMap that stores the callback function as a WeakMap, and its key value is each object, which is two OBJ in this column. The value of targetMap is a Map. In this column, two OBJ have two attributes A and B respectively, and the key of Map is attribute A and B,Ma The value of p is the Set of callback functions corresponding to attributes A and b
Maybe you will have a question why targetMap uses WeakMap and each object’s properties store Map, this is because WeakMap can only use object as key,Map is an object or string, like the column of sub-attributes a and B can only use Map to store
Let’s use the actual API to further understand this storage structure
- computed
const c = computed((a)= > test.a)
Copy the code
Here we need to put the () => test.a callback in the test.a collection, as shown in the figure
- watch
watch((a)= > test.a, val => { console.log(val) })
Copy the code
Here we need to put the val => {console.log(val)} callback in the test.a collection, as shown in the figure
- template
createApp({
setup() {
return (a)= > h('div', test.a); }});Copy the code
Here we need to put the DOM refresh function in test.a, as shown in the figure
Now that we know how to store callback functions, how do we put callback functions into our defined storage structure
I’m going to do that again
watch((a)= > test.a, val => { console.log(val) })
Copy the code
In this column, we need to put the callback function val => {console.log(val)}) into the Set of test.a, so we need to get the object test and the property a of the current object, if only through () => Test. a, we can only get the value of test.a, we can not know the specific object and attribute
Test. a = test.a; test.a = test.a; test.a = test.a; test.a
The first argument to get is the object being read, and the second argument is the property being read
So the collection of callback functions is inproxy
theget
Processing in intercept
Now let’s implement this idea in code
First we create an effect.js file that holds the collection method and the trigger method of the callback function
reactivity/effect.js
// A collection of callback functions
const targetMap = new WeakMap(a);// Collect callback functions
export function track(target, key) {}// Trigger the callback function
export function trigger(target, key) {}Copy the code
Then rewrite the intercepting content in the proxy
reactivity/reactive.js
import { isPlainObject } from './utils';
+ import { track, trigger } from './effect';/ / this column only arrays and objects can be observed the function canObserve (value) {return Array. The isArray (value) | | isPlainObject (value); }- // The assumed callback function
- function notice(key) {
- console.log(' ${key} was changed and triggered the callback function ');
-}Export function reactive(value) {if (! canObserve(value)) { return; } const observe = new Proxy(value, {// Get (target, key, receiver) {+ // Collect callback functions
+ track(target, key);return Reflect.get(target, key, receiver); }, // Set (target, key, newValue, receiver) {const res = reflect. set(target, key, newValue, receiver);+ // Triggers the callback function
+ trigger(target, key);
- // Triggers the hypothetical callback function
- notice(key);return res; }}); // Proxy instance return observe; }Copy the code
What is not added here is the effect that makes it clear where the collection and trigger are
Now we add the track collection callback function and the trigger trigger callback function
reactivity/effect.js
// A collection of callback functions
const targetMap = new WeakMap(a);// Collect callback functions
export function track(target, key) {
// Get a map of each object by object
let depsMap = targetMap.get(target);
if(! depsMap) {// When objects are collected for the first time we need to add a map collection
targetMap.set(target, (depsMap = new Map()));
}
// Get the collection of callback functions for each property under the object
let dep = depsMap.get(key);
if(! dep) {// We need to add a set when the object attributes are first collected
depsMap.set(key, (dep = new Set()));
}
// Add the callback function here
dep.add((a)= > console.log('I'm a callback function'));
}
// Trigger the callback function
export function trigger(target, key) {
// Get the map of the object
const depsMap = targetMap.get(target);
if (depsMap) {
// Get the collection of callback functions corresponding to each attribute
const deps = depsMap.get(key);
if (deps) {
// Trigger the callback function
deps.forEach((v) = >v()); }}}Copy the code
And then run our demo
main.js
import { reactive } from './reactivity';
const test = reactive({
a: 1.b: 2}); test.b;// Read the collection callback function
setTimeout((a)= > {
test.a = 2; // There is no trigger because no callback is collected
test.b = 3; // I am a callback
}, 1000);
Copy the code
Let’s look at the targetMap structure at this point
Key value {a: 1,b: Log (‘ I am a callback function ‘);} 2} 2} 3} 3} 3} 3} 3} 3} 3} 3} 4} 3} 4} 4} 4} 4}
That’s what happens when you use a graphic structure
You might think that collecting callback functions and reading test.b is an anti-human operation, because we haven’t talked about the corresponding API yet. Normal reading operations don’t need to be called manually
watch
A big problem with the above example is that we don’t have a custom callback function. The callback function is written out directly in the code
Now we will implement the custom callback function through Watch
The WATCH API in Vue-Next is quite extensive, and we’ll implement some of these types, which is enough to understand the reactive principle
The demo we will implement is as follows
export function watch(fn, cb, options) {}
const test = reactive({
a: 1}); watch((a)= > test.a,
(val) => { console.log(val); });Copy the code
Watch accepts three arguments
The first argument is a function that expresses the value being listened on
The second argument is a function that expresses the callback to be triggered if the listening value is changed. The first argument is the changed value and the second argument is the value before the change
The third argument is an object that has only one deep property, the deep table
Now all we need to do is call the callback function (val) => {console.log(val); } into test.a’s Set
So before () => test.a reads test.a, we need to store the callback in a variable
When the track function is triggered by reading test.a, the variable can be obtained in the track function and stored in the Set of corresponding attributes
reactivity/effect.js
// Set of callback functions const targetMap = new WeakMap();+ // The currently active callback function
+ export let activeEffect;
+ // Sets the current callback function
+ export function setActiveEffect(effect) {
+ activeEffect = effect;
+}Export function track(target, key) {// If (! activeEffect) { return; } let depsMap = targetmap.get (target); if (! Targetmap.set (target, (depsMap = new map ())); } let dep = depmap.get (key); if (! Depmap.set (key, (dep = new set())); } // Add the callback function here-dep.add (() => console.log(' I am a callback function '));
+ dep.add(activeEffect);Export function trigger(target, key) {// omit}Copy the code
Since the watch method is not in the same file as the track and trigger methods, we use export to export the variable activeEffect and provide a method setActiveEffect to modify it
This is also a way to use public variables in different modules
Now let’s create watch.js and add the watch method
reactivity/watch.js
import { setActiveEffect } from './effect';
export function watch(fn, cb, options = {}) {
let oldValue;
// Store the callback function before executing fn to get oldValue
setActiveEffect((a)= > {
// Make sure the callback triggers a new value
let newValue = fn();
// Trigger the callback function
cb(newValue, oldValue);
// Assign the new value to the old value
oldValue = newValue;
});
// Read the value and collect the callback function
oldValue = fn();
// empty the callback function
setActiveEffect(' ');
}
Copy the code
Very simple a few lines of code, before the implementation of fn read value to set the callback function through setActiveEffect in order to read the track function can get the current callback function activeEffect, after reading the empty callback function, complete
We also need to export the watch method
reactivity/index.js
export * from './reactive';
+ export * from './watch';
Copy the code
main.js
import { reactive, watch } from './reactivity';
const test1 = reactive({
a: 1}); watch((a)= > test1.a,
(val) => {
console.log(val) / / 2;}); test1.a =2;
Copy the code
As you can see that the column executes normally and prints out 2, let’s look at the structure of the targetMap
{console.log(val) => {console.log(val); {console.log(val) => {console.log(val); }
The graphic structure of targetMap is as follows
computed
The other API additions of Watch will come later, and after feeling the responsive principle of thinking, we will strike while the iron is hot to implement computed functions
For the same computed API, there are several ways to write it in VUe-Next, and we will implement only the return value of the function
export function computed(fn) {}
const test = reactive({
a: 1});const w = computed((a)= > test.a + 1);
Copy the code
But if we just write computed incoming functions, vue-Next has little to do with the responsive principle
Because the API read value provided in vue-next is not directly read w but W. value
We create computed. Js to complement computed functions
reactivity/computed.js
export function computed(fn) {
return {
get value() {
returnfn(); }}; }Copy the code
As you can see, just a few lines of code, rerun the fn evaluation every time you read value
reactivity/index.js
Let’s export it again
export * from './reactive';
export * from './watch';
+ export * from './computed';
Copy the code
main.js
import { reactive, computed } from './reactivity';
const test = reactive({
a: 1});const w = computed((a)= > test.a + 1);
console.log(w.value); / / 2
test.a = 2;
console.log(w.value); / / 3
Copy the code
You can see that the column works perfectly
Two problems arise
- why
api
Is not read directlyw
butw.value
In the form of
This is the same reason why there is a ref. The proxy cannot intercept the underlying type, so it wraps the object with a value layer
vue-next
In thecomputed
Does it really have nothing to do with the responsive principle
In fact, it does. In writing computed functions only, the responsive principle works
It can be seen that if we write w.value as we did before, fn will be executed once when we read, even if the value of W.value does not change. When there is a large amount of data, the performance impact will be significant
So how do we optimize?
It’s easy to think of doing fn once to compare the old and the new, but it’s really the same as before, because we’re still doing fn once
Here we can apply the reactive principle. Whenever the internal influence value test.a has been modified, we will re-execute fn to fetch the value, otherwise we will read the previously stored value
reactivity/computed.js
import { setActiveEffect } from './effect';
export function computed(fn) {
// This value will only be true if the variable is changed the first time it comes in
let dirty = true;
/ / the return value
let value;
// Set to true to re-fetch the next read
function changeDirty() {
dirty = true;
}
return {
get value() {
// When the flag is true, the variable needs to be changed
if (dirty) {
dirty = false;
// Set variable control to
setActiveEffect(changeDirty);
/ / get the value
value = fn();
// null-dependent
setActiveEffect(' ');
}
returnvalue; }}; }Copy the code
We define a variable, dirty, to indicate whether the value has been changed, which is true
Similarly, we assign the callback function () => {dirty = true} to the intermediate variable activeEffect before each read, and then perform fn read. At this time, the callback is collected, and dirty is changed when the corresponding property changes
When we run the above example again, the program still works
Function changeDirty() {dirty =; function changeDirty() {dirty =; function changeDirty() {dirty = true; }
The graphic structure of targetMap is as follows
Extraction effect
In both Watch and computed, we have gone through three steps: set the callback => read the value (store the callback)=> empty the callback
In vue-Next’s source code, this step is extracted as a common function. In order to conform to vue-next’s design, we extracted this step, named effect
The first argument to a function is a function that, when executed, triggers a read of the variables in the function and collects the corresponding callback function
The second argument to the function is an object
There is a schedular attribute that expresses a specially specified callback function, which is the first argument if it is not available
There is a lazy attribute, which when true means that the function passed in with the first argument is not executed immediately. The default is false, which specifies the function passed in with the first argument immediately
reactivity/effect.js
// Set of callback functions const targetMap = new WeakMap(); Export let activeEffect; // Export let activeEffect;- // Sets the current callback function
- export function setActiveEffect(effect) {
- activeEffect = effect;
-}
+ // Sets the current callback function
+ export function effect(fn, options = {}) {
+ const effectFn = () => {
+ // Sets the currently active callback function
+ activeEffect = effectFn;
+ // Execute the fn collection callback function
+ let val = fn();
+ // null callback function
+ activeEffect = '';
+ return val;
+};
+ // Options configuration
+ effectFn.options = options;
+ // The function is executed for the first time by default
+ if (! options.lazy) {
+ effectFn();
+}
+ return effectFn;
+}Export function track(target, key) {// Const depsMap = targetmap. get(target); If (depsMap) {const deps = depmap. get(key); If (deps) {// triggers the callback function- deps.forEach((v) => v());
+ deps.forEach((v) => {
+ // Specially specified callback functions are stored in the schedular
+ if (v.options.schedular) {
+ v.options.schedular();
+}
+ // When no callback function is specified
+ else if (v) {
+ v();
+}
+});}}}Copy the code
reactivity/index.js
Export effect
export * from './reactive';
export * from './watch';
export * from './computed';
+ export * from './effect';
Copy the code
main.js
import { reactive, effect } from './reactivity';
const test = reactive({
a: 1}); effect((a)= > {
document.title = test.a;
});
setTimeout((a)= > {
test.a = 2;
}, 1000);
Copy the code
Effect () => {document.title = test.a; } This callback is put into test.a. When test.a changes, the corresponding callback is triggered
TargetMap is shown
The graph structure is shown in figure
Similarly, we changed the notation in computed and watch to effect
reactivity/computed.js
import { effect } from './effect';
export function computed(fn) {
// This value will only be true if the variable is changed the first time it comes in
let dirty = true;
let value;
const runner = effect(fn, {
schedular: (a)= > {
dirty = true;
},
// This is not required for the first time
lazy: true});/ / the return value
return {
get value() {
// When the flag is true, the variable needs to be changed
if (dirty) {
value = runner();
// null-dependent
dirty = false;
}
returnvalue; }}; }Copy the code
reactivity/watch.js
import { effect } from './effect';
export function watch(fn, cb, options = {}) {
let oldValue;
const runner = effect(fn, {
schedular: (a)= > {
// When the dependency is executed, the new value is obtained
let newValue = fn();
// Trigger the callback function
cb(newValue, oldValue);
// Assign the new value to the old value
oldValue = newValue;
},
// This is not required for the first time
lazy: true});// Read values and collect dependencies
oldValue = runner();
}
Copy the code
main.js
import { reactive, watch, computed } from './reactivity';
const test = reactive({
a: 1});const w = computed((a)= > test.a + 1);
watch(
(a)= > test.a,
(val) => {
console.log(val); / / 2});console.log(w.value); / / 2
test.a = 2;
console.log(w.value); / / 3
Copy the code
You can see the code executing normally, with the targetMap shown and two callback functions stored in attribute A
The targetMap graph structure is shown in figure 1
Add options for Watch
Let’s look at this example
import { watch, reactive } from './reactivity';
const test = reactive({
a: {
b: 1,}}); watch((a)= > test.a,
(val) => {
console.log(val); // No trigger}); test.a.b =2;
Copy the code
We observed test.a with watch, and when we changed test.a.b, the observed callback did not trigger, as students who have used vue will know, this situation should be solved with deep property
So how is deep implemented
Let’s recall the process of collecting callback functions
When test.a is read, the callback is collected in test.a, but test.a.b is not read, so the callback is not collected in test.a.b
So we just need to go through test in depth to read the properties while the callback is being collected
Another important thing to note here is that when we intercept objects using Reactive, we don’t intercept the second layer of the object
const test = {
a: {
b: 1,}};const observe = new Proxy(test, {
get(target, key, receiver) {
return Reflect.set(target, key, receiver); }}); test.a// Trigger interception
test.a.b // No interception is triggered
Copy the code
So we need to recursively proxy the intercepted values
reactivity/reactive.js
Const observe = new Proxy(value, {get(target, key, receiver) {// Collect callback function track(target, key);+ const res = Reflect.get(target, key, receiver);
+ return canObserve(res) ? reactive(res) : res;
- return Reflect.get(target, key, receiver);}, // Set (target, key, newValue, receiver) {const res = reflect. set(target, key, newValue, receiver); // Trigger the callback function trigger(target, key); return res; }});Copy the code
reactivity/watch.js
import { effect } from './effect';
+ import { isPlainObject } from './utils';
+ // Depth traversal value
+ function traverse(value) {
+ if (isPlainObject(value)) {
+ for (const key in value) {
+ traverse(value[key]);
+}
+}
+ return value
+}
export function watch(fn, cb, options = {}) {
+ let oldValue;
+ let getters = fn;
+ // Depth traversal value when the deep attribute is present
+ if (options.deep) {
+ getters = () => traverse(fn());
+}
+ const runner = effect(getters, {
- const runner = effect(fn, {Schedular: () => {// let newValue = runner(); // Trigger the callback cb(newValue, oldValue); // assign the newValue to the oldValue oldValue = newValue; }, // the first time not to execute lazy: true,}); // Read the value and collect the callback oldValue = runner(); }Copy the code
main.js
import { watch, reactive } from './reactivity';
const test = reactive({
a: {
b: 1,}}); watch((a)= > test.a,
(val) => {
console.log(val); // { b: 2 }
},
{
deep: true}); test.a.b =2;
Copy the code
TargetMap is as follows, we added back functions not only on the object {a: {b: 1}}, but also on {b: 1}
The targetMap graph structure is shown in figure 1
As you can see, the “deep” property allows you to observe the data in depth. In the examples above, we are using objects. In fact, the “deep” property is also required for arrays, but arrays are handled differently
Array handling
import { watch, reactive } from './reactivity';
const test = reactive([1.2.3]);
watch(
(a)= > test,
(val) => {
console.log(val); // No trigger}); test[0] = 2;
Copy the code
The above column will not fire because we only read test and there is nothing in targetMap
So in the case of arrays, we also fall into the category of deep observation, and when we do deep traversal, we need to read every item in the array
reactivity/watch.js
Function traverse(value) {if (isPlainObject(value)) {for (const key in value) {traverse(value[key]); }}+ // handle arrays
+ else if (Array.isArray(value)) {
+ for (let i = 0; i < value.length; i++) {
+ traverse(value[i]);
+}
+}
return value;
}
Copy the code
main.js
import { watch, reactive } from './reactivity';
const test = reactive([1.2.3]);
watch(
(a)= > test,
(val) => {
console.log(val); / / (2, 2, 3]
},
{
deep: true}); test[0] = 2;
Copy the code
Add deep to true in the column above to see that the callback fires
TargetMap is shown
The first Set item is a Symbol(symbol.tostringTag), which we don’t care about
We store each item of the array as a callback function, and we also store it on the length property of the array
Let’s look at one more example
import { watch, reactive } from './reactivity';
const test = reactive([1.2.3]);
watch(
(a)= > test,
(val) => {
console.log(val); // No trigger
},
{
deep: true}); test[3] = 4;
Copy the code
The above column will not trigger. If you are careful, you may remember that only three positions with index 0, 1 and 2 are collected in targetMap, and the newly added index 3 is not collected
How do we deal with this critical situation?
Remember when we first talked about parsing the array pop method under proxy, we summed it up in one sentence
Length is read and reset when it has a length effect on the array itself
Now when we increment the index, we actually change the length of the array itself, so the length will be reset, and now we have a method, if we can’t find the callback function in the new index, we can read the callback function stored in the array length
reactivity/reactive.js
Const observe = new Proxy(value, {get(target, key, receiver) {// Collect callback function track(target, key); const res = Reflect.get(target, key, receiver); return canObserve(res) ? reactive(res) : res; }, // Intercept set(target, key, newValue, receiver) {+ const hasOwn = target.hasOwnProperty(key);
+ const oldValue = Reflect.get(target, key, receiver);
const res = Reflect.set(target, key, newValue, receiver);
+ if (hasOwn) {
+ // Sets the previous properties
+ trigger(target, key, 'set');
+ } else if (oldValue ! == newValue) {
+ // Add new attributes
+ trigger(target, key, 'add');
+}
- // Triggers the callback function
- trigger(target, key);return res; }});Copy the code
We use hasOwnProperty to determine if the current property is on the object. If the new index of the array is not on the object, it will trigger(target, key, ‘add’). This function
reactivity/effect.js
Export function trigger(target, key, type) {const depsMap = targetmap. get(target); If (depsMap) {// Get the set of callback functions for each attribute- const deps = depsMap.get(key);
+ let deps = depsMap.get(key);
+ // Gets the callback function stored in length when an array is added to an attribute
+ if (type === 'add' && Array.isArray(target)) {
+ deps = depsMap.get('length');
+}Schedular if (v.mask. Schedular) {v.mask. Schedular (); } else if (v) {v(); }}); }}}Copy the code
Then we deal with the case of type add, when type is add and the object is an array, we read the callback stored on length
You can see that with this rewrite, the column is working fine
conclusion
In fact, after reading this article, you will find that this is not a vUE source code anatomy, we did not post the corresponding source code in VUE-next, because I think it is better to think about how to implement from scratch rather than from the source code interpretation to think about why it is implemented
Of course, this article has only implemented the simple responsive principle, if you want to see the complete code can click here, although many function points are not implemented, but the general idea is the same, if you can read this question explained the idea, you can certainly understand vuE-next corresponding source code