Vue Source Code Learning (4) Watcher Principle Analysis

Introduction

With the reading of the vue source code, I gradually found that Watcher is everywhere. Whether it is the principle of responsiveness, or the calculation of properties, the listening properties all use Watcher, and almost most of the features of Vue are inseparable from Watcher.

It even gives me a feeling that Vue is going big, that is, how to establish the relationship between data and Watcher, how to trigger Watcher updates when data changes, and how to update Watcher.value.

How this is updated determines what the Watcher is. If it is an updated view, it is a rendered Watcher, and if it is a computeWatcher, it is an updated calculation property.

Source code and comments

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
/* @flow */

import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError,
noop
} from '../util/index'

import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import type { SimpleSet } from '../util/index'

let uid = 0

/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
Vm: Component;//the vm instance where the Watcher is located
Expression: string;//used to evaluate the value property of the Watcher, which can be a function or an expression
Cb: Function;//callback function after each re-evaluation (value)
Id: number;//Watcher id, unique identifier
Deep: boolean;//whether to detect changes in depth
user: boolean;
Lazy: boolean;//Whether to perform a job search when the constructor function is executed, that is, to calculate the value value once through experssion
sync: boolean;
Dirty: boolean;//used with lazy
active: boolean;
Deps: Array < Dep >;//The dependencies of the current watcher are continuously increased by addDeps, but each time the value is recalculated, the dependencies that are not in newDepIds will be unregistered
newDeps: Array < Dep >;//increase each time addDeps, recalculate and finally clear
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
Getter: Function;//convert experssion to function
Value: any;//The value of the current watcher, for computed, is the calculation result, for rendered watcher, it is the rendering result

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
//This code shows that the _watcher on each vue instance is actually the rendering watcher
if (isRenderWatcher) {
vm._watcher = this
}
//The _watchers on the vue instance is all the watchers of the current instance, the first one is generally the rendering watcher
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV ! 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn = 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV ! 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
//This step is to determine whether it is lazy or not. If it is, it will not be evaluated for the time being. The calculation property is true here, and the rendering watcher is false here.
this.value = this.lazy
? undefined
: this.get()
}

/**
* Evaluate the getter, and re-collect dependencies.
*/
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
}

/**
* Add a dependency to this directive.
*/
//Collect dependencies to newDeps and record the id. Each time the value of this newDeps is recalculated, it will be assigned to dep and then cleared
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 () {
//According to newDeps to determine whether you still need to subscribe to the original dep, if not, unsubscribe through removeSub
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
//clear newDeps after setting deps to newDeps
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
}

/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
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.
*/
//currently active state,
run () {
if (this.active) {
//recalculate the value
const value = this.get()
//The newly calculated value is different from the old value (this judgment is mainly to avoid that the value dependent on the calculated attribute has changed but the calculation result has not changed)
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)
}
}
}
}

/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}

/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}

/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}

Dep

In general, watcher and dep are used together, so the source code of dep released here can be verified with the watcher source code above

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
66
67
/* @flow */

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
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) {
Dep.target.addDep(this)
}
}

notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV ! 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}

export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

Application

The Reactive Principle (renderWatcher)

You can see my other article for this.关于响应式原理的博客

Principles of Computational Attributes (computeWatcher)

initState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData (vm)//initialize data
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch ! nativeWatch) {
initWatch(vm, opts.watch)
}
}

It can be seen from this code that the reactive processing’observe (vm._data = {}, true/* asRootData */) 'is performed before initComputed, because the calculation property is also used by watcher, and it is also necessary to define reactive data for dependency collection.

In this way, it seems that the dependency collection of the calculated property is a simplified version of the response. The dependency collection in the principle of response is to call the getter of the data when rendering, and the calculated property is that the function itself calls the getter of the dependency number.

initComputed

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()

for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef = 'function' ? userDef : userDef.get
if (process.env.NODE_ENV ! 'production' && getter null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}

if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}

// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV ! 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}

export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef = 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache ! false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV ! 'production' &&
sharedPropertyDefinition.set = noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}

This code is very simple. In fact, first define a watcher for each calculation property. Since’lazy 'is true, it will not be evaluated in the constructor function.

In defineComputed, the calculation property is mounted on the vm, and its getter returns the value of the watcher.

If dirty is true, you need to call’watcher.evaluate () ‘to recalculate watcher.value,’ dirty 'will be set to true after each update, and will be set to false after recalculation, and this calculation process will call the get method back, will first putshTarget, and then execute the function that calculates the properties, so that computedWatcher will subscribe to the changes in the data used in the function.

At the same time, its getter will also collect dependencies. When the Dep.target is not empty, it means that there is currently a rendering watcher under construction, so it is necessary to add its own watcher to the watchers of the current vue. In fact, this step is not to let the rendering watcher Subscribe to the changes of the computeWatcher, but to subscribe to the changes of the data that the computeWatcher relies on.

Listening attribute (userWatcher)

The process of userWatcher is relatively simple compared to the above two. After understanding the above two, the last one is easy to understand

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
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}

function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler = 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}

This step is actually to create a watcher for each value in the watcher.

The definition of’vm. $watcher 'is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}

This code can be seen that it will set the user of this watcher to true, which means that this watcher is userWatcher.

If immediate is true, the callback function will be executed immediately, which is the function we defined in the watcher.

The next logic is to create a new watcher, here we go back to the definition of watcher.

One thing to note is that when we define watcher, we usually use Attribute - Value Pair. In this case, expOrFn is the key name. At this time, Watcher will use’paresPath 'to find the corresponding data according to the path. At this time, watcher.getter is actually a function that returns the corresponding data.