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