本文主要记录 nextTick api 源码的阅读理解,尽量说清一个执行调度机制,如果理解vue2 中nextTick的实现,应该也能很快上手,换汤不换药,一些叫法/名词不太一样。
先看看调度任务 SchedulerJob
/SchedulerJobs
类型定义:
interface SchedulerJob extends Function {
id?: number // 调度任务权重 - 用于执行顺序控制
pre?: boolean // 调度任务是否在 组件更新 update 前执行
active?: boolean
computed?: boolean // 关联computed
allowRecurse?: boolean // 允许递归
ownerInstance?: ComponentInternalInstance // 组件实例
}
type SchedulerJobs = SchedulerJob | SchedulerJob[] // 任务队列类型
调度任务的执行涉及两个状态字段:
isFlushing
:正在清空调度任务队列(正在执行调度任务)isFlushPending
:清空调度任务队列逻辑 加入微任务队列
全局调度任务队列:
queue: SchedulerJob[] = []
:组件更新前要执行的调度任务队列,flushIndex
记录正在执行的调度任务的索引,控制新的调度任务可以正确加入队列queue,保证正确执行。pendingPostFlushCbs: SchedulerJob[] = []
:组件更新后才执行的任务队列。activePostFlushCbs: SchedulerJob[] | null = null
组件更新后才执行且正处于执行过程的任务队列,pendingPostFlushCbs 经过去重(Set)的任务,postFlushIndex
记录正在执行的post类型调度任务的索引,控制新的post调度任务可以正确加入队列queue,保证正确执行。
加入微任务队列方法:
// 全局 Promise,用于将调度任务加入微任务队列
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
// 全局 Promise,用于将清空调度任务队列的逻辑加入微任务队列,全局唯一
let currentFlushPromise: Promise<void> | null = null
Vue3异步更新机制直接利用 Promise
机制实现,不像Vue2受限制,需要进行兼容判断使用 MutationObserver/setTimeout/setImmediate
先看看 nextTick
api:
export function nextTick<T = void>(
this: T,
fn?: (this: T) => void // 调度任务
): Promise<void> {
const p = currentFlushPromise || resolvedPromise // 获取Promise,如果已经执行清空调度任务队列,使用currentFlushPromise,否则,直接使用全局 resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p // 将nextTick调度任务加入微任务队列
}
第一眼看是没看懂,竟然就这么两行,相比vue2的api,确实少了挺多!!!
简单说明一下:
调用 nextTick ,传入的回调函数 fn (调度任务) ,如果存在
currentFlushPromise
(当前已经将清空调度任务队列queue逻辑加入微任务队列),则用此Promise来添加nextTick的调度任务到微任务队列,保证nextTick回调能在queue任务清空后才执行(update之后调用);如果不存在currentFlushPromise
(未将执行清空queue逻辑加入微任务队列),则直接使用resolvedPromise
将nextTick任务加入微任务队列。
那 currentFlushPromise
是怎么来的呢?
先看一下queueJob
:
// 调度任务加入队列queue
export function queueJob(job: SchedulerJob) {
// the dedupe search uses the startIndex argument of Array.includes()
// by default the search index includes the current job that is being run
// so it cannot recursively trigger itself again.
// if the job is a watch() callback, the search will start with a +1 index to
// allow it recursively trigger itself - it is the user's responsibility to
// ensure it doesn't end up in an infinite loop.
// 队列queue为空 || 队列queue不含此任务job
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
// flushIndex 记录正在执行调度任务的索引
// 如果在执行队列清空时有新任务加入,需要添加到flushIndex索引之后,这样才能保证正确执行。
if (job.id == null) {
// 队列末尾加入任务 id==null -> infinite
queue.push(job)
} else {
// findInsertionIndex 根据 flushIndex 和 job.id 获取合适的索引位置
// 根据id值-选择合适位置插入
queue.splice(findInsertionIndex(job.id), 0, job)
}
// 加入队列后,需要将清空队列操作逻辑加入微任务中
queueFlush()
}
}
queueFlush 类型定义:
function queueFlush() {
// 如果当前还未处于调度任务队列执行的状态
// 或者 还未将调度任务清空逻辑加入微任务队列中
// 根据isFlushing 和 isFlushPending 状态控制
if (!isFlushing && !isFlushPending) {
isFlushPending = true // 标识 已加将清空队列操作加入微任务队列中
currentFlushPromise = resolvedPromise.then(flushJobs) // 加入微任务队列中,并设置 currentFlushPromise
}
}
当有调度任务 SchedulerJob
加入队列queue中,会默认执行调度任务队列queue的清空操作,使用 resolvedPromise
加操作放入微任务队列,并返回该Promise,用于后续添加微任务。
再来看看 flushJobs
:
function flushJobs(seen?: CountMap) {
isFlushPending = false // 重置isFlushPending状态
isFlushing = true // 标识正在执行调度任务
if (__DEV__) {
seen = seen || new Map()
}
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child so its render effect will have smaller
// priority number) 组件从父组件向子组件更新,父组件总在先于子组件创建
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped. 组件在父组件更新时被卸载,它的更新会被跳过
// 调度任务排序 - 根据id大小
queue.sort(comparator)
// conditional usage of checkRecursiveUpdate must be determined out of
// try ... catch block since Rollup by default de-optimizes treeshaking
// inside try-catch. This can leave all warning code unshaked. Although
// they would get eventually shaken by a minifier like terser, some minifiers
// would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
// 函数递归调用检查/登记,防止陷入死循环
// checkRecursiveUpdates 记录同个函数fn执行次数(维护在CountMap<Map<Function,number>>)
// 如果次数超过阈值 RECURSION_LIMIT = 100,则提示错误
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP
try {
// 遍历任务队列,执行任务
// 记录flushIndex
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue
}
// console.log(`running:`, job.id)
// 调度任务错误处理
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
// 还原queue相关状态
flushIndex = 0
queue.length = 0
// 开始执行post任务执行
// update 在queue队列
flushPostFlushCbs(seen)
// 重置执行状态标识 新的任务加入queue可以再次执行清空
isFlushing = false
currentFlushPromise = null // 执行队列删除
// some postFlushCb queued jobs!
// keep flushing until it drains.
// 如果还有剩余任务等待执行。嵌套执行,防止任务调度缺失
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
清空完queue任务队列后,执行 flushPostFlushCbs()
清空 pendingPostFlushCbs
任务队列:
export function flushPostFlushCbs(seen?: CountMap) {
if (pendingPostFlushCbs.length) {
// post 任务去重
const deduped = [...new Set(pendingPostFlushCbs)]
pendingPostFlushCbs.length = 0 // 清空post任务队列
// 加入正在执行的post任务队列 activePostFlushCbs
// #1947 already has active queue, nested flushPostFlushCbs call
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
// activePostFlushCbs 队列已经在清空(任务执行),加入队列即可等待执行
return
}
// 否则需要开启清空activePostFlushCbs(执行任务)
activePostFlushCbs = deduped
if (__DEV__) {
seen = seen || new Map()
}
// post任务排序
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
// 开始执行任务,清空队列
// 记录postFlushIndex
for (
postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
// 函数调用次数检测
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
) {
continue
}
// 执行post任务
activePostFlushCbs[postFlushIndex]()
}
// 重置状态
activePostFlushCbs = null
postFlushIndex = 0
}
}
怎么加入 pendingPostFlushCbs
队列 ?主要是组件声明钩子注入。
queuePostFlushCb
定义:
export function queuePostFlushCb(cb: SchedulerJobs) {
if (!isArray(cb)) {
// 如果当前还未开始执行 post 任务,或者正在执行post任务的队列中不存在此任务
if (
!activePostFlushCbs ||
!activePostFlushCbs.includes(
cb,
cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex
)
) {
// 就将任务加入到 pendingPostFlushCbs 队列中,等待执行
pendingPostFlushCbs.push(cb)
}
} else {
// if cb is an array, it is a component lifecycle hook which can only be
// triggered by a job, which is already deduped in the main queue, so
// we can skip duplicate check here to improve perf
// 只有组件声明钩子才会使用数组形式
// 直接跳过判断条件,全部加入post任务队列中
pendingPostFlushCbs.push(...cb)
}
// 执行queue队列清空
queueFlush(),保证 post任务在 update 之后执行
}
除此之外,还提供一个 flushPreFlushCbs
方法,从队列queue中找到需要在组件更新update前执行的调度任务,执行并从队列中移除,防止重复执行。
flushPreFlushCbs
定义:
export function flushPreFlushCbs(
seen?: CountMap,
// if currently flushing, skip the current job itself
i = isFlushing ? flushIndex + 1 : 0
) {
if (__DEV__) {
seen = seen || new Map()
}
for (; i < queue.length; i++) {
const cb = queue[i]
if (cb && cb.pre) {
// 找到 pre = true 的任务
// 校验调用次数
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
continue
}
// 执行任务并移出队列queue
queue.splice(i, 1)
i--
cb()
}
}
}