scheduler
scheduler
(调度器)的作用是,将所有 Watcher 的重新计算放入一个异步队列里,并调用nextTick(flushSchedulerQueue)
在下一个 Tick 里对 Watcher 进行重新计算。
这样做的好处是:(主线程里)多次触发同一个 Watcher,最终 Watcher 只会执行一次。
<script>
export default {
name: "App",
data() {
return {
a: 1,
b: 2,
c: 3,
};
},
mounted() {
this.$watch(
function () {
return this.a + this.b;
},
(newValue) => {
this.c = newValue;
}
);
},
methods: {
change() {
this.a = 'a';
this.$nextTick(() => {
console.log("c", this.c); // c ab
});
this.b = "b";
this.$nextTick(() => {
console.log("c", this.c); // c ab
});
},
},
};
</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
32
33
34
比如上述代码里,执行change
方法后,修改a
和b
都会导致同一个 Watcher 重新计算。修改a
导致的 Watcher 的重新计算会被加入异步队列里,而修改b
导致的 Watcher 的重新计算不会被加入异步队列里,因此两次打印c
的结果都是ab
。
此外,在异步队列执行时,还会按 Watcher 的id
进行排序,具体好处在本文后面详细介绍。
同步计算 VS 异步队列
创建 Watcher 实例时,若选项对象里的sync
为true
,则意味着只要依赖项发生改变,将立即计算 Watcher 实例的表达式;否则,Watcher 在依赖项发生变化时,会先将 Watcher 实例添加到异步队列,等下一次tick
时再重新计算表达式。
export default class Watcher {
// ...
/**
* 依赖改变时,依赖会调用 watcher.update
*/
update () {
if (this.lazy) {
// 若是惰性计算的 watcher,只将 dirty 标志为 true,但不重新计算表达式;等到获取 value 时,再重新计算表达式
this.dirty = true
} else if (this.sync) {
// 若是同步计算,则依赖改变时,立即计算表达式
this.run()
} else {
// 否则,将 watcher 放入异步队列,在下一次 tick 时再计算表达式
queueWatcher(this)
}
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。—— 深入响应式原理 - 异步更新队列open in new window
调度器(Scheduler)
queueWatcher
纵观目前 Vue 里所有源码,基本上没有 Watcher 是同步计算表达式的。这也就是说,除了计算属性是使用的惰性计算的 Watcher 之外,其他所有的 Watcher 在update
时都会进入队列里,等待在下一个tick
里执行。
此处的队列是 Vue 在通知更新时的优化,主要保证了:
- 同一事件循环里多次触发的 Watcher,最终只执行一次(借鉴了 DOM 的渲染方式?)
- 所有加入队列的 Watcher,会按其
id
进行排序 - 队列里的所有 Watcher 都将在下一个事件循环
tick
里执行
// src/core/observer/scheduler.js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 若当前 wathcer 没进行过 queueWatcher 处理,则进行如下处理;否则,忽略
if (has[id] == null) {
has[id] = true
if (!flushing) {
// 若是队列还没有 flush,则将当前 watcher 加入到队列末尾
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.
// 若是队列里正在 flush,则将当前 watcher 按照 id 插入到队列里
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
// 加锁,在下一个 tick 里 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
flushSchedulerQueue
// src/core/observer/scheduler.js
function flushSchedulerQueue () {
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// 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
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
// 队列里的 wathcer 执行完后,重置调度器的状态,方便下次再次循环执行该队列
resetSchedulerState()
// call component updated and activated hooks
// 调用 activated 钩子
// TODO: 跟 keep-alive 有关,待之后分析
callActivatedHooks(activatedQueue)
// 调用 update 钩子
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
/**
* 渲染 Watcher,在重新计算表达式后,调用 updated 钩子
*/
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
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
Watcher 按 id 排序
在执行队列里的 Watcher 之前,会先将 Watcher 按其id
从小到大排序,这主要是为了保证:
- 组件的更新顺序是先父后子。原因是,父组件是在子组件之前创建的,父组件的 Watcher 也是先于子组件的 Watcher 创建的。
- 组件里用户自定义的 Watcher 比如组件选项对象里定义的
watch
选项,在组件的渲染 Watcher 之前执行。原因是,用户自定义的 Watcher 是在组件渲染 Watcher 之前创建的。 - 若组件在其父组件 Watcher 执行的过程中被销毁,则该组件的 Watcher 就没必要执行,可以跳过。
排序好之后,就会遍历队列里的每个 Watcher。若是该 Watcher 有before
方法,则先执行,之后再执行watcher.run()
方法。
beforeUpdate 钩子
在挂载组件时,会创建组件的渲染 Watcher,创建渲染 Watcher 时,参数options
会存在before
方法,该方法最终将挂载到 Watcher 实例上。before
方法的作用就是,在渲染 Watcher 重新计算之前,先调用组件的beforeUpdate
钩子函数
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (options) {
// ...
this.before = options.before
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
实时获取队列的长度
我们发现,在每次获取队列的长度时,都是使用的queue.length
。而一般在数组长度不变化的情况下,我们只在首次获取数组长度len
,之后遍历的过程中始终使用len
;只有当数组长度在遍历过程中发生改变时,才需要实时获取queue.length
的值。
事实上,在flushSchedulerQueue
执行过程中,仍然会有新的 Watcher 加入到队列里,新加入的 Watcher 会按id
大小,插入到队列里。
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
// 若是队列还没有 flush,则将当前 watcher 加入到队列末尾
queue.push(watcher)
} else {
// 若是队列里正在 flush,则将当前 watcher 按照 id 插入到队列里
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
重置状态
队列里的 wathcer 执行完后,需要重置调度器的状态,方便下次再次循环执行该队列。
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
2
3
4
5
6
7
8
调用 activated 钩子
待学习
调用 updated 钩子
对于(已挂载的)组件的渲染 Watcher 来说,每次重新渲染之后,都需要调用组件的updated
钩子。
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
2
3
4
5
6
7
8
9
10