# 派发更新
在上节介绍完依赖收集之后,我们来分析派发更新。这里我们将介绍派发更新完成了什么事情以及其具体实现过程。
首先派发更新主要做什么事情?
当我们进行依赖收集时,就是为了当我们对响应式数据修改的时候,会通过管理 Watcher 的类 Dep 的实例来通知所有订阅了该数据的 Watcher, 从而执行 update 方法。对于渲染 watcher 来说,update 方法会组件更新重新渲染;对于 computed watcher 来说,update 方法就是重新对计算属性进行求值;对于用户自定义的 watcher 而言,update 方法就是调用用户提供的回调函数。
# 派发更新另三种场景
一般大家分析派发更新的时候都只分析 Object.defineProperty 中 setter 函数触发的派发更新,实际上派发更新还会有以下另外三种场景触发。
Vue.set或者this.$set()方法调用的时候,会进行触发,其内部详细实现会在变化侦测API实现中进行说明。export function set (target: Array<any> | Object, key: any, val: any): any { //... defineReactive(ob.value, key, val) ob.dep.notify() return val }1
2
3
4
5
6Vue.delete或者this.$delete方法调用的时候,会进行派发更新。export function del (target: Array<any> | Object, key: any) { // ... delete target[key] if (!ob) { return } ob.dep.notify() }1
2
3
4
5
6
7
8当对数组使用
Vue提供的七种数组变异方法时,会进行派发更新。const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // cache original method const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) })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上面三种方法中的
dep是从this.__ob__中获取,__ob__属性是在Observe被实例化的时候通过def定义的,它指向Observe实例。constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) // ... }1
2
3
4
5
6
7这个之前依赖收集部分已经介绍过了,正式
Observe中定义了这个属性,我们才能在上述三种情况相对方便的进行派发更新。这与
Object.defineProperty中setter触发派发更新不同,setter中触发的派发更新是定义在defineReactive方法中的闭包变量const dep = new Dep()。
# 派发更新过程
首先我们来回顾下 setter 的实现:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
//...
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
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
这里在设置了新值之后主要做了两件事,一是 childOb = !shallow && observe(newVal),即在 shallow 为 falsy 时,会将设置的新值也转换为响应式,一般只有 $attrs 和 $listeners 调用该方法的时候会将 shallow 值设置为 true。之后会调用 dep.notify() 来派发更新。
dep.notify 是 Dep 的实例方法,代码如下所示:
class Dep {
//...
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()
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这里首先判断 config.async 的值是否为 true,且在开发环境下,那么会先根据实例化的 watcher 的 id 进行排序,从而确保派发更新的顺序没有问题,这个将在后面详细描述,因为一般只有在 Vue 的单元测试中会将其设置为 false,并且这将会显著的降低性能。
其实 notify 的代码逻辑非常简单,他会遍历 Watcher 的实例数组 subs,然后调用每个 Watcher 的 update 方法。
import { queueWatcher } from './scheduler'
/**
* 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)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
当 update 方法执行时,其首先会判断 this.lazy 和 this.sync 属性。其中 this.lazy 属性主要是计算属性 watcher 的标识,在进行 Watcher 实例化的时候不会立即求值,具体在后续的计算属性中会详细介绍。当我们实例化 Watcher 时 options 选项中 this.sync 设置为 true, 那么会执行 this.run()方法。
/**
* 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)
}
}
}
}
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
通过上面的代码可知,在满足数据变化或者监听的是对象等,回调会被同步调用,因此不建议使用该属性。实际上 Vue2 官方文档中也只列出了 deep 和 immediate 属性配置,虽然 Vue3 中可以通过设置 flush: 'async' 可以达到回调同步调用的效果,但是官方也是极不推荐的,默认是异步执行更新的。
因此我们来介绍 update 方法中的重点 queueWatcher(this),它定义在 core/observe/scheduler.js 文件中,如下所示:
export const MAX_UPDATE_COUNT = 100
const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
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
首先我们来看其顶部定义的几个变量,便于后续判断及理解:
queue是Watcher队列,只要不是重复的Watcher实例,即id不同且当前队列不是再flushing状态中,都会被添加进queue中。has对象用来防止重复添加相同Watcher实例index当前queue中遍历Watcher实例的索引,在flushSchedulerQueue方法中对queue队列数组遍历的index。flushing当前queue是否处于flushing中,即当前队列中的watcher是否处于执行中, 在flushSchedulerQueue方法中被修改为true。
现在我们来分析 queueWatcher 的具体过程,其定义在 src/core/observe/scheduler.js 中:
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
// 没有刷新队列的话,直接将wacher塞入队列中排队
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
// 如果正在刷新,那么这个watcher会按照id的排序插入进去
// 如果已经刷新了这个watcher,那么它将会在下次刷新再次被执行
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
// 如果是开发环境,同时配置了async为false,那么直接调用flushSchedulerQueue
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
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
- 首先获取当前
Watcher实例的自增id,然后通过对象has来判断当前watcher是否被添加到队列中,如果没有,则对id进行标记,并赋值为true。保证对同一个watcher只会添加一次,避免重复渲染。 - 判断当前是否处于
flushing状态,如果判断为false,则正常将watcher添加到队列中,如果在flushing阶段触发了queueWatcher函数,那么将按照watcher实例的id按顺序插入到队列中,如果已经执行过watcher,那么它将在队列的下次调用中立即执行。 - 之后通过
waiting的状态来保证nextTick(flushSchedulerQueue)的调用只有一次,nextTick将在后面进行介绍,这里我们只需要知道其参数将在下一个tick中执行,也就是说异步执行flushSchedulerQueue
接下来,我们来看 flushSchedulerQueue 函数:
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
// 刷新之前对队列做一次排序
// 这个操作可以保证:
// 1. 组件都是从父组件更新到子组件(因为父组件总是在子组件之前创建)
// 2. 一个组件自定义的watchers都是在它的渲染watcher之前执行
//(因为自定义watchers都是在渲染watchers之前执行(render watcher))
// 3. 如果一个组件在父组件的watcher执行期间刚好被销毁,那么这些watchers都将会被跳过
queue.sort(function (a, b) { return a.id - b.id; });
// 不对队列的长度做缓存,因为在刷新阶段还可能会有新的watcher加入到队列中来
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
// 执行watch里面定义的方法
watcher.run();
// 在测试环境下,对可能出现的死循环做特殊处理并给出提示
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\"")
: "in a component render function."
),
watcher.vm
);
break
}
}
}
// 重置状态前对activatedChildren、queue做一次浅拷贝(备份)
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
// 重置定时器的状态,也就是这个异步刷新中的has、waiting、flushing三个变量的状态
resetSchedulerState();
// 调用组件的 updated 和 activated 钩子
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// deltools 的钩子
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
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
上述方法主要做了以下几件事:还原 flushing 状态、对 watcher 队列 queue 进行排序、遍历 queue 执行 watcher.run 方法、还原状态,触发组件钩子函数。
- 还原
flushing状态,这就可以确保在执行queue队列执行过程中,仍然有Watcher可以被添加到queue队列,也就是之前所说的在flushing阶段触发了queueWatcher函数。 - 对
queue队列排序,通过sort方法来对队列进行从小到大的排序,主要是为了确保以下几点:- 组件的更新是先从父组件开始,然后到子组件。在组件渲染的时候,会先创建父组件的渲染
watcher,之后是子组件的渲染watcher,因此执行顺序也应保持先父后子。 - 用户自定义的
watcher优先于渲染watcher执行;因为用户自定义watcher先于渲染watcher创建。 - 如果子组件在父组件执行
watcher的时候被销毁,那么子组件的watcher都应该跳过,所以父组件watcher先执行。
- 组件的更新是先从父组件开始,然后到子组件。在组件渲染的时候,会先创建父组件的渲染
- 遍历
queue。在遍历的时候,我们每次都会对queue.length进行求值,是因为在执行watcher.run的时候,queue队列中可能会有新的watcher被添加进来
这里我们可以看到循环中,首先会判断 watcher.before 如果存在,则执行该函数,一般渲染 watcher 中存在该方法,它调用了 beforeUpdate 函数。
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
2
3
4
5
6
7
之后会执行 watcher.run 方法,其代码如下:
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
// ...
this.cb.call(this.vm, value, oldValue)
}
}
2
3
4
5
6
7
8
9
10
11
对于渲染 watcher 来说,在执行 this.get() 方法求值时,执行 getter 方法,也就是 updateComponent:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
2
3
从而修改组件相关响应式数据,最终会触发组件重新渲染。之后会执行我们实例化 Watcher 时提供的回调函数 this.cb.call(this.vm, value, oldValue)。
还原状态:在遍历之后通过调用
resetSchedulerState方法,来将queue队列相关的状态进行初始化。const queue: Array<Watcher> = [] const activatedChildren: Array<Component> = [] let has: { [key: number]: ?true } = {} let circular: { [key: number]: number } = {} let waiting = false let flushing = false let index = 0 /** * Reset the scheduler's state. */ function resetSchedulerState () { index = queue.length = activatedChildren.length = 0 has = {} if (process.env.NODE_ENV !== 'production') { circular = {} } waiting = flushing = false }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19触发组件钩子函数:通过调用
callActivatedHooks和callUpdatedHooks函数来分别触发组件的activated和updated钩子函数。
# flushing 阶段触发 queueWatcher 函数
下面我们通过一个简单的例子来介绍如何在 flushing 阶段触发 queueWatcher 函数,如以下代码:
<template>
<div id="app">
<div>
this is count: {{count}}
</div>
<button @click="changeCount">Add Count</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
};
},
methods: {
changeCount() {
this.count++
}
},
watch: {
count(newVal) {
if(this.count < 10) {
this.count++
}
console.log(newVal)
}
}
};
</script>
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
当我们点击按钮执行 changeCount 函数时,会修改 this.count 的值,从而触发 setter ,执行 dep.notify 函数进行派发更新,过程如下图所示:

