preface
We all know that Vue is a kind of MVVM framework, and its biggest feature is data-driven view. So what is a data-driven attempt? Here we can simply view data as state, view as UI, UI can’t be static, it should be dynamic, so to get state changes and view changes with it is called data-driven attempt. We use the following mathematical formula to describe:
UI = Render(State)
Here UI represents the user interface, State represents the State, and Vue acts as the Render role. After Vue found that the state changed, it went through a series of processing and was finally displayed on the user UI. So the first question is, how does Vue know when state has changed?
I. What is change detection
How does Vue know that state has changed? This leads to the concept of change detection, or state tracking, to notify the view when state changes. Change detection is a term that has been around for a long time. In other MVVM frameworks React uses contrast virtual nodes for change detection, and Angular uses a dirty data check process for change detection. Then I will analyze how Vue performs change detection through source code parsing. Here we parse version 2.6.x.
2. Change detection of Object
1. Make an Object detectable.
let myTeslaCar = {
name: 'My little car'.age: 1.description: '2016 Black Tesla'
}
Object.defineProperty(myTeslaCar, "name", {
set(newVal) {
console.log('Name property set to value',newVal)
},
get () {
console.log('Name property read')}})let carName = myTeslaCar.name
myTeslaCar.name = 'My Tesla Model3'
Copy the code
The following output is displayed:
Next: We make each property of myTeslaCar detectable, and we create an Observe class. The complete code is shown below.
function def(object, key ,val) {
Object.defineProperty(object,key, {
configurable: true.writable: true.enumerable: true.value: val
})
}
/ / corresponding vue source location: SRC/core/observer/index. Js
/** The Observer class recursively makes all attributes of an object monitorable */
class Observer {
constructor(value) {
this.value = value;
// Add an '__ob__' attribute to value with the current Observe instance.
// Make it responsive. // Make it responsive
def(value,'__ob__'.this);
if(Array.isArray(value)) {
// The logic when val is array
} else {
this.walk(value); }}/ * * *@description Iterate through each property, then make it testable *@param {Object} obj
*/
walk(obj) {
const keys = Object.keys(obj).filter(key= >key! = ='__ob__');
for(let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]); }}}/ * * *@description: Makes the key of an object testable *@param {object} obj
* @param {string} key
* @param {*} val* /
function defineReactive(obj, key, val) {
Val =obj[key]
if(arguments.length === 2) {
val = obj[key]
}
// If val is an object, recurse
if(typeof val === 'object') {
new Observer(val);
}
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
console.log(`${key}Property was read)
return val;
},
set(newVal) {
if(val === newVal){
return
}
console.log(`${key}Property is set to a new value${newVal}`) val = newVal; }})}Copy the code
We create an ObServer class that will convert an Object into a detectable Object. We added an __ob__ attribute to Value to mark the object and converted it to reactive to avoid duplication; Only the Object type then calls Walk to convert each property into a getter/setter to detect property changes. If the fruit property is still an object in defineReactive, use the New Observer(val) to recurse the child property so that we can convert all properties in the object (including child properties) to getter/setter form. In other words, if you pass in an object to Observe, you can change all the properties of the object to a detectable, responsive object.
The observer is located in the source code of the SRC/core/observer/index. Js.
Now let’s redefine a myCar2 object:
Const myCar2 = {name: 'Tesla 2016 black', age: 1, description:' Tesla 2016 black', infomation: {color: 'black', date: }} const carObserver = new Observer(myCar2); console.log(carObserver) console.log(carObserver.value.name)Copy the code
The running results are as follows:
So all the objects in myCar2 become responsive.
2. Rely on collection
2.1 What is dependency Collection?
In the previous chapter, we completed the first step by converting an object into a responsive detectable object. Knowing when the data is changing, we can tell the view to update the change. So the question is, how exactly do we tell the view to change? The view is so big, who should we tell to change it? You can’t just update the whole view for one data change, right? This is obviously unreasonable.
At this point you will think, so who is using the corresponding status, update who! Yes, that’s the idea. Now let’s put it in a more elegant way: To whom we use the data to rely on the state, we put the each state to create a dependency array (because a state may be used for several), when a state has changed, we will go to the corresponding dependent on array to inform each dependent, tell them you rely on state change, change your update! This process is dependent collection.
2.2 When to collect dependencies and when to notify dependency updates?
Now that we understand the concept of dependency collection, let’s ask ourselves the question, when do dependencies actually get collected? When do you update dependencies? Do you think of getters and setters? Right, those are the two key points. In fact, whoever uses the state is the same as whoever reads the state. Thus we should collect the dependency in the getter and then notify the dependency change in the setter when the state changes. A summary is as follows:
Dependencies are collected in getters, and dependency updates are notified in setters
2.3 Where to collect dependencies?
Now that we know about dependency collection and how to collect and when to update dependencies, let’s think about where to collect dependencies.
In 2.1, we mentioned that we can store dependencies in a dependency array, and those who depend on states are put into the dependency array for the corresponding state. But using just an array to manage dependencies seems inadequate and the code is too coupled. Better yet, we should extend our dependency management capabilities by creating a dependency manager for each piece of data that manages all dependencies for that data. Vue uses the Dep class to manage dependencies. Let’s look at the following simplified version of the dependency management class Dep.
/* * @description: dependency management Dep */
export default class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub)
}
depend() {
if(window.target) {
this.addSub(window.target)
}
}
}
/** Removes the given item */ from the array
export function remove(arr, item) {
if(arr.length) {
const index = arr.indexOf(item)
if(index > -1) {
return arr.splice(index, 1)}}}Copy the code
We defined a simple Dep class, and then added methods for adding and deleting dependencies, using Depend to add dependencies, and notify dependency updates. Now we can do dependency collection in the getter and notify dependency updates in the setter. Look at the setter and getter in defineReactive.
function defineReactive(obj, key, val) {
Val =obj[key]
if(arguments.length === 2) {
val = obj[key]
}
// If val is an object, recurse
if(typeof val === 'object') {
new Observer(val);
}
const dep = new Dep(); // Instantiate a dependency manager
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
console.log(`${key}Property was read)
// Collect dependencies
dep.depend();
return val;
},
set(newVal) {
if(val === newVal){
return
}
console.log(`${key}Property is set to a new value${newVal}`)
val = newVal;
dep.notify(); // Notifications depend on updates}})}Copy the code
3. Who is dependence?
From the previous chapter, you should now have an understanding of what collecting dependencies is, and when to collect dependencies and notify dependency updates. So who is the dependency? Although we say that whoever uses the state is a dependency, this is just our dictation and we need to know how the real dependency is implemented in the code.
In Vue2, there is actually a Watcher class implemented, which is what we described above, in other words: whoever uses the state is a dependency and creates an instance of Watch for that person. Instead of notifying the dependency directly when the data changes, we notify the dependent Watch instance, which notifies the view of updates. Here is a simplified implementation of the Watch class:
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn)
this.value = this.get()
}
get () {
window.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
/** * Parse simple path. * Extract the value represented by a string path of the form 'data.a.b.c' from the real data object * for example: * data = {a:{b:{c:2}}} * parsePath('a.b.c')(data) // 2 */
const bailRE = /[^\w.$]/
export function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('. ')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if(! obj)return
obj = obj[segments[i]]
}
return obj
}
}
Copy the code
We create a Watch instance for each dependency. During the initialization of the Watch instance, the data is read. First, it will mount itself to the globally unique location window.target(the vue2 source code uses dep.target). Since the data is read, the getter for that data will be triggered to collect the dependency, and then the dependency that is currently reading the data will be fetched via dep.Depend (Watch instance, Window.target) and store it to the dependency array, and then release window.target in Watch’s get method.
When data changes, dependency updates are notified via dep.notify() in the setter for the current data, and the view is updated by iterating through the dependency in dep.notify() and calling the Update method of Watch.
In a nutshell:
Watch will mount itself to a globally unique location, then collect the dependency through the getter when reading data, get the value that is globally mounted (such as window.target in this case) and add it to the dependency group. Once the dependencies are collected, Dep notifies all dependencies of updates and calls Watch update to reach the update view.
For easy comprehension, please refer to the following figure:
Graph LR Data --> Getter(Getter) --3 Get the dependency that is currently reading Data --> Global(window.target) Data(Data) --> Setter(Setter) Getter(Getter) -.2. Reading data triggers the getter to return data.-> Wather(Wather) Wather(Wather) --1. Mount to globally unique location --> Global(window.target) Global --4 Add to dependency array --> Dep(Dep)
4. Deficiencies
- Object.defineproperty has one downside: adding a new pair to the Object
key/value
When it is undetectable, that is, there will be a problem when we add or remove onekey/value
There is no way to know about state changes, no way to notify dependencies, and no way to update views. This is one of the reasons why our developers often find data changes but views are not updated. - Object.defineproperty cannot intercept most operations on arrays. Vue2’s solution is to override several commonly used methods in arrays, which we’ll explain in the chapter on array change detection.
Vue2 also takes note of this and provides global apis to solve this problem, vue. set and vue. delete, which are explained in detail in the global API section.
5. To summarize
First, we implement the change detection of attributes through Object.defineProperty, and encapsulate the Observer class to implement the dynamic detection of all attributes of Object into getter/setter form.
Then we learned what collecting dependencies are, collecting dependencies in getters and notifying dependency updates in setters. Dep dependency management classes are then encapsulated to manage dependency collection.
Finally, we create Wacth instances for each dependency. When data changes, we notify the Watch instance, and the Watch instance updates it.
The overall process is as follows:
1. Use the Observer class to convert objects into getter/setter forms to track state changes.
2. When the outside world reads data using the Watch instance, the getter will be triggered to add the Watch instance to the Dep.
3. When data changes, the setter notifies the dependency (Watch) to update the change.
4. When the Watch receives the notification, it will notify the outside world. After receiving the notification, the outside world may update the view or call the callback function set by the user.
Three. Array change detection
In the previous chapter, we introduced change detection for objects, so in this chapter we will study change detection for arrays. Why does Array change detection need to be explained in turn? Why is it two sets of logic with Object?
Array is not available because we are using defineProperty on the object’s prototype, so we need to understand how Vue designs the change detection logic for arrays. Array detection of objects has a different set of logic, but the basic idea remains the same:
Dependencies are collected when data is read, and all dependencies are notified to update when data changes.
1. Where do you collect dependencies
We should collect dependencies where we use arrays, so the question is, where do we collect dependencies?
An Array is collected in the getter in the same way as an Object.
DefineProperty can’t listen for array changes, so how do you listen for data changes, and how do you trigger getters?
Let’s recall that we defined arrays as follows:
export default {
data () {
return{... .hobby: ['1' hobbies.'2' hobbies. .'n' hobbies]... ,}}}Copy the code
For those of you who may have noticed that our data is written to objects, to get to the object, We simply need to read from the object, which triggers the getter for Hobby, so that we can collect dependencies. Thus, the solution of the title can be obtained:
Array collection dependencies are also done in the getter.
2. Make the Array testable.
In the previous section, we learned that Array dependencies are also collected through the getter. Recall the process of making an Object detectable. Dependencies should be collected in the getter when data is read, and dependencies updated when data changes in the setter. At this point we know that the collection dependency is done through the getter, and that’s part of the story, that we know when the Array was read, and we don’t know when the Array changed. Let’s analyze this question: How do we know when the Array data changes?
2.1 train of thought
How do we know when the data changes in an Object is sensed by a setter, but there is no setter for Array?
Actually, if you think about it, Array changes, it absolutely calls the Array methods, and there are only a few methods that can change the original Array. Can we rewrite these methods and extend their functionality without changing the original functionality, as in the following example:
Array.prototype.newPush = function(value) {
this.push(value)
console.log(The array has been modified)}Copy the code
The running result of the browser is as follows:
2.2 the interceptor
There are 7 methods to change their arrays: Push, POP, Shift,unshift, splice, sort, reverse The Array instance actually calls the methods implemented in our interceptor.
/ / source location: / SRC/core/observer/array. Js
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const arrayMap = [
'push'.'pop'.'shift'.'unshift'.'splice'.'reverse'.'sort'
]
arrayMap.forEach(function(method){
const origin = arrayPrototype[method];
Object.defineProperty(arrayMethods, method, {
configurable: true.enumerable: true.writable: true.value: function mutator(. args) {
const result = origin.apply(this, args)
returnresult; }})})Copy the code
For ease of understanding, the call flow is shown below:
Graph LR Array(Array) --> POP --> interceptor [arrayMethods] Array --> Push --> Shift --> Unshift --> Prototype interceptor --> Array --> splice --> Array --> sort --> Array --> reverse --> Array. Prototype
In the above code, we first create an empty interceptor object, arrayMethods, that inherits Array’s prototype object. Then we use defineProperty to encapsulate the 7 methods that change the original array one by one. When the array instance uses these 7 methods, it actually uses the method with the same name in our interceptor, in which Origin represents the original corresponding method, so that the method in our interception can extend some logic, such as notification update.
2.3 Using interceptors.
We defined the interceptor in the previous section, but we haven’t yet mounted it between the Array instance and the Array prototype object. Instead, we simply assign the __proto__ of the data to our interceptor arrayMethods when the data type is Array.
Observe the Observe class with the Array definition:
class Observer {
constructor(value) {
this.value = value;
// Add an '__ob__' attribute to value with the current Observe instance.
// Make it responsive. // Make it responsive
def(value,'__ob__'.this);
if(Array.isArray(value)) {
// The logic when val is array
const augment = hastProto ? protoToTarget : copyToSelf;
augment(value, arrayIntercepter, arrayIntercepterKeys)
} else {
// Object becomes testable logic
this.walk(value); }}// omit the previous code...
}
/** Checks whether the browser supports __proto__ */
export const hastProto = '__proto__' in {}
const arrayIntercepterKeys = Object.getOwnPropertyNames(arrayIntercepter)
/** Mount SRC prototype to target, that is, src.__proto__ = target */
function protoToTarget(src,target) {
src.__proto__ = target;
}
/ * * *@description: copies properties on the target to itself@param {object} src
* @param {object} target
* @param {string[]} keys* /
function copyToSelf(src, target, keys) {
for(let i =0 ; i < keys.length; i++) {
constkey = keys[i]; def(src, key, target[key]); }}Copy the code
In this code, we first check if the __proto__ attribute is supported by the browser. If so, we call protoToTarget and assign the __proto__ attribute to the arrayIntercepter we wrote earlier. If not, we call the copyToSelf method to copy the seven overridden methods defined above the interceptor into the value.
When the interceptor is in effect, when the Array has changed, we can notify the change in the interceptor, which means we know when the Array has changed, and we’re done detecting the change in the Array.
3. Array dependency collection
3.1 Where is the dependency collection for arrays
We know that the Observer class is done adding getters/setters to the data, so dependencies should also be collected in the Observer. The source code is completed as follows:
export class Observer {
constructor(value) {
this.value = value;
// Add an '__ob__' attribute to value with the current Observe instance.
// Make it responsive. // Make it responsive
def(value,'__ob__'.this);
// Instantiate a dependency manager to collect array dependencies
this.dep = new Dep();
if(Array.isArray(value)) {
// The logic when val is array
const augment = hastProto ? protoToTarget : copyToSelf;
augment(value, arrayIntercepter, arrayIntercepterKeys)
} else {
// Object becomes testable logic
this.walk(value); }}}Copy the code
The source code is an addition to the instancedep
Dependency manager to collect the dependencies of arrays.
3.2 How do I Collect Dependencies
In the previous section, we mentioned that array dependencies should be collected in the getter. In the Observe class, we added a dependency manager to collect array dependencies. How do we collect dependencies in the getter?
Let’s think: do we just need to get the DEP in the getter for an instance of the Observer class? How do we get an Observer instance?
The __ob__ attribute is used to store the value of the Observer instance itself. Let’s look at the implementation method in the source code.
function defineReactive(obj, key, val) {
/ /... Omit this part of the code...
const childOb = observer(val); // Get an Observer instance of val
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get() {
console.log(`${key}Property was read)
// Collect dependencies
dep.depend();
if(childOb) {
// The array's own dependencies are collected.
childOb.__ob__.dep.depend();
if(Array.isArray(value)) {
// Each item in the array is collected by dependencydependArray(value); }}return val;
}
/ /... Omit this part of the code...})})/ * * *@description Each dependency of the array is collected */
function dependArray(arrayData) {
if(!Array.isArray(arrayData)) {
return;
}
for(let i = 0; i < arrayData.length; i++) {
const e = arrayData[i];
e && e.__ob__ && e.__ob__.dep.depend();
if(Array.isArray(e)) { dependArray(e); }}}function isObject (obj) {
returnobj ! = =null && typeof obj === 'object'
}
/ * * *@description Returns an Observer instance of value, and if __ob__ is not present it becomes responsive * via the Observer@param {object} value
*/
export function observer(value) {
// If it is not an object or a virtual node
if(! isObject(value) || valueinstanceof VNode) {
return;
}
let ob;
if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value);
}
return ob;
}
Copy the code
In this code, we first try to determine whether the object passed in val has an __ob__ attribute. As we mentioned earlier, objects with this attribute are already responsive. If not, we call new Observer(val) to return responsive data and return the Observer instance. So we get the dependency manager for the instance, and then we do dependency collection in the getter, and then we recursively collect the dependencies for each item in the array.
The result of typeof arrays or null is Object
3.3 How do I notify dependency Updates
We mentioned earlier that we don’t issue setters to add or subtract data from an array, and there are seven ways to change the array itself, so we wrap the interceptor between the array instance and the array prototype object, and we should notify the dependencies in the method of the same name in the interceptor.
So how do you do that? To notify the dependency, you have to access the dependency first. We should get the Observer instance of the responsive data, the __ob__ property, and once we get it, we can access the dependency manager of the corresponding instance and call notify to notify the update. Here’s the code:
arrayMap.forEach(function(method){
const origin = arrayPrototype[method];
Object.defineProperty(arrayIntercepter, method, {
configurable: true.enumerable: true.writable: true.value: function mutator(. args) {
const result = origin.apply(this, args)
// Get the Observer instance
const ob = this.__ob__;
// Notifications depend on updates
ob.dep.notify();
returnresult; }})})Copy the code
Since the interceptor is mounted to the stereotype property, this represents the value of the data. Once you get the Observer instance, you can access the dependency manager DEP and update it via notify. At this point, change detection of the array is done.
4. Implement depth detection
What we have mentioned above is the detection of changes in the array itself, adding an element to the array or deleting an element can be detected. However, if the child elements of the array change, the above operation will not be detected. In Vue, both Object data and Array data are deeply detected, and the data of Object type has been recursively processed in defineReactive.
So how to implement depth detection for Array type data? Some of you might think, well, let’s just go through everything and go through The New Observer. Right, that’s the core idea. Let’s look at the code:
Tips: Deep detection is to detect not only changes in itself, but also changes in the data of each child element.
export class Observer {
constructor(value) {
this.value = value;
def(value,'__ob__'.this);
this.dep = new Dep();
if(Array.isArray(value)) {
const augment = hastProto ? protoToTarget : copyToSelf;
augment(value, arrayIntercepter, arrayIntercepterKeys)
// Convert each item of the array to detectable responsive data
this.observeArray(value);
} else {
this.walk(value); }}/ * * *@description: turns each item of the array into responsive data *@param {Array} items* /
observeArray (items) {
for(let i = 0; i < items.length; i++) { observer(items[i]); }}/ /... Omit other definition code
}
/ * * *@description Returns an Observer instance of value, or reactive * via Observer if ob does not have one@param {Observer} value
*/
export function observer(value) {
if(! isObject(value) || valueinstanceof VNode) {
return;
}
let ob;
if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value);
}
return ob;
}
Copy the code
The code above calls each item of the observerArray recursive array, and then calls an Observer to turn non-responsive child elements into responsive data.
5. Change detection of new elements.
We have already done in-depth inspection for arrays, but there is a problem with this logic. When we add an element, the element is not reactive, and we need to convert the element to be reactive.
There are three ways to add elements to an array: push,unshift, and splice. When we call these three methods, we add logic to the interceptor to convert the elements into responsive data. Let’s go straight to the code:
arrayMap.forEach(function(method){
const origin = arrayPrototype[method];
def(arrayIntercepter, method, function mutator(. args) {
const result = origin.apply(this, args)
// Get the Observer instance
const ob = this.__ob__;
let inserted;
switch (method) {
case 'push':
case 'unshift': {
// For push and unshift, the first term is the element to be inserted
inserted = args;
break;
}
case 'splice': {
// splice the second argument is the element to be inserted
inserted = args.splice(2); }}// If the inserted element exists, call the observe function to turn the new element into responsive data
if(inserted) ob.observeArray(inserted);
// Notifications depend on updates
ob.dep.notify();
returnresult; })})Copy the code
Tips: INSERTED is an array, because the observeArray argument is an array.
6. Deficiencies
The above changes to arrays are all based on prototype object interception, but another method we use everyday is to use subscripts to access and manipulate arrays. This causes a problem. If you use subscripts to manipulate arrays, then change detection will not be detected, as shown in the following code:
let array = [1.2.3]
array[0] = 5;
array.length = 0;
Copy the code
Of course, Vue is aware of this and has added two global apis to solve this problem, vue. set and vue. delete. The implementation of these two apis will be analyzed separately in the global API analysis section, but not explained here.
7. To summarize
In this chapter, we can clearly know that data access is easily sensed by getters, but data changes are not known. So we are aware of the array changes by rewriting 7 methods of the array changes through the interceptor. Second, our dependence on the array collection and inform rely on update has carried on the deep analysis, we know that the Vue not only to make the change detection array itself, also the son of the array elements and the elements of new change detection logic, we also analyzed the implementation principle, expounds the Vue to everybody from shallow to deep for data core idea and the basic principle of the change detection. Here’s a mind map.
4. Conclusion
4.1 The author says
If we find that the article is not reasonable and wrong to welcome criticism and correction; If you do not understand the place can also consult the author, do not feel ashamed, I will know all.
There are no shortcuts to becoming a bigshot, only love and constant learning.