监听器的初始化也是在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)
}
}
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)
}
}
}
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
}
}
})
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)
}
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()
}
}
}
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)
}
)
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()
}
// ......
}
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
}
}
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
}
// ......
}
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)
}
}
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)
}
}
}
})
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)
}
}
// ......
}
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)
}
}
}
})
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)
}
}
}
}
// ......
}
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
的第一个和第二个参数
至此,监听器部分则全部分析完毕