这里我们可以看到 Dep 实例中的 subs 数组里面分别为 id 分别为 1 的 user watcher 和为 2 的 render watcher。之后执行 update 方法内部会执行 queueWatcher 函数。从而依次会将上面两个 watcher 实例 push 到 watcher 队列 queue 中。
// 伪代码
queue = [{ id:1, type: 'user watcher' }, { id: 2, type: 'render watcher' }]
2
当在 flushSchedulerQueue 函数中循环执行 queue 中每一项的 watcher.run 函数时,首先会执行用户自定义的 watcher, 其中会执行我们传递的回调函数
userCallback = (newVal) => {
if(this.count < 10) {
this.count++
}
console.log(newVal)
}
2
3
4
5
6
在这里我们对 this.count 重新赋值,从而会触发 setter 函数,再次执行 dep.notify,之后是 queueWatcher 函数,此时 flushing 为 false ,因此会走 else 分支,之后 queue 的值为如下所示:
// queueWatcher
if (has[id] == null) {
has[id] = true
// 没有刷新队列的话,直接将wacher塞入队列中排队
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
// 如果正在刷新,那么这个watcher会按照id的排序插入进去
// 如果已经刷新了这个watcher,那么它将会在下次刷新再次被执行
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
//....
}
// queue队列
queue = [
{ id:1, type: 'user watcher' },
{ id:1, type: 'user watcher' },
{ id: 2, type: 'render watcher' }
]
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
之后会重复上面的步骤,user watcher 会不断 push 进 queue 数组中,直到 this.count < 10 为止,此时的 queue 队列里面有 10 个 user watcher ,一个 render Watcher,如下图所示:

上面就是在 flushing 阶段触发 queueWatcher 函数的全流程。
同时为了避免出现死循环的情况,Vue 在开发环境会对这种情况进行一定的限制,当同一 watcher 被添加到 queue 队里中超过 100 次时,则会进行报错,在 flushSchedulerQueue 中这段代码中可以看到
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
2
3
4
5
6
7
8
9
10
11
12
13
14
如我们将上述的 user watcher 进行如下修改
watch: {
count(newVal) {
this.count++
console.log(newVal)
}
}
2
3
4
5
6
我们可以在控制台中看到如下报错信息:
vue.js:634 [Vue warn]: You may have an infinite update loop in watcher with expression "count"
(found in <Root>)
2
3
← 依赖收集 nextTick 实现原理 →