监听器的初始化也是在initState
方法中,在计算属性的初始化之后,在src/core/instance/state.js
文件中可查看
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options // ...... if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
Copied!
2
3
4
5
6
7
8
9
10
初始化监听器会调用initWatch
方法,和initState
方法定义在同一文件中
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) } } }
Copied!
2
3
4
5
6
7
8
9
10
11
12
在参数合并 (opens new window)小节对watch
的合并做了分析,如果存在parentVal
的情况下,合并后watch
中的每项都会是一个数组,相同名称的监听器会合并为一个数组,所以在initWatch
方法内会有一个数组的判断,接着会调用createWatcher
方法
在分析之前,可借助如下例子逐步带入分析
const vm = new Vue({ el: '#app', template: `<div id="app"></div>`, data: { age: 30, name: 'zhangsan', detailInfo: { addr: '四川省巴中市', phone: '7799139' } }, watch: { age (val, oldVal) { console.log(val, oldVal) }, detailInfo: { handler (val, oldVal) { console.log(val, oldVal) }, deep: true } } })
Copied!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
createWatcher
和initWatch
方法在同一文件中定义
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) }
Copied!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在createWatcher
方法内部会对handler
做对象和字符串的判断以便获取真正的监听器函数,需要注意的是handler
如果为字符串,则会将methods
中的同名方法作为监听器的handler
,最后会调用vm.$watch
方法
vm.$watch
在stateMixin
方法内部定义,该方法和initState
方法定义在同一文件中
export function stateMixin (Vue: Class<Component>) { // flow somehow has problems with directly declared definition object // when using Object.defineProperty, so we have to procedurally build up // the object here. // ...... 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) { const info = `callback for immediate watcher "${watcher.expression}"` pushTarget() invokeWithErrorHandling(cb, vm, [watcher.value], vm, info) popTarget() } return function unwatchFn () { watcher.teardown() } } }
Copied!
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
vm.$watch
方法逻辑较简单,在方法内部对cb
也做了一层对象判断,因为$watch
方法可以直接调用,参数cb
可以是对象也可以是函数,options
是监听器选项,且默认存在user
选项,在方法内部为每个监听器创建一个监听器实例,需要注意的是expOrFn
可以是一个函数也可以是一个字符串,例如:
vm.$watch( function () { return this.name }, function (val, oldVal) { console.log(val, oldVal) } )
Copied!
2
3
4
5
6
继续回到Watcher
部分
export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } 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.value = this.lazy ? undefined : this.get() } // ...... }
Copied!
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
在实例化监听器watcher
的过程中,会对expOrFn
做类型判断,如果为函数类型则直接赋值给this.getter
,如果为字符串类型,则会调用parsePath
方法,该方法会返回一个函数,该方法定义在src/core/util/lang.js
中
export function parsePath (path: string): any { 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 } }
Copied!
2
3
4
5
6
7
8
9
10
11
12
13
14
接着会调用this.get
方法
export default class Watcher { // ...... 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 } // ...... }
Copied!
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
会将当前监听器watcher
赋值给Dep.target
,执行this.getter
方法将监听数据的值赋给this.value
,收集监听器watcher
的依赖,接着就是判断该监听器watcher
是否有深度监听选项(this.deep
),如果有深度监听则会调用traverse
方法,该方法在一个单独的文件中定义src/core/observer/traverse.js
const seenObjects = new Set() /** * Recursively traverse an object to evoke all converted * getters, so that every nested property inside the object * is collected as a "deep" dependency. */ export function traverse (val: any) { _traverse(val, seenObjects) seenObjects.clear() } function _traverse (val: any, seen: SimpleSet) { let i, keys const isA = Array.isArray(val) if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } if (val.__ob__) { const depId = val.__ob__.dep.id if (seen.has(depId)) { return } seen.add(depId) } if (isA) { i = val.length while (i--) _traverse(val[i], seen) } else { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) } }
Copied!
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
看过Vue
文档的同学都会知道,当监听一个对象,并且修改监听对象的某个键对应的值的时候,如果希望能够被监听器捕获到,这时需要深度监听,traverse
方法在内部调用_traverse
,在该方法内部会对被监听的数据做一层判断,如果被监听的数据val
是一些简单类型的值,或者是被冻结的对象,或者为vnode
会直接return
,如果被监听的数据val
为数组或对象的时候,会循环每一项的值并递归调用_traverse
方法,这样便会触发被监听数据的每一项的Getter
方法,进而实使得该监听器watcher
对遍历的每一项进行依赖收集,如果不深度监听,监听器watcher
只会依赖被监听的数据
_traverse
方法内部有个小的优化点值得关注下,在调用该方法之前有一个对监听数据的depId
收集的过程,做了去重处理,这样可以避免重复依赖
例如:
const Person = { name: '张三' } new Vue({ data: { a: { b: Person, c: Person } }, watch: { a: { deep: true, handler: function (val, newVal) { console.log(val, newVal) } } } })
Copied!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在上面例子中,对this.a
进行深度监听,在递归调用_tarverse
方法时会进行depId
的收集,由于this.a.b
和this.a.c
都是引用的同一对象,所以不会重复收集,第二次调用会直接return
继续回到get
方法的逻辑,依赖收集结束后,接着会将Dep.target
重置为上一状态、依赖清除
当监听器watcher
依赖的数据发生变化时,会循环调用依赖数据订阅的每个watcher
的update
方法
export default class Watcher { // ...... update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } // ...... }
Copied!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
当定义的监听器为同步监听器时,会直接调用this.run
方法,例如:
new Vue({ data: { name: '张三' }, watch: { name: { sync: true, handler: function (val, newVal) { console.log(val, newVal) } } } })
Copied!
2
3
4
5
6
7
8
9
10
11
12
13
上述例子就是同步监听器,一般默认的监听器都是异步的,则会调用queueWatcher
方法,异步更新,最后还是会执行this.run
方法,queueWatcher
方法在派发更新小节已经讲解过,这里就不多说明,直接看this.run
方法的定义
export default class Watcher { // ...... 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) { const info = `callback for watcher "${this.expression}"` invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info) } else { this.cb.call(this.vm, value, oldValue) } } } } // ...... }
Copied!
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
在派发更新小节也有对this.run
方法的说明,该方法会在内部执行this.get
方法,获取被监听数据变化后的值,如果变化前的值和变化后的值不相同或者被监听的数据为一个对象,或者为深度监听,则将新旧值做一个交换,因为监听器watcher
默认会存在user
选项,所以会进入true
分支执行监听器函数this.cb
,value
和oldValue
会分别为调用监听器函数this.cb
的第一个和第二个参数
至此,监听器部分则全部分析完毕
v1.4.16