前言
我们在前面 effect 里最终调用 trigger 的地方有一个 effect.scheduler ? effect.scheduler() : effect.run()
调度器并非 Vue 独有的功能,React 中也有自己的调度器,它的作用是根据设定好的算法,决定任务什么时候被执行。
在 Vue 里的生命周期,watch,组件更新的回调等都并不是立刻同步执行的,而是会存入到调度器里。
在由调度器决定什么时候执行。 调度器 和 Vue 是完全解耦的,所以可以在任何项目中使用。
在 Vue 里的调度器暴露出了 “塞入队列” 的方法,至于何时调用都不需要 Vue 去操心,由调度器本身完成。
整段代码读起来比较顺畅,就不做过多解释。
/**
* 在下面代码里,有 2个重要的队列 , queue 和 pendingPostFlushCbs
* queue: 存放 前置任务,普通任务
* pendingPostFlushCbs: 存放后置任务
*
* 前置任务: DOM更新前的任务(比如 watch 的回调,响应式数据(Vue 模板依赖的 ref、reactive、data 等数据的变化))
* 普通任务: DOM更新 (实际上是调用 instance.update 函数,该函数会对比组件 data 更新前的 VNode 和组件 data 更新后的 VNode)
* 后置任务: DOM更新后(比如 生命周期钩子)
*
* 【注】
* 响应式数据更新 ≠ 组件 DOM **更新,响应式数据更新,只是变量值的改变,
* 此时还没修改 DOM,但会立即执行 queueJob(instance.update),将组件 DOM 更新任务,加入到队列。
* 即数据修改是立即生效的,但 DOM 修改是延迟执行
*/
import { isArray } from '../utils'
export interface SchedulerJob extends Function {
id?: number // 优先级。 id 越小,越靠前,越先执行
pre?: boolean // 前置任务的标志
active?: boolean // 是否失效
computed?: boolean
/**
*
* 是否允许effect递归触发自身
*/
allowRecurse?: boolean
}
export type SchedulerJobs = SchedulerJob | SchedulerJob[]
let isFlushing = false // 标志: 正在调度清空任务
let isFlushPending = false // 标志: 准备调度清空任务
let flushIndex = 0 // 标志: 正在调度清空的下标
// 存放 前置任务 和 普通任务 的队列
const queue: SchedulerJob[] = []
// 存放 后置任务 队列
const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerJob[] | null = null
let postFlushIndex = 0 // 标志: 正在调度清空的后置任务下标
const resolvedPromise: Promise<void> = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null
const getId = (job: SchedulerJob) => {
return job.id == null ? Infinity : job.id
}
export function nextTick<T = void>(this: T, fn?: (this: T) => void): Promise<void> {
const p = currentFlushPromise || resolvedPromise
// currentFlushPromise 返回了一个 Promise 是为了保证 nextTick 能够顺序执行
// nextTick 就相当于 Promise.resolve().then( flushJobs是同步任务 ).then( nextTickFn )
// nextTickFn 会在 flushJobs 这个同步任务执行完之后,整个 currentFlushPromise 才会变成 fulfilled 状态
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
// 通过二分法查找到合适的位置
const findInsertionIndex = (id: number): number => {
// 开始下标 永远从 正在调度清空的下标 +1 开始
let start = flushIndex + 1
let end = queue.length
while (start < end) {
// 取中间值
const mid = (start + end) >>> 1
const midId = getId(queue[mid])
// 如果中间值id 大于 任务的id ,那么就说明在左半边, 将end设为 mid,反之设置start
midId > id ? (end = mid) : (start = mid + 1)
}
return start
}
// 删除 job (是因为 job 已经更新了,不需要重复更新所以才删除,而失效是因为 组件卸载了,所以失效了)
// 组件 DOM 更新(instance.update),是深度更新,会递归的对所有子组件执行 instance.update。
// 因此,在父组件深度更新完成之后,不需要再重复更新子组件,更新前,需要将组件的 Job 从队列中删除
// 【注】 删除job 和 失效的区别: 失效是设置 active 为false,还保留在 queue 队列中,所以如果又有进来的相同job,会被去重掉
export function invalidateJob(job: SchedulerJob) {
const i = queue.indexOf(job)
// 只能删除 flushIndex 后面的job
if (i > flushIndex) {
queue.splice(i, 1)
}
}
/**
*
* 往 queue 里插入任务
*/
export const queueJob = (job: SchedulerJob) => {
// 1. 对任务进行去重。判断 job 是否已存在
// 普通情况下从 flushIndex 开始检测是否重复,只有不存在于 queue 中的 job 才能被塞入
// 【注】特殊情况下:如果 正在调度清空 且 job是允许递归的,那么需要跳过 正在调度任务的去重检测(从 flushIndex + 1)
if (!queue.length || !queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) {
// 2. 塞入队列
// 如果没有id,则放到最后执行
if (job.id == null) {
queue.push(job)
} else {
// 存在 id ,通过二分法快速找到 该job 应该存在于哪个位置
queue.splice(findInsertionIndex(job.id), 0, job)
}
// 3. 清空队列
// 所以每次 存入job 都会清空队列
// 但是由于 isFlushing 和 isFlushPending 的标志 加上 queueFlush 是利用 Promise 微任务异步队列清空的特性
// 不管你在同步任务中执行多少次 queueJob ,清空队列只会执行一次
queueFlush()
}
}
/**
*
* 往 后置队列 里插入任务
*/
export function queuePostFlushCb(cb: SchedulerJobs) {
if (!isArray(cb)) {
if (
!activePostFlushCbs ||
!activePostFlushCbs.includes(cb, cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex)
) {
pendingPostFlushCbs.push(cb)
}
} else {
/**
* 如果 cb 是一个数组,那么在后置队列中这只可能是由一个 job 触发的组件生命周期钩子,
* 已经在主queue中去重了,所以这里可以不用去重,来提高性能
*/
pendingPostFlushCbs.push(...cb)
}
// 不管是 普通队列 还是 后置队列都是调用这个方法清空,所以 普通队列一定会优先于后置队列清空
queueFlush()
}
// 清空任务队列
const queueFlush = () => {
// 如果正在清空 或者 准备清空 则退出
if (isFlushing || isFlushPending) return
// 设置准备清空标志
isFlushPending = true
// flushJobs 会被放入微任务队列中,等待同步任务执行完执行
// 且由于 isFlushPending 已设为 true,那么在同步任务执行期间,如果多次执行了 queueJob
// 也只会执行一次 flushJobs
currentFlushPromise = resolvedPromise.then(flushJobs)
}
const flushJobs = () => {
// 设置清空标志
isFlushPending = false
isFlushing = true
// 1. 因为组件是从父 -> 子,所以父亲的id会优先于子,排序是为了确保顺序
// 2. 如果父组件在更新期间卸载了子组件,那么可以跳过子组件的更新
// 重新排序
queue.sort(comparator)
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
// 组件被卸载,那么 job 会失效。
// 如果 active 为false 表示任务已失效,不需要执行了
if (job && job.active !== false) {
job()
}
}
} finally {
// 执行完任务,重置队列
flushIndex = 0
queue.length = 0
// 执行 后置任务队列
flushPostFlushCbs()
isFlushing = false
currentFlushPromise = null
// 在调用 后置任务队列的时候,可能又会生成队列,所以继续清空,直到全部两个队列都清空为止
if (queue.length || pendingPostFlushCbs.length) {
flushJobs()
}
}
}
// 清空前置任务队列
// 【注】该清空是一个 同步任务, 和 flushJobs 不同,flushJobs虽然也是同步的,但调用它的是一个异步,所以我们姑且认为是异步的
// 这么做有个好处就是,你不论在何处调用 flushPreFlushCbs ,总能保证 前置队列 被正确且优先执行
export function flushPreFlushCbs(i = isFlushing ? flushIndex + 1 : 0) {
for (; i < queue.length; i++) {
const cb = queue[i]
if (cb && cb.pre) {
queue.splice(i, 1)
i--
cb()
}
}
}
// 清空后置任务队列
export function flushPostFlushCbs() {
if (pendingPostFlushCbs.length) {
const deduped = [...new Set(pendingPostFlushCbs)]
pendingPostFlushCbs.length = 0 // 暂放到 deduped 时就重置 pendingPostFlushCbs
// 特殊情况,在 issue #1947 中,有人使用createApp 递归,结果导致在下面的for循环中 activePostFlushCbs 变为了 null
// 这里用判断拦截一层,避免报错
// 那么就继续往push deduped 即可,不需要再往下执行,所以 return 出去
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}
activePostFlushCbs = deduped
// 重新排序,后置任务由于没有 pre 属性,所以可以直接用 a - b 来重排序
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) {
activePostFlushCbs[postFlushIndex]()
}
activePostFlushCbs = null
postFlushIndex = 0
}
}
// sort 里 a 为 next ,b 为 current
const comparator = (a: SchedulerJob, b: SchedulerJob) => {
// 由于咱们 job 的 id 都是优先级数字,所以可以直接相减判断
const diff = getId(a) - getId(b)
// 如果 diff 相等,就用 pre 属性去判断
if (diff == 0) {
// 如果 a 是前置任务,那么就返回 -1 ,表示将 a 与 b 交替
if (a.pre && !b.pre) return -1
if (!a.pre && b.pre) return 1
}
return diff
}
向外部暴露了 queueJob , queuePostFlushCb ,通过这 2 个方法,进行调度器任务。