July 23, 2023

Vue2源码(6):侦听器原理

watch的源码原理属于比较简单清晰的了

当然要理解还需要配合前面几篇:依赖收集、派发更新、数据劫持

1 watch的入口

源码位置:vue-main\src\core\instance\state.ts

initState() 方法中,判断 opts.watch 如果存在,则执行 initWatch 进行监听器初始化

2 创建 user Watcher

首先 Vue 中定义了 3 个 watcher

user watcher 就是侦听器创建的,下面我们来看看是怎样实现的

2.1 watch的写法

首先,我们要了解 watch 的几种写法

数组写法:

1
2
3
4
5
6
7
8
9
10
watch: {
firstname: [
(newValue,oldValue) => {
console.log(newValue, oldValue)
},
(newValue,oldValue) => {
console.log(newValue, oldValue)
}
]
},

字符串写法:

1
2
3
4
5
watch: {
firstname(newValue,oldValue){
console.log(newValue, oldValue)
}
},

2.2 initWatch 初始化

这里用来判断 watch侦听器 的类型

看看源码:

1
2
3
4
5
6
7
8
9
10
11
12
function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}

2.3 创建 Watcher

第一步:

createWatcher() 方法中源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createWatcher(
vm: Component,
expOrFn: string | (() => any),
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 回调是字符串的情况
if (typeof handler === 'string') {
handler = vm[handler]
}
// 去创建 Watcher
return vm.$watch(expOrFn, handler, options)
}

注:回调是字符串的情况

1
2
3
4
5
6
7
8
methods: {
myFn(newValue, oldValue){
console.log("changed")
}
}
watch: {
firstname: 'myFn'
}

第二步:

然后会执行 vm上的$watch去创建 Watcher

源码位置:vue-main\src\core\instance\state.ts

1
2
3
4
// user置为true 表示创建监听器的watcher
options.user = true
// 创建创建监听器的watcher
const watcher = new Watcher(vm, expOrFn, cb, options) // expOrFn是函数名 cb是回调函数

到这里,user Watcher 就创建好了

3 初始化和更新

3.1 初始化

当我们在代码中使用了 watch侦听器

经过初始化后,创建好了 user Watcher

经过上面的流程后,最终会进入 new Watcher 的逻辑,这里面也是依赖收集和更新的触发点

Watcher 会默认调用 get() 方法,这是 watcher 求值的普遍方法

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: any) {
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
}

3.2 更新

当修改侦听的属性后

数据劫持会触发 set

因为前面做过依赖收集了,在派发更新时,会通知 user Watcher 执行 update(最终会执行run方法)

run() 方法中:

run() 源码:

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
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
) {
// 缓存旧值
const oldValue = this.value
// 赋予新值
this.value = value
// 触发回调函数 cb
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)
}
}
}
}

4 深度监听

深度监听是 watch 监听中一项很重要的配置,它能为我们观察对象中任何一个属性的变化。

目光再拉回到 get() 函数,其中有一段代码是这样的:

1
2
3
if (this.deep) {
traverse(value)
}

traverse方法源码位置:/src/core/observer/traverse.js

它主要做的事情就是:

递归获取每一项属性,触发它们的“数据劫持get”收集依赖

About this Post

This post is written by Duan WeiCheng, licensed under CC BY-NC 4.0.