preface
1. This paper will write a response principle in Vue next from scratch. Due to the difficulty of space and understanding, we will only implement the core api and ignore some boundary function points
The APIs to be implemented in this article include
- track
- trigger
- effect
- reactive
- watch
- computed
2. Recently, many people asked me about the front end of their personal letters. Few people logged in to the blog didn't reply in time, so I built a front-end buckle skirt 519293536. If you have any questions, please come to me directly. I will try my best to help you. I seldom read blog letters
Project construction
We created the project with the recently popular vite
Versions demonstrated in this article
- node v12.16.1
- npm v6.14.5
- yarn v1.22.4
Let's download the template first
yarn create vite-app vue-next-reactivity Copy code
After the template is downloaded, enter the directory
cd vue-next-reactivity Copy code
Then install the dependency
yarn install Copy code
Then we only keep the main.js File, empty the rest and create the activity folder we want to use
The entire file directory is as shown in the figure. Enter the npm run dev project to start it
Handwritten code
The essence of responsive principle
Before we start to write, let's think about the principle of response?
Let's explain it from the use of Vue next
The response expressions used in Vue next are roughly divided into three
- template or render
After the variables used in the page are changed, the page is refreshed automatically
- computed
When the variables used in the calculation attribute function change, the calculation attribute changes automatically
- watch
When the listening value changes, the corresponding callback function is triggered automatically
From the above three points, we can summarize the essence of response principle
When a value changes, the corresponding callback function will be triggered automatically
The callback function here is the page refresh function in template, the recalculate attribute value function in computed and the watch callback that is a callback function
So we're going to implement the responsive principle, and now we're going to split it into two questions
- Change of monitoring value
- Trigger the corresponding callback function
We solved these two problems and wrote the principle of response
Change of monitoring value
javascript provides two APIs to change the listening value
One is used in vue2.x Object.defineProperety
const obj = {};
let aValue = 1;
Object.defineProperty(obj, 'a', {
enumerable: true,
configurable: true,
get() {
console.log('I was read');
return aValue;
},
set(value) {
console.log('I was set up');
aValue = value;
},
});
obj.a; // I was read
obj.a = 2; // I was set up
Copy code
Another method is the proxy used in Vue next, which is also used in this handwriting
This method solves the problem Object.defineProperety Four pain points of
- Unable to block the addition and deletion of properties on the object
- Unable to block the methods that can affect the current array by calling push pop shift unshift on the array
- Too much performance overhead in intercepting array indexes
- Unable to block Set types such as Set Map
The first two, of course
As for the third point, the change of array index in vue2.x has to be set through this.$set, which leads to many students mistaking it Object.defineProperety In fact, it can't intercept array index. The reason why vue2.x didn't do this is that it's not cost-effective
The above four points of proxy can be solved perfectly. Now let's start to write a proxy interception!
proxy interception
We created two files in the activity directory
utils.js Store some common methods
reactive.js Method of storing proxy interception
Let's go first utils.js First add the method to be used to determine whether it is a native object
reactivity/utils.js
// Get original type
export function toPlain(value) {
return Object.prototype.toString.call(value).slice(8, -1);
}
// Is it a native object
export function isPlainObject(value) {
return toPlain(value) === 'Object';
}
Copy code
reactivity/reactive.js
import { isPlainObject } from './utils';
// Only arrays and objects in this column can be observed
function canObserve(value) {
return Array.isArray(value) || isPlainObject(value);
}
// Intercept data
export function reactive(value) {
// Values that cannot be monitored are returned directly
if (!canObserve(value)) {
return;
}
const observe = new Proxy(value, {
// Block read
get(target, key, receiver) {
console.log(`${key}Read it`);
return Reflect.get(target, key, receiver);
},
// Intercept settings
set(target, key, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver);
console.log(`${key}Set up`);
return res;
},
});
// Return the observed proxy instance
return observe;
}
Copy code
reactivity/index.js
Export method
export * from './reactive'; Copy 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 read
// 4
testArr.pop(); // pop read length read 2 read length set
Copy code
As you can see, we added a reactive method for proxy interception of objects and arrays, and returned the corresponding proxy instance
1, 2, 3 in the column are all well understood. Let's explain the fourth one
When we call the pop method, we first trigger the get intercept, and the Print Pop is read
After calling the pop method, the length of the array is read, the get intercept is triggered, and the print length is read.
The return value of pop method is the currently deleted value. It will read the value of array index 2 and trigger get intercept. Print 2 is read
After pop, the array length will be changed, which will trigger set interception, and the print length will be set
You can also try other ways to change the array
Can be summed up as a sentence
When there is an effect on the length of the array itself, the length will be read and reset, and the index of the changed value will also be read or reset (push unshift)
Add callback function
We use proxy to intercept the value and solve our first problem
But we didn't trigger the callback function after the value changed. Now let's supplement the callback function
reactivity/reactive.js
import { isPlainObject } from './utils';
// Only arrays and objects in this column can be observed
function canObserve(value) {
return Array.isArray(value) || isPlainObject(value);
}
+ // Assumed callback function
+ function notice(key) {
+ console.log(`${key}Changed and triggered callback function`);
+ }
// Intercept data
export function reactive(value) {
// Values that cannot be monitored are returned directly
if (!canObserve(value)) {
return;
}
const observe = new Proxy(value, {
// Block read
get(target, key, receiver) {
- console.log(`${key}Read it`);
return Reflect.get(target, key, receiver);
},
// Intercept settings
set(target, key, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver);
- console.log(`${key}Set up`);
+ // Trigger hypothetical callback function
+ notice(key);
return res;
},
});
// Return the observed proxy instance
return observe;
}
Copy code
In the most intuitive way, we trigger our hypothetical callback in the set interception when the 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 function is triggered
test.b = 3; // b is changed and the callback function is triggered
Copy code
You can see that when the value changes, the corresponding log is output
But there must be something wrong with this list. There is more than one problem. Let's upgrade it step by step
Collection of callback functions
A and b in the above column all correspond to a callback function notice. In the actual scenario, a and b may correspond to different callback functions respectively. If we only use a simple global variable to store the callback function, it is obviously not appropriate. If the latter will cover the former, then how can we make the callback function correspond to each value?
It's easy to think of the key value object in js. If attribute a and attribute b are the key values of the object, they can distinguish their value values
But collecting callback functions with objects is problematic
In the above column, we have a test object with properties a and b. when we have another object test1, if it also has properties a and b, isn't it repeated? This will trigger the problem of repetition we mentioned earlier
Some students may say that it's not good to use test and test1 as attribute names in another layer. This method is also infeasible. In the same execution context, there won't be two identical variable names, but different execution contexts can, which leads to the problem of repetition mentioned above
To deal with this problem, we need to use the characteristics of js object passing by reference
// 1.js
const obj = {
a: 1,
};
// 2.js
const obj = {
a: 1,
};
Copy code
We define obj with the same name attribute data structure in two folders, but we know that these two obj are not equal because their memory points to different addresses
So if we can directly use the object as the key value, can we distinguish the seemingly "same" object?
The answer must be yes, but we have to change the data structure, because the key value of an object in js cannot be an object
Here we will use a new data structure Map and WeakMap in es6
We illustrate the storage mode of this data structure with an example
Suppose now that we have two objects obj with the same data structure, each of which has its own properties a and b. the change of each property will trigger different callback functions
// 1.js
const obj = {
a: 1,
b: 2
};
// 2.js
const obj = {
a: 1,
b: 2
};
Copy code
Use Map and WeakMap to store as shown in the following figure
We define the global variable targetMap for storing callback function as a WeakMap. Its key value is each object. In this column, there are two objs. The value value of targetMap is a Map. In this column, two objs have two attributes a and b respectively. The key of Map is the attribute a and b, and the value of Map is the Set callback function Set corresponding to the attributes a and b respectively
You may wonder why targetMap uses WeakMap and the Map used for storing the attributes of each object. This is because WeakMap can only use the object as the key, and Map can be an object or a string. For example, the sub attributes a and b of the above columns can only be saved with Map
Let's use the actual api to deepen our understanding of this storage structure
- computed
const c = computed(() => test.a) Copy code
Here we need to put the () = > test.a callback function in the test.a collection, as shown in the figure
- watch
watch(() => test.a, val => { console.log(val) })
Copy code
Here we need to add Val = >{ console.log (VAL)} callback function is placed in the set of test.a, as shown in the figure
- template
createApp({
setup() {
return () => h('div', test.a);
},
});
Copy 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 the callback function, let's think about how to put the callback function into our defined storage structure
Let's take the list above
watch(() => test.a, val => { console.log(val) })
Copy code
In this column, we need to change the callback function Val = >{ console.log (VAL)}) put it into the Set set of test.a, so we need to get the object test and the property a of the current object. If we only pass () = > test.a, we can only get the value of test.a, and we cannot know the specific object and property
But in fact, after reading the value of test.a, you can get specific objects and properties in disguise
Do you remember that we used proxy to intercept the reading of test.a. the first parameter intercepted by get is the currently read object, and the second parameter is the currently read property
So the collection of callback functions is handled in get interception of proxy
Now, let's use the code to implement the ideas we just made
First we create effect.js This file is used to store the collection method and trigger method of the callback function
reactivity/effect.js
// Callback function collection
const targetMap = new WeakMap();
// Collect callback functions
export function track(target, key) {
}
// Trigger callback function
export function trigger(target, key) {
}
Copy code
Then rewrite the intercepted content in the proxy
reactivity/reactive.js
import { isPlainObject } from './utils';
+ import { track, trigger } from './effect';
// Only arrays and objects in this column can be observed
function canObserve(value) {
return Array.isArray(value) || isPlainObject(value);
}
- // Assumed callback function
- function notice(key) {
- console.log(`${key}Changed and triggered callback function`);
- }
// Intercept data
export function reactive(value) {
// Values that cannot be monitored are returned directly
if (!canObserve(value)) {
return;
}
const observe = new Proxy(value, {
// Block read
get(target, key, receiver) {
+ // Collect callback functions
+ track(target, key);
return Reflect.get(target, key, receiver);
},
// Intercept settings
set(target, key, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver);
+ // Trigger callback function
+ trigger(target, key);
- // Trigger hypothetical callback function
- notice(key);
return res;
},
});
// Return the observed proxy instance
return observe;
}
Copy code
What we haven't added here is that we can clearly see the location of the collection and trigger
Now let's supplement the track collection callback function and trigger callback function
reactivity/effect.js
// Callback function collection
const targetMap = new WeakMap();
// Collect callback functions
export function track(target, key) {
// Get the map of each object through the object
let depsMap = targetMap.get(target);
if (!depsMap) {
// When objects are first collected, we need to add a map collection
targetMap.set(target, (depsMap = new Map()));
}
// Get the callback function collection of each property under the object
let dep = depsMap.get(key);
if (!dep) {
// When the object properties are collected for the first time, we need to add a set set
depsMap.set(key, (dep = new Set()));
}
// Add callback function here
dep.add(() => console.log('I am a callback function'));
}
// Trigger 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 property
const deps = depsMap.get(key);
if (deps) {
// Trigger callback function
deps.forEach((v) => v());
}
}
}
Copy code
Then run our demo
main.js
import { reactive } from './reactivity';
const test = reactive({
a: 1,
b: 2,
});
test.b; // Read collection callback function
setTimeout(() => {
test.a = 2; // No triggers because no callbacks are collected
test.b = 3; // I am a callback function
}, 1000);
Copy code
Let's take a look at the targetMap structure at this time
There is a key value {a: 1,b: 2} in targetMap. Its value value is also a Map. There is a key value b in this Map. The value of this Map is the Set of callback functions. Now there is only one () we have written = > console.log ('I'm a callback function')
It's like this with graphic structure
You may think that to collect callback functions and read test.b once is an anti-human operation. This is because we haven't talked about the corresponding api yet. The normal read operation doesn't need such a manual call, and the api will handle it by itself
watch
There is a big problem in the above columns, that is, we do not have a custom callback function, which is directly written dead in the code
Now we will implement the custom callback function through watch
There are many APIs for watch in Vue next, and we will implement some of them, which is enough for us to understand the principle of response
The demo we will implement is as follows
export function watch(fn, cb, options) {}
const test = reactive({
a: 1,
});
watch(
() => test.a,
(val) => { console.log(val); }
);
Copy code
watch takes three parameters
The first parameter is a function that represents the value being monitored
The second parameter is a function to express the callback to be triggered after the listening value changes. The first parameter is the value after the change, and the second parameter is the value before the change
The third parameter is an object with only one deep attribute. Deep table depth observation
Now what we need to do is to put the callback function (VAL) = >{ console.log (VAL);} in the Set set of test.a
So before () = > test.a can read test.a, we need to store the callback function with a variable
When reading test.a to trigger the track function, you can get this variable in the track function and store it in the Set of corresponding properties
reactivity/effect.js
// Callback function collection
const targetMap = new WeakMap();
+ // Currently active callback function
+ export let activeEffect;
+ // Set current callback function
+ export function setActiveEffect(effect) {
+ activeEffect = effect;
+ }
// Collect callback functions
export function track(target, key) {
// No callback function is activated, exit directly and do not collect
if (!activeEffect) {
return;
}
// Get the map of each object through the object
let depsMap = targetMap.get(target);
if (!depsMap) {
// When objects are first collected, we need to add a map collection
targetMap.set(target, (depsMap = new Map()));
}
// Get the callback function collection of each property under the object
let dep = depsMap.get(key);
if (!dep) {
// When the object properties are collected for the first time, we need to add a set set
depsMap.set(key, (dep = new Set()));
}
// Add callback function here
- dep.add(() => console.log('I am a callback function'));
+ dep.add(activeEffect);
}
// Trigger callback function
export function trigger(target, key) {
// ellipsis
}
Copy code
Because the watch method is not in the same file as the track and trigger methods, we export the variable activeEffect with export and provide a method setActiveEffect to modify it
This is also a way to use common 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 callback function before executing fn to get oldValue
setActiveEffect(() => {
// Make sure that the callback function triggers a new value
let newValue = fn();
// Trigger callback function
cb(newValue, oldValue);
// New value assigned to old value
oldValue = newValue;
});
// Read values and collect callback functions
oldValue = fn();
// Null callback function
setActiveEffect('');
}
Copy code
Very simple lines of code, set the callback function through setActiveEffect before executing fn to read the value, so that the current callback function activeEffect can be obtained in the track function when reading, and empty the callback function after reading, and then it is finished
We also need to export the watch method
reactivity/index.js
export * from './reactive';
+ export * from './watch';
Copy code
main.js
import { reactive, watch } from './reactivity';
const test1 = reactive({
a: 1,
});
watch(
() => test1.a,
(val) => {
console.log(val) // 2;
}
);
test1.a = 2;
Copy code
You can see that the normal execution of column sub prints out 2. Let's take a look at the structure of targetMap
There is a key value {a:1} in targetMap, and its value value is also a Map. There is a key value a in this Map, and the value of this Map is the callback function (VAL) = >{ console.log (val); }
The graph structure of targetMap is as follows
computed
We will go back to other api supplements of watch. After feeling the thinking of responsive principle, we can take advantage of the hot iron to realize the function of computed
There are many ways to write the same calculated api in Vue next. We will only write the function return value
export function computed(fn) {}
const test = reactive({
a: 1,
});
const w = computed(() => test.a + 1);
Copy code
However, if we only implement the written method of computed incoming function, in fact, it has little to do with the responsive principle in Vue next
Because the api read value provided in Vue next is not directly read W, but w.value
We create computed.js , complementing the computed function
reactivity/computed.js
export function computed(fn) {
return {
get value() {
return fn();
},
};
}
Copy code
You can see just a few lines of code. Each time you read value, run fn evaluation again
reactivity/index.js
Let's export it again
export * from './reactive';
export * from './watch';
+ export * from './computed';
Copy code
main.js
import { reactive, computed } from './reactivity';
const test = reactive({
a: 1,
});
const w = computed(() => test.a + 1);
console.log(w.value); // 2
test.a = 2;
console.log(w.value); // 3
Copy code
You can see the perfect operation of Liezi
Here are two questions
- Why api is written in the form of w.value instead of directly reading w
This is the same reason as ref. proxy cannot intercept the basic type, so it needs to add a layer of value to wrap the object
- Does the computed in Vue next really have nothing to do with the responsive principle
In fact, there is a relationship. In the writing method of only realizing the computed input function, the principle of response enables optimization
We can see that if the value of w.value does not change according to our previous writing method, we will execute fn once when we read it. When the amount of data increases, the performance will be greatly affected
How can we optimize it?
It's easy to think of executing fn once to compare old and new values, but this is the same as before, because we still execute fn once
Here we can use the response principle. As long as the internal influence value test.a is modified, we will execute fn again to get the value, otherwise we will read the previously stored value
reactivity/computed.js
import { setActiveEffect } from './effect';
export function computed(fn) {
// This value will be true only after the variable is changed. It will be true the first time it comes in
let dirty = true;
// Return value
let value;
// Set to true to indicate that the next read needs to be retrieved
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 value
value = fn();
// Air control dependency
setActiveEffect('');
}
return value;
},
};
}
Copy code
We have defined a variable "dirty" to express whether the value has been modified or not. If it has been modified, it will be true
Similarly, before reading the value, we assign the callback function () = > {directory = true} to the intermediate variable activeEffect, and then perform fn reading. At this time, the callback is collected. When the corresponding property is changed, the directory is also changed
Let's run the above column again. The program is still running normally
Let's take a look at the structure of targetMap. targetMap has a key value {a:1}, and its value value is also a Map. In this Map, there is a key value a, and the value of this Map is the callback function changedirty() {dirty = true;}
The graph structure of targetMap is as follows
Extract effect
In watch and computed, we have gone through three steps: setting callback function = > reading value (storing callback function) = > clearing callback function
In the source code of Vue next, this step is extracted as a common function. In order to conform to the design of Vue next, we extract this step, named effect
The first parameter of the function is a function. After the function is executed, it will trigger the reading of each variable in the function and collect the corresponding callback function
The second argument to the function is an object
There is a schedule attribute that represents a specially specified callback function. If there is no such attribute, the callback function is the first parameter
There is a lazy attribute. If it is true, the function passed in by the first parameter does not need to be executed immediately. The default is false, that is, the function passed in by the first parameter is specified immediately
reactivity/effect.js
// Callback function collection
const targetMap = new WeakMap();
// Currently active callback function
export let activeEffect;
- // Set current callback function
- export function setActiveEffect(effect) {
- activeEffect = effect;
- }
+ // Set current callback function
+ export function effect(fn, options = {}) {
+ const effectFn = () => {
+ // Set the currently active callback function
+ activeEffect = effectFn;
+ // Execute fn collection callback function
+ let val = fn();
+ // Empty callback function
+ activeEffect = '';
+ return val;
+ };
+ // options configuration
+ effectFn.options = options;
+ // Default first execution function
+ if (!options.lazy) {
+ effectFn();
+ }
+ return effectFn;
+ }
// Collect callback functions
export function track(target, key) {
// ellipsis
}
// Trigger 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 property
const deps = depsMap.get(key);
if (deps) {
// Trigger callback function
- deps.forEach((v) => v());
+ deps.forEach((v) => {
+ // The specially specified callback function is stored in the scheduler
+ if (v.options.schedular) {
+ v.options.schedular();
+ }
+ // Trigger directly when no callback function is specified
+ else if (v) {
+ v();
+ }
+ });
}
}
}
Copy code
reactivity/index.js
Export effect
export * from './reactive';
export * from './watch';
export * from './computed';
+ export * from './effect';
Copy code
main.js
import { reactive, effect } from './reactivity';
const test = reactive({
a: 1,
});
effect(() => {
document.title = test.a;
});
setTimeout(() => {
test.a = 2;
}, 1000);
Copy code
The first time the effect is self executing, it will () = >{ document.title =test.a;} this callback function is put into test.a. when test.a changes, trigger the corresponding callback function
Target map is as shown in the figure
The figure structure is as shown in the figure
Similarly, we change the writing method in computed and watch and replace it with effect
reactivity/computed.js
import { effect } from './effect';
export function computed(fn) {
// This value will be true only after the variable is changed. It will be true the first time it comes in
let dirty = true;
let value;
const runner = effect(fn, {
schedular: () => {
dirty = true;
},
// No execution for the first time
lazy: true,
});
// Return value
return {
get value() {
// When the flag is true, the variable needs to be changed
if (dirty) {
value = runner();
// Air control dependency
dirty = false;
}
return value;
},
};
}
Copy code
reactivity/watch.js
import { effect } from './effect';
export function watch(fn, cb, options = {}) {
let oldValue;
const runner = effect(fn, {
schedular: () => {
// When the dependency is executed, the new value is obtained
let newValue = fn();
// Trigger callback function
cb(newValue, oldValue);
// New value assigned to old value
oldValue = newValue;
},
// No execution for the first time
lazy: true,
});
// Read values and collect dependencies
oldValue = runner();
}
Copy code
main.js
import { reactive, watch, computed } from './reactivity';
const test = reactive({
a: 1,
});
const w = computed(() => test.a + 1);
watch(
() => test.a,
(val) => {
console.log(val); // 2
}
);
console.log(w.value); // 2
test.a = 2;
console.log(w.value); // 3
Copy code
You can see that the code executes normally, targetMap is as shown in the figure, and two callback functions are stored in attribute a
The graph structure of targetMap is as shown in the figure
options to supplement watch
Let's take a look at this list
import { watch, reactive } from './reactivity';
const test = reactive({
a: {
b: 1,
},
});
watch(
() => test.a,
(val) => {
console.log(val); // No trigger
}
);
test.a.b = 2;
Copy code
We use watch to observe test.a. when we change test.a.b, the observed callback is not triggered. Students who have used vue will know that this situation should be solved by using the deep attribute
So how does deep work
Let's recall the process of callback function collection
When test.a is read, the callback function is collected into test.a, but test.a.b is not read here, so the callback function is not collected into test.a.b naturally
So we only need to go through the test in depth and read the properties when we collect the callback function
It should also be noted that when we intercept an object with reactive, we will not 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 intercept
test.a.b // Intercept will not be triggered
Copy code
So we need to recursively proxy the intercept value
reactivity/reactive.js
const observe = new Proxy(value, {
// Block read
get(target, key, receiver) {
// Collect callback functions
track(target, key);
+ const res = Reflect.get(target, key, receiver);
+ return canObserve(res) ? reactive(res) : res;
- return Reflect.get(target, key, receiver);
},
// Intercept settings
set(target, key, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver);
// Trigger callback function
trigger(target, key);
return res;
},
});
Copy 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 exists
+ if (options.deep) {
+ getters = () => traverse(fn());
+ }
+ const runner = effect(getters, {
- const runner = effect(fn, {
schedular: () => {
// When the dependency is executed, the new value is obtained
let newValue = runner();
// Trigger callback function
cb(newValue, oldValue);
// New value assigned to old value
oldValue = newValue;
},
// No execution for the first time
lazy: true,
});
// Read values and collect callback functions
oldValue = runner();
}
Copy code
main.js
import { watch, reactive } from './reactivity';
const test = reactive({
a: {
b: 1,
},
});
watch(
() => test.a,
(val) => {
console.log(val); // { b: 2 }
},
{
deep: true,
}
);
test.a.b = 2;
Copy code
targetMap is as follows, we not only added the return function on the object {A: {b: 1}}, but also added the return function on {b: 1}
The graph structure of targetMap is as shown in the figure
You can see that after adding the deep attribute, you can observe the data in depth. In the above columns, we all use objects. In fact, deep observation is also necessary for arrays, but the array processing is a little different. Let's see the differences
Array processing
import { watch, reactive } from './reactivity';
const test = reactive([1, 2, 3]);
watch(
() => test,
(val) => {
console.log(val); // No trigger
}
);
test[0] = 2;
Copy code
The above columns will not be triggered, because we only read the test and there is nothing in the targetmap
So in the case of arrays, we also belong to the deep observation category. When traversing the depth, we need to read every item of the array
reactivity/watch.js
// Depth traversal value
function traverse(value) {
// Working with objects
if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key]);
}
}
+ // Working with arrays
+ else if (Array.isArray(value)) {
+ for (let i = 0; i < value.length; i++) {
+ traverse(value[i]);
+ }
+ }
return value;
}
Copy code
main.js
import { watch, reactive } from './reactivity';
const test = reactive([1, 2, 3]);
watch(
() => test,
(val) => {
console.log(val); // [2, 2, 3]
},
{
deep: true
}
);
test[0] = 2;
Copy code
Add "deep" as "true" in the above column to see that the callback is triggered
Target map is as shown in the figure
The first item Set is a symbol( Symbol.toStringTag )We don't have to worry about it
We store the callback function for each item of the array, and also store it on the length attribute of the array
Let's have another look at Liezi
import { watch, reactive } from './reactivity';
const test = reactive([1, 2, 3]);
watch(
() => test,
(val) => {
console.log(val); // No trigger
},
{
deep: true,
}
);
test[3] = 4;
Copy code
The above columns will not be triggered. Careful students may remember that only three locations with index 0 1 2 are collected in targetMap, and those with index 3 are not collected
How should we deal with this critical situation?
Do you remember that we first talked about the parsing of array pop method under proxy? At that time, we summed it up as a sentence
Length will be read and reset when there is a length effect on the array itself
Now we have changed the length of the array itself by adding new value through the index, so the length will be reset. Now we have a method. When we can't find the callback function on the new index, we can read the callback function stored on the length of the array
reactivity/reactive.js
const observe = new Proxy(value, {
// Block read
get(target, key, receiver) {
// Collect callback functions
track(target, key);
const res = Reflect.get(target, key, receiver);
return canObserve(res) ? reactive(res) : res;
},
// Intercept settings
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) {
+ // Set previous properties
+ trigger(target, key, 'set');
+ } else if (oldValue !== newValue) {
+ // Add a new property
+ trigger(target, key, 'add');
+ }
- // Trigger callback function
- trigger(target, key);
return res;
},
});
Copy code
We use hasOwnProperty to determine whether the current property is on the object. It is obvious that the new index of the array is not available. At this time, we will go to trigger(target, key, 'add'); this function
reactivity/effect.js
// Trigger callback function
export function trigger(target, key, type) {
// Get the map of the object
const depsMap = targetMap.get(target);
if (depsMap) {
// Get the collection of callback functions corresponding to each property
- const deps = depsMap.get(key);
+ let deps = depsMap.get(key);
+ // Get the callback function stored on the length directly when the array adds a new attribute
+ if (type === 'add' && Array.isArray(target)) {
+ deps = depsMap.get('length');
+ }
if (deps) {
// Trigger callback function
deps.forEach((v) => {
// The specially specified callback function is stored in the scheduler
if (v.options.schedular) {
v.options.schedular();
}
// Trigger directly when no callback function is specified
else if (v) {
v();
}
});
}
}
}
Copy code
Then we deal with the case of type add. When the type is add and the object is an array, we will read the callback function stored on the length
You can see that with such an override, the column can run normally
summary
1. In fact, after reading this article, you will find that this article is not an anatomy of vue source code. We didn't post the corresponding source code in vue next in the whole process, because I think it's better to think about how to realize it from scratch than from the source code interpretation
2. Of course, this article only implements the simple responsive principle. If you want to see the complete code, you can click here Although many function points have not been implemented, the general ideas are the same. If you can read the ideas explained in this question, you can definitely understand the corresponding source code in Vue next
3. Recently, I asked many people about the front-end problems in their private letters. Few people logged in to the blog didn't reply in time, so I built a front-end buckle skirt 519293536. If you have any questions, please come to me directly. I will try my best to help you. I seldom read blog letters
The text and pictures of this article come from the Internet and my own ideas. They are only for learning and communication. They have no commercial use. The copyright belongs to the original author. If you have any questions, please contact us in time for handling