Vue Source Code Learning (1) Responsive Principle

Vue is an MVVM framework, and the most appealing thing about it is its responsiveness.

Official explanation

First, let’s take a look at the explanation of the reactive principle in the official doc.

How to track change

When you pass a plain JavaScript object to a Vue instance as the’data 'option, Vue will iterate over all the properties of the object and use Object.defineProperty Convert all these properties to getter/setterObject.defineProperty is a non-shim feature in ES5, which is why Vue does not support IE8 and earlier browsers.

These getters/setters are invisible to the user, but internally they allow Vue to track dependencies and notify changes when properties are accessed and modified. It should be noted here that different browsers format getters/setters differently when printing data objects in the Console, so it is recommended to install vue-devtools To get a more user interface friendly to inspection data.

Each component instance corresponds to a watcher instance, which records the data properties “touched” as dependencies during the component rendering process. Later, when the setter of the dependency is triggered, the watcher will be notified so that its associated component can be re-rendered.

The process of the above figure is roughly like this. When a component is instantiated, it will first define getters and setters for all the data in the data, and then trigger the render function. In the process of rendering into virtual dom, it will trigger the rendering process. The getters of those data will be added to the Watcher in the getter. This step is called dependency collection. When a certain dependent data changes, the Watcher will be notified to re-trigger the render function to re-render the page.

Considerations for detecting changes

Due to JavaScript limitations, Vue ** cannot detect changes in ** arrays and objects. However, we still have some ways to circumvent these limitations and ensure their responsiveness.

Vue cannot detect the addition or removal of properties. Since Vue performs getter/setter conversions on properties when the instance is initialized, the property must exist on the’data 'object for Vue to convert it to reactive. For example:

1
2
3
4
5
6
7
8
9
10
var vm = new Vue({
data:{
a:1
}
})

//'vm.a' is responsive

vm.b = 2
//'vm.b' is not responsive

For already created instances, Vue does not allow dynamic addition of root-level reactive properties. However, you can use the Vue.set (object, propertyName, value) method to add reactive properties to nested objects. For example, for:

1
Vue.set(vm.someObject, 'b', 2)

You can also use the vm. $set instance method, which is also an alias for the global Vue.set method:

1
this.$set(this.someObject,'b',2)

Sometimes you may need to assign multiple new properties to an existing object, such as using Object.assign () or _ (). However, the new property added to the object in this way will not trigger an update. In this case, you should create a new object with the original object and the property of the object you want to blend in.

1
2
// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

For an array

Vue cannot detect changes in the following arrays:

  1. When you use an index to set an array item directly, for example: ‘vm.items [indexOfItem] = newValue’
  2. When you modify the length of the array, for example: ‘vm.items.length = newLength’

For example:

1
2
3
4
5
6
7
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
Vm.items [1] = 'x'//not responsive
Vm.items.length = 2//not responsive

In order to solve the first type of problem, the following two methods can achieve the same effect as’vm.items [indexOfItem] = newValue ', and also trigger status updates in a responsive system:

1
2
3
4
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

You can also use vm.$set Example method, which is an alias for the global method Vue.set:

1
vm.$set(vm.items, indexOfItem, newValue)

To solve the second type of problem, you can use’splice ':

1
vm.items.splice(newLength)

Source code parsing

1
2
3
4
5
6
7
8
9
10
11
new Vue({
el: '#example',
data(){
return{
obj:{
a:1
}
}
},
})

When we write this line of code, vue performs dependency tracking on the obj object we defined in data.

By implementing the new Observer (obj)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//After the above code, our obj object will look like the following
{
obj:{
a:1,
_ _ ob __:{ // Observer instance
Dep: {Dep instance
Subs: [//store Watcher instance
new Watcher(),
new Watcher(),
new Watcher(),
new Watcher(),
]
}
}
}
}

Observe

defineReactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function defineReactive(obj, key, val) {
observe(val)

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get');
const ob = this.__ob__
ob.dep.depend();
return val
},
set: function reactiveSetter(newVal) {
console.log('set');
if (newVal = val) return
val = newVal
observe(newVal)
const ob = this.__ob__
ob.dep.notify();
},

})
}

Observe function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
/*
Attempts to create an Observer instance (__ob__), returns a new Observer instance if it is successfully created, and returns an existing Observer instance if it already exists.
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
/* Determine if it is an object */
if (!isObject(value)) {
return
}
let ob: Observer | void

/* This property __ob__ used here to determine whether there is already an Observer instance. If there is no Observer instance, a new Observer instance will be created and assigned to the __ob__ property. If there is an Observer instance, the Observer instance will be returned directly */
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (

/* The judgment here is to ensure that value is a simple object, not a function or Regexp, etc. */
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {

/* Count if it is root data, the observed asRootData in the Observer is not true */
ob.vmCount++
}
return ob
}

