这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战
前言
在前边学习Watcher class的update方法时,看到里边调用了一个queueWatcher的方法。
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
接下来就来看下queueWatcher具体是做了什么??
queueWatcher
queueWatcher定义在src/core/observer/scheduler.js中
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let flushing = false
let waiting = false
let index = 0
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)
}
}
}
总体来看,这里引入了一个Watcher队列。被监听的数据发生改变时并不会立即去执行它的回调(除非设置sync值为true),而是将它push到队列中,在下一个Tick去执行flushSchedulerQueue
-
首先根据Watcher id来判断这个Watcher是否存在has对象中,这里主要是避免将Watcher重复添加。
-
接下来判断flushing的值(flushing代表当前Watcher对列是不是正在执行),如果flushing为false,则将当前Watcher添加到队列中。
-
flushing为true,则从队尾开始根据id大小(队列是从小到大的顺序)将这个Watcher插入到队列对应的位置。如果没有找到合适的位置,则直接插入队列头部。index值代表队列中当前正在执行的Watcher的位置。
-
接下来判断waiting的值,如果为false,则将waiting置为true,并执行nextTick函数。waiting变量是为了保证当前只执行一次flushSchedulerQueue逻辑。
-
最后调用nextTick来执行flushSchedulerQueue。nextTick的实现前边也看到过,在现代浏览器中它就是一个微任务。把所有的回调函数(这里就是这个Watcher的回调)放到一个callbacks数组中,然后在当前事件循环结束后,去执行这个微任务。
flushSchedulerQueue
const activatedChildren: Array<Component> = []
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
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()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
-
将当前时间赋值给currentFlushTimestamp变量,然后将flushing置为true,代表正在刷新队列。
-
给队列按照从小到大的顺序排列。
-
for循环遍历Watcher队列。首先判断当前Watcher项是否设置了before方法(渲染Watcher会把beforeUpdate钩子函数传给before方法),如果设置了则执行before方法;接下来调用该Watcher的run方法去执行对应的监听回调,同时将has表中记录的当前Watcher id对应的值置为null。
-
开发环境下判断circular[id]是否超过最大限制的更新值(100),如果超过,抛出警告可能有循环更新。
-
在重置队列状态之前拷贝activedChildren和queue队列数据,用来传递给后边的钩子函数。
-
最后重置初始化参数,并且执行activated与updated钩子函数。
这里有一大段注释来解释为什么给Watcher队列要从小到大排序,具体来看一下:
- 组件的更新是从父到子,因为父组件的创建是在子组件之前的。
- 组件的user Watcher要在render Watcher之前执行,因为用户自定义的Watcher是在渲染Watcher之前创建的。
- 如果一个组件在父组件的Watcher执行期间被销毁,那么它的Watcher可以被跳过,所以父组件要先执行。
resetSchedulerState
Watcher队列更新完毕后,调用resetSchedulerState函数来重置初始参数
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
queueActivatedComponent
从注释中看出这个函数是为keep-alive组件服务的。
activatedChildren队列保存了要被激活的keep-alive组件。
/**
* Queue a kept-alive component that was activated during patch.
* The queue will be processed after the entire tree has been patched.
*/
export function queueActivatedComponent (vm: Component) {
// setting _inactive to false here so that a render function can
// rely on checking whether it's in an inactive tree (e.g. router-view)
vm._inactive = false
activatedChildren.push(vm)
}
queueActivedComponent函数是在componentVNodeHooks被调用的,componentVNodeHooks是一个定义组件Vnode钩子函数的对象,代码在src/core/vdom/create-component.js中。
具体组件的创建部分这里也先不看,大概看一下这个hooks函数:
const componentVNodeHooks = {
...
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
}
...
}
从代码来看,大概就是说如果<keep-alive>的组件已经mounted,则将这个组件添加到activatedChildren数组中。
callActivedHooks
我们已经知道queueActivatedComponent函数是为了将当前vm实例添加到activatedChildren数组中,而callActivedHooks函数就是通过遍历该数组来将被keep-alive缓存的组件激活。
function callActivatedHooks (queue) {
for (let i = 0; i < queue.length; i++) {
queue[i]._inactive = true
activateChildComponent(queue[i], true /* true */)
}
}
activateChildComponent函数定义在src/core/instance/lifecycle.js,整体来看就是递归调用,执行所有子组件的activated钩子函数。
export function activateChildComponent (vm: Component, direct?: boolean) {
...
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
callUpdatedHooks
生命周期部分后边会专门看,这里大概了解下,updated钩子函数的执行时机就是在flushSchedulerQueue函数中的callUpdatedHooks
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
在看Watcher的构造函数的时候,会判断当前Watcher实例是否是渲染Watcher,如果是会赋值给vm._watcher
if (isRenderWatcher) {
vm._watcher = this
}
callUpdatedHooks函数遍历Watcher队列,满足当前watcher是vm._watcher(渲染watcher),且该Vue实例已经mounted,同时没有被destroyed,则执行updated钩子函数。