Simple – VUE change detection principle
I actually wrote an article about the vUE responsive principle a year ago, but RECENTLY I opened it up and found that the content was a little different from what I was thinking, so I decided to write a new article that is more accessible
My goal is to enable readers to learn knowledge after reading my articles. The titles of some articles begin with a simple and profound statement, which aims to remove the factors that interfere with learning from a complex thing and make readers learn knowledge through simple descriptions of the remaining core principles
The internal principles of VUE actually have many branches, one for change detection, one for template compilation, one for virtualDOM, and one for the overall running process. There are about four parts
Today I’m going to focus on change detection in a separate section
How do I detect changes?
Object defineProperty and ES6 proxy can be used to detect changes in objects. Object defineProperty and ES6 proxy can be used to detect changes
So far, Vue has used Object.defineProperty, so let’s use Object.defineProperty as an example to illustrate how this works.
Here I want to say is, no matter later vue will use a proxy to rewrite this part, I speak of is a principle, not API, so no matter how vue will change after, this principle will not change, even if the vue with other completely different principle to achieve the change detection, but this article speak principle can implement change detection, The principle is never out of date
Before I like to write articles has one problem is the source of translation, the result after half a year’s source code, I write articles were not worth a bean, but also has a drawback to the source code translation is a little stiff to the requirement of readers, readers if did not see the source code or watch and I’m not a version, so don’t know what I’m saying
Good don’t talk nonsense, continue to talk about the content just now
Knowing that Object.defineProperty can detect Object changes, we can instantly write code like this:
function defineReactive (data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
return val
},
set: function (newVal) {
if(val === newVal){
return
}
val = newVal
}
})
}Copy the code
DefineProperty: Write a function that encapsulates Object.defineProperty. Because object.defineProperty is so complicated to use, all I need to do is pass a data and a key and val
Now that the package is wrapped, get can be triggered whenever the data key is read, and set can be triggered whenever the data is set, but ,,,,,,,,,,,,,,,,,, does not seem to be useful
How do you observe that?
Now I’m going to ask the second question, “How?”
Think about it. The reason we want to look at a piece of data is so that when the properties of the data change, we can notify those places where the key is used
To take one:
<template>
<div>{{ key }}</div>
<p>{{ key }}</p>
</template>Copy the code
There are two places in the template where keys are used, so you want to notify both places when data changes
So my answer to the above question is to collect dependencies first, collect them where they are used by key, and then trigger the dependency loop once when the property changes
So it’s really just one sentence, getter, collect dependencies, setter, fire dependencies
Where is the dependency collection?
Now that we have a clear goal of collecting dependencies in getters, where does our dependency collection go?
If you think about it, the first thing to think about is that each key has an array to store the current key’s dependencies. Suppose the dependencies are a function on window.target
Function defineReactive (data, key, val) {let dep = [] // Add object.defineProperty (data, key, {enumerable: Function () {dep.push(64x) {// New return val}, 64x: disables and controls any additional information. Function (newVal) {if(val === newVal){return} for (let I = 0; i < dep.length; i++) { dep[i](newVal, val) } val = newVal } }) }Copy the code
An array DEP was added in defineReactive to store collected dependencies and then loop through the DEP to fire the collected dependencies when the setter fires. But this is a bit coupled, so we wrap up the dependency collection part of the code and write it like this:
export default class Dep { static target: ? Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { this.addSub(Dep.target) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }Copy the code
Then modify defineReactive:
Function defineReactive (data, key, val) {let dep = new dep () // Modify Object.defineProperty(data, key, {enumerable: Function () {p. Depend () {// Modify return val}, set: disables and controls any additional information. Function (newVal) {if(val === newVal){return} dep.notify() // add val = newVal}})}Copy the code
This time the code looks much clearer. By the way, to answer the question above, where do dependencies get collected? Collected into the Dep, which is specifically used to store dependencies
Who collect?
Target is a dependency that needs to be collected. If you have noticed that the code window.target has been changed to dep. target, what is dep. target? Who exactly are we going to collect?
Who to collect, in other words, who to notify when a property changes.
We will inform the use of the data, and use this place has a lot of data, and the type is not the same, it is possible that the template, it is possible that the user write a watch, so this time we need to abstract out a can focus on the different situation of class, and then we collect phase only depends on the good class instances come in, And then it’s responsible for notifying the rest of the world, so we’re going to abstract this thing and we’re going to have to call it watcher
So now you can answer the above question, collect who? Collect Watcher
What is a Watcher?
Watcher is an intermediary, notifying Watcher of changes in data, and then Notifying watcher elsewhere
Let’s look at a classic use of Watcher:
// keypath
vm.$watch('a.b.c', function (newVal, oldVal) {
// do something
})Copy the code
This code indicates that when the data.a.b.c property changes, the function with the second parameter is triggered
Think about how do you implement this feature?
It looks like you just need to add the Watcher instance to the Dep of the data.a.b.c property, and then when data.a.b.c fires, Watcher is notified and watcher executes the callback function in the parameter
Ok, think it over, start, write the following code
class Watch { constructor (expOrFn, Getter = parsePath(expOrFn) this.cb = cb this.value = this.get()} get () { Dep.target = this value = this.getter.call(vm, vm) Dep.target = undefined } update () { const oldValue = this.value this.value = this.get() this.cb.call(this.vm, this.value, oldValue) } }Copy the code
This code can actively push itself into the Dep of data.a.B.c
Traget is set to this, which is the current watcher instance, and then read the value of data.a.b.c.
Since the data.a.b.c value is read, the getter must be triggered.
The defineReactive function we encapsulated above the getter has a piece of logic that reads a dependency push from the dep. target into the Dep.
So as a result, I just assign this to dep. target, and then I read the value and trigger the getter, and I can actively push this into the keypath dependency
After dependency injection into the Dep, when the data.a.B.c value changes, all dependency loops trigger the update method, which is the update method in the code above.
The update method triggers the callback function in the parameter, passing value and oldValue to the parameter
$watch(‘ A.B.C ‘, (value, oldValue) => {})
Recursively detects all keys
It’s already possible to detect changes, but the code we wrote could only detect one key in the data, so we’ll work with defineReactive
// add function walk (obj: Object) {const keys = object.keys (obj) for (let I = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } function defineReactive (data, key, Var) {new dep = new dep () Object. DefineProperty (data, key, {enumerable: true, different: true, get: function () { dep.depend() return val }, set: function (newVal) { if(val === newVal){ return } dep.notify() val = newVal } }) }Copy the code
If the value in the key is an object, all the keys in the object will also be detected
How does Array detect changes?
Not all values in data are objects and primitive types. What if data is an array? There is no way for arrays to detect behavior through Object.defineProperty
The solution to this array problem in VUE is pretty straightforward. Let me tell you how VUE is implemented, basically in three steps
Step 1: Inherit the prototypical methods of the native Array
Step 2: Do some intercepting on the inherited Object using Object.defineProperty
Step 3: Assign the processed prototype that can be intercepted to the prototype of Array type data that needs to be intercepted
The realization of the vue
The first step:
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)Copy the code
The second step:
; [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // cache original method const original = arrayProto[method] Object.defineProperty(arrayMethods, method, { value: function mutator (... Args) {console.log(methods) return original. Apply (this, args)}, Enumerable: false, writable: true, configurable: true }) })Copy the code
Now you can see that every time the detected array execution method operates on an array, I can know what method it executed and print it to console
Now I need to check the array method type. If the array method is push unshift splice, I need to change the new element with the wrapped walk
And no matter what method I’m using on the array, I’m going to fire a message that tells me that the dependency data in the dependency list has changed, right
So how do we access the dependency list now, maybe we need to work on the wrapped walk
Function def (obj: Object, key: string, val: any, enumerable? : boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !! enumerable, writable: true, configurable: true }) } export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: Any) {this.value = value this.dep = new dep () def(value, '__ob__', If (array.isarray (value)) {this.observearray (value)} else {this.walk(value)}} /** * walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } /** * Observe a list of Array items. */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { new Observer(items[i]) } } }Copy the code
We define an Observer class that converts data to data that can be detected, and we add a type check. If value is of type Array, Array throws each element into the Observer
Value has an __ob__ flag so that we can get an Observer instance from value’s __ob__ and use dep.notify() on __ob__ to send the notification
Then we worked on the interceptor for the Array prototype
; [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // cache original method const original = arrayProto[method] def(arrayMethods, method, function mutator (... args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result })})Copy the code
You can see that there’s a switch that says method, if it’s push, unshift, Splice, a way to add array elements, uses ob.observearray (INSERTED) to throw the new element into an Observer as well, converting it into data that can be detected
Ob.dep.notify () is called to notify Watcher that the data has changed, regardless of the method used to manipulate the array. Okay
How does arrayMethods work?
Now we have an arrayMenthods that is array. prototype after processing. How do we apply this object to an Array?
Think about it: we can’t change Array.prototype directly because that would pollute the global Array. We want arrayMenthods to only work on arrays in Data
So we just assign arrayMenthods to value’s __proto__
Let’s revamp the Observer
export class Observer { constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', This) if (array.isarray (value)) {value.__proto__ = arrayMethods // Add this.observearray (value)} else { this.walk(value) } } }Copy the code
If you can’t use __proto__, just loop through arrayMethods and load those methods directly onto value.
Under what circumstances can not be used__proto__
I don’t know, but who knows if you can leave me a message? Thanks ~
So our code needs to be changed again
// can we use __proto__? Const hasProto = '__proto__' in {} // Add export class Observer {constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', This) if (array.isarray (value)) {// modify const augment = hasProto? ProtoAugment: copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } } } function protoAugment (target, src: Object, keys: any) { target.__proto__ = src } function copyAugment (target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }Copy the code
Questions about Array
Vue intercepts Array operations that cannot be intercepted by vue. For example:
this.list[0] = 2Copy the code
Changing the value of the first element of the array does not detect the array change, so it does not trigger re-render or watch, etc
In such as:
this.list.length = 0Copy the code
The empty array operation does not detect changes in the array, so it does not trigger re-render or watch, etc
ES6 has the ability to do this. Before ES6, it was impossible to simulate the native behavior of arrays. Now ES6 proxies can simulate the native behavior of arrays. It is also possible to intercept arrays by inheriting their native behavior through ES6 inheritance
conclusion
Finally, I took out a diagram on the official website of VUE. This diagram is actually very clear, which is a schematic diagram of change detection
And there’s a line between the getter and the watcher, and it says collect dependencies, which means that when you get the getter you collect the setter, and there’s a line between the getter and the watcher that says Notify which means that when the data fires the setter, Notify Watcher watcher that there is a line to ComponentRenderFunction that says Trigger re-render