Observer类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* Observer class that are attached to each observed
* object. Once attached, the observer converts target
* object's property keys into getter/setters that
* collect dependencies and dispatches updates.
*/
export class {
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()
this.vmCount = 0

/*
Bind the Observer instance to the __ob__ attribute of the data. As mentioned before, when observing, it will first detect whether there is __ob__ object to store the Observer instance. The def method definition can refer to https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16
*/
def(value, '__ob__', this)
if (Array.isArray(value)) {

/*
If it is an array, replace the original method in the prototype of the array with the array method that can intercept the response after modification to achieve the effect of listening to the change response of the array data.
Here, if the current browser supports the __proto__ property, the native array method on the current array object prototype is directly overwritten, and if the property is not supported, the prototype of the array object is directly overwritten.
*/
const augment = hasProto
? protoAugment/* directly overrides the prototype method to modify the target object */
: CopyAugment/* Define (overwrite) a method of the target object or array */
augment(value, arrayMethods, arrayKeys)
/*Github:https://github.com/answershuto*/
/* If it is an array, you need to traverse each member of the array to observe */
this.observeArray(value)
} else {

/* If it is an object, walk directly to bind */
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)

/* The walk method will traverse every property of the object for defineReactive binding */
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>) {

/* The array needs to traverse each member to observe */
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

This involves Dep, and we also implement Dep.

Dep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Dep {
constructor() {
this.subs = []
}

addSub(sub) {
this.subs.push(sub)
}

depend() {
this.subs.push(Dep.target)
}

notify() {
for (let i = 0; i < this.subs.length; i++) {
this.subs[i].fn()
}
}
}

Dep.target = null

The Observer class mainly does the following

  1. Traverse each property under data. If it is an object, execute new Observer () and add the ob property on the object, the value of which is an instance of Observer
  2. Hijack the change of object properties. When getting the getter, get the dep instance of the Observer instance and execute dep.depend (). The code is as follows
1
2
const ob = this.__ob__
ob.dep.depend();

Take a look at what dep.depend () does

1
this.subs.push(Dep.target)

Add Dep.target to the subscription array, this.subs

That is, as long as we

1
2
3
Dep.target =function test(){}
dep.depend()
//test function is a subscriber of Dep

Watcher

The implementation of this watcher is a simplified version, and even some of the logic defined in other functions is directly placed here, just for better understanding.

For a detailed explanation of Watcher, see my other blogVue的数据驱动原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Dep = require('./Dep')

class Watcher {
constructor(vm, exp, fn) {
this.vm = vm
this.exp = exp
this.fn = fn
Dep.target = this//mounts itself to the Dep.target, which is read when Dep.depend is called
This.vm [exp]//The source code here actually calls the mount function, indirectly calls the render function, and uses the data when rendering.
}
}

module.exports = Watcher

Understanding’Watcher 'from a small example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {
a: 1,
b: {
c: 2
}
}

new Observer(obj)
new Watcher(obj, 'a', () => {
Console.log ('Watcher callback execution')
})
obj.a='222'


The process is as follows:

  1. Observe the obj object first ('new Observer (obj) ')
  2. When instantiating’Watcher ‘, it will execute’ Dep.target = this’, and then execute’this.vm [exp] ', that is, take the value once, then it will trigger the getter and add itself (Watcher instance) to the subscriber array of dep
1
2
3
4
5
6
get: function reactiveGetter() {
const ob = this.__ob__
ob.dep.depend();
return val
},

Finally, when changing the data, trigger’setter’

1
2
3
4
5
6
7
8
set: function reactiveSetter(newVal) {
if (newVal = val) return
val = newVal
observe(newVal)
const ob = this.__ob__
ob.dep.notify();
},

Execute’ob.dep.notify () ’

1
2
3
4
5
notify() {
for (let i = 0; i < this.subs.length; i++) {
this.subs[i].fn()
}

Traverse, the subscriber (subs) executes the callback function, and the whole process ends

Dependency collection process

  • add getters and setters to each property in the data via the observer function
  • Add a new Watcher, Watcher construction process, will first assign the Dep.target to itself, and then manually call the getter again, so that you can trigger the getter to add Dep.target to the subs logic
  • Once a property is modified, it will trigger notify in the setter to notify all added subs (that is, all Watcher instances) to call the update function.

Dependency logout process

I don’t know if you have thought about it, the above process is about how vue collects dependencies, how to trigger the update of the rendering watcher when the dependency changes, but we also know that due to the existence of v-if and the like, when the condition is true, we may collect some dependencies, but when the condition is false, these dependency changes should not trigger re-rendering, then these dependencies should be logged off.

So how does Vue do it?

We already know that when the data changes, 'dep.notify () ’ will be triggered, which will trigger’update 'of all watchers subscribed to the dep.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value ! this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

The’get 'method will be called again here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

The last step, ‘cleanupDeps’, is actually used to remove invalid dependencies. It is actually used in conjunction with’addDep ’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}

Each time a dependency is collected, the id of the dep will be saved to the newDepIds array. After the update is triggered, all the dependencies (this.deps) from the last update will be searched. If it is no longer in newDepIds, the dep will be removed

Vue3 Responsive Upgrade

Vue3 reuses ES6’s Proxy syntax to update the response, which can better handle the array update mentioned above.

https://sunra.top/posts/e5782665/

Reference article:

https://cn.vuejs.org/v2/guide/reactivity.html

https://github.com/answershuto/learnVue/blob/master/docs/%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86.MarkDown

https://github.com/answershuto/learnVue/blob/master/docs/%E4%BE%9D%E8%B5%96%E6%94%B6%E9%9B%86.MarkDown

https://juejin.im/post/6844903901003513863