前言
什么是调度?
调度这一概念最开始应该来自于操作系统。
由于 计算机资源的有限性,必须按照一定的原则,选择任务来占用资源。
操作系统引入调度, 目的是解决计算机资源的分配问题,因为任务是源源不断的,但 CPU 不能同时执行所有的任务。如:对部分优先级高的任务(如:用户交互需要立即反馈),需要先占用资源/ 运行,这就是一个优先级的调度。
Vue 的调度是什么?有什么不同?
Vue 的调度,行为上也是 按照一定的原则,选择任务来占用资源/执行。但同样的行为,目的却是不一样的。
因为,Vue 并不需要解决计算机资源分配的问题(操作系统解决)。 【关注尚硅谷,轻松学IT】Vue 利用调度算法, 保证 Vue 组件渲染过程的正确性以及 API 的执行顺序的正确性(不好理解的话可以先看下文)
在 Vue3 的 API 设计中,存在着各种的异步回调 API 设计,如:组件的生命周期,watch API 的回调函数等。这些回调函数,并不是立即执行的,
它们都作为任务(Job), 需要按照一定的规则/顺序去执行部分规则如下:
- watch 的 callback 函数,需要在组件更新前调用
- 组件 DOM 的更新,需要在响应式数据(Vue 模板依赖的 ref、reactive、data 等数据的变化)更新之后
- 父组件需要先更新,子组件后更新
- Mounted 生命周期,需要在组件挂载之后执行
- updated 生命周期,需要在组件更新之后执行
- ……
Vue 的 API 设计中就制定了这份规则, 在什么时候应该执行什么任务,而这个规则在代码中的实现,就是调度算法。
学习 Vue 调度的目的
Vue 不是调度算法发明者,相反,Vue是 调度算法的使用者和受益者。这些设计,都是基于先人的探索沉淀,再结合自身需求改造出来的。
前端技术的更新迭代速度非常快, 但是这些优秀的设计,却是不变的,这也就是我们学习这些优秀设计的目的,能够做到,以不变应万变。
调度算法基本介绍
调度算法有两个基本数据结构: 队列(queue),任务(Job)
- 入队:将任务加入队列,等待执行
- 出队:将任务取出队列,立即执行
调度算法有很多种,它们都有不同的目的,但它们的基本数据结构都相同, 不同点在于入队和出队的方式
下面是两种常见的调度算法
- 先来先服务(FCFS):先入队的 Job 先执行。这种算法常见于,Job 平等、没有优先级的场景。
- 优先级调度算法:优先级高的 Job 先执行。
调度算法里面一点关于 Vue 的东西都没有,如何跟 Vue 扯上关系?
调度算法是对整个调度过程的抽象,算法无需关心任务(Job)的内容是什么,它作为 Vue3 的一种基础设施,起到了 解耦的作用(如果暂时还理解不了这句话,下一小节还有解释)
调度算法只调度执行的顺序,不负责具体的执行
那么 Vue 是如何利用调度算法,来实现自身 API 的正确调度的呢?我们在文章后面会详细描述
Vue3 调度算法的使用
Vue3 的调度算法,与上面提到的算法,大致相同,只是适配了 Vue 的一些细节
Vue 有 3 个队列,分别为:
- 组件 DOM 更新(不是组件的数据 data 更新) 前队列,后面也称为 Pre 队列
- 组件 DOM 更新(不是组件的数据 data 更新) 队列,后面也称为 queue 队列 / 组件异步更新队列
- 组件 DOM 更新(不是组件的数据 data 更新) 后队列,后面也称为 Post 队列
3 个队列的部分特性对比(大概看看即可,后面会详细介绍):
整个调度过程中, 只有入队过程,是由我们自己控制,整个队列的执行(如何出队),都由队列自身控制
因此:调度算法对外暴露的 API,也只有入队 API:
- queuePreFlushCb:加入 Pre 队列
- queueJob:加入 queue 队列
- queuePostFlushCb:加入 Post 队列
下面是用法:
const job1 = () => {
// 假设这里是父组件的 DOM 更新逻辑
console.log('父组件 DOM 更新 job 1')
}
job1.id = 1 // 设置优先级,Vue 规定是 id 越小,优先级越高
const job2 = () => {
// 假设这里是子组件的 DOM 更新逻辑
console.log('子组件 DOM 更新 job 2')
}
job2.id = 2 // 设置优先级
// 加入 queue 队列
// job 2 先加入,但是会在 job 1 之后执行,因为 id 小的,优先级更高
queueJob(job2)
queueJob(job1)
// 加入 Post 队列
queuePostFlushCb(() => {
// 假设这里是 updated 生命周期
console.log('执行 updated 生命周期 1')
})
// 加入 Post 队列
queuePostFlushCb(() => {
// 假设这里是 updated 生命周期
console.log('执行 updated 生命周期 2')
})
// 加入 Pre 队列
queuePreFlushCb(() => {
// 假设这里是 watch 的回调函数
console.log('执行 watch 的回调函数 1')
})
// 加入 Pre 队列
queuePreFlushCb(() => {
// 假设这里是 watch 的回调函数
console.log('执行 watch 的回调函数 2')
})
console.log('所有响应式数据更新完毕')
打印结果如下:
// 所有响应式数据更新完毕
// 执行 watch 的回调函数 1
// 执行 watch 的回调函数 2
// 父组件 DOM 更新 job 1
// 子组件 DOM 更新 job 2
// 执行 updated 生命周期 1
// 执行 updated 生命周期 2
队列使用上非常的简单,只要往对应的队列,传入 job 函数即可。队列会 在当前浏览器任务的所有 js 代码执行完成后,才开始 依次执行 Pre 队列、queue 列、Post 队列
调度算法是对整个调度过程的抽象
这里我们应该能更好的理解这句话,队列只是根据其自身的队列性质(先进先出 or 优先级),选择一个 Job 执行,队列不关心 Job 的内容是什么。
这样的设计, 可以极大的减少 Vue API 和 队列间耦合,队列不知道 Vue API 的存在,即使 Vue 未来新增新的异步回调的 API,也不需要修改队列。
在上述例子中:我们大概可以看出,Vue3 是如何使用调度 API,去控制各种类型的异步回调的执行时机的。对于不同的异步回调 API, 会根据 API 设计的执行时机,使用不同的队列。
如:
- watch 的回调函数,默认是在组件 DOM 更新之前执行,因此使用 Pre 队列。
- 组件 DOM 更新,使用 queue 队列。
- updated 生命周期需要在组件 DOM 更新之后执行,因此使用的是 Post 队列。
本文不会过多的介绍 Job 的具体内容的实现(不同的 API,Job 的内容都是不一样的),而是 专注于调度机制的内部实现,接下来我们的深入了解 Vue 的调度机制内部。
名词约定
我们从一个例子中,理解用到的各种名词:
import { ref } from 'vue'
const count = ref(0)
function add() {
count.value = count.value + 1 // template 依赖 count,修改后会触 queueJob(instance.update)
}
响应式数据更新
指模板依赖的 ref、reactive、组件 data 等响应式数据的变化
这里指点击按钮触发的 click 回调中,响应式数据 count.value 被修改
组件 DOM 更新
实际上是调用 instance.update 函数,该函数会对比 组件 data 更新前的 VNode 和 组件 data 更新后的 VNode,对比之间的差异,修改差异部分的 DOM。该过程叫 patch,比较 vnode 的方法叫 diff 算法(因为这里没有篇幅展开,因此大概看看记住 instance.update 的特点即可)
- instance 是指 Vue 内部的组件实例,我们直接使用接触不到该实例。
- instance.update 是 深度更新,即除了会更新组件本身,还会递归调用子组件的 instance.update ,因此,这个过程会更新整个组件树。
- instance.update 会 更新该组件的属性(如果父组件的传入发生变化),然后更新它对应的 DOM
- **响应式数据更新 ≠ 组件 DOM **更新,响应式数据更新,只是变量值的改变,此时还没修改 DOM,但会立即执行 queueJob(instance.update),将组件 DOM 更新任务,加入到队列。 即数据修改是立即生效的,但 DOM 修改是延迟执行
调度细节
用一个表格总结 3 个调度过程中的一些细节
接下来我们一个个细节进行解析:
任务去重
每次修改响应式变量(即修改相应的响应式数据),都会将 组件 DOM 更新 Job加入队列。
// 当组件依赖的响应式变量被修改时,会立即调用 queueJob
queueJob(instance.update)
那当我们同时修改多次,同一个组件依赖的响应式变量时,会多次调用 queueJob。
下面是一个简单的例子:
import { ref } from 'vue'
const count = ref(0)
function add() {
count.value = count.value + 1 // template 依赖 count,修改后会触 queueJob(instance.update)
count.value = count.value + 2 // template 依赖 count,修改后会触 queueJob(instance.update)
}
count.value 前后两次被修改, 会触发两次 queueJob。
为了防止多次重复地执行更新,需要在入队的时候,对 Job 进行去重(伪代码):
export function queueJob(job: SchedulerJob) {
// 去重判断
if (!queue.includes(job)) {
// 入队
queue.push(job)
}
}
其他队列的入队函数也有类似的去重逻辑。
优先级机制
只有 queue 队列和 Post 队列,是有优先级机制的, job.id 越小,越先执行。
为什么需要优先级队列?
queue 队列和 Post 队列使用优先级的原因各不相同。
我们来逐一分析:
queue 队列的优先级机制
queue 队列的 Job,是执行组件的 DOM 更新。在 Vue 中, 组件并不都是相互独立的,它们之前存在父子关系
必须先更新父组件,才能更新子组件,因为父组件可能会传参给子组件(作为子组件的属性)
下图展示的是,父组件和子组件及其属性更新先后顺序:
父组件 DOM 更新前,才会修改子组件的 props,因此,必须要先执行父组件 DOM 更新,子组件的 props 才是正确的值。
因此: 父组件优先级 > 子组件优先级。
如何保证父组件优先级更高?即如何保证父组件的 Job.id 更小?
我们上一小节说过,组件 DOM 更新,会深度递归更新子组件。组件创建的过程也一样,也会深度递归创建子组件。
下面是一个组件树示意图,其创建顺序如下:
深度创建组件,即按树的深度遍历的顺序创建组件。深度遍历,一定是先遍历父节点,再遍历子节点
因此,从图中也能看出, 父组件的序号,一定会比子组件的序号小,使用序号作为 Job.id 即可保证父组件优先级一定大于子组件
这里我们可以感受一下深度遍历在处理依赖顺序时的巧妙作用,前辈们总结出来的算法,竟有如此的妙用。
我们学习源码,学习算法,就是学习这些设计。
当我们以后在项目中,遇到依赖谁先执行的问题,会想起深度遍历这个算法。
要实现 queue 队列 Job 的优先级,我们只需要实现插队功能即可:(伪代码):
export function queueJob(job: SchedulerJob) {
// 去重判断
if ( !queue.includes(job) ) {
// 没有 id 放最后
if (job.id == null) {
queue.push(job)
} else {
// 二分查找 job.id,计算出需要插入的位置
queue.splice(findInsertionIndex(job.id), 0, job)
}
}
}
Post 队列的优先级机制
先回顾一下我们常常使用到的 Post 队列的 Job,都有哪些:
- mounted、updated 等生命周期,它们有个共同特点,就是需要等 DOM 更新后,再执行
- watchPostEffect API,用户手动设置 watch 回调在 DOM 更新之后执行
这些用户设定的回调之间,并没有依赖关系
那为什么 Post 队列还需要优先级呢?
因为有一种内部的 Job,要提前执行,它的作用是, 更新模板引用。
因为用户编写的回调函数中,可能会使用到模板引用,因此 必须要在用户编写的回调函数执行前,把模板引用的值更新。
看如下代码:
import {onUpdated, ref} from 'vue'
const count = ref(0)
function add() {
count.value = count.value + 1
}
const divRef = ref
onUpdated(() => {
console.log('onUpdated', divRef.value?.innerHTML)
})
响应式变量 count 为奇数或偶数时,divRef.value 指向的 DOM 节点是不一样的。
必须要在用户写的 updated 生命周期执行前,先更新 divRef,否则就会取到错误的值。
因此,更新模板引用的 Job,job.id = -1,会先执行
而其他用户设定的 job,没有设置 job.id,会加入到队列末尾,在最后执行。
失效任务
当组件被卸载(unmounted)时,其对应的 Job 会失效,因为不需要再更新该组件了。失效的任务,在取出队列时,不会被执行。
只有 queue 队列的 Job,会失效。
下面是一个失效案例的示意图:
- 点击按钮,count.value 改变
- count 响应式变量改变,会立即 queueJob 将子组件 Job 加入队列
- emit 事件,父组件 hasChild.value 改变
- hasChild 响应式变量改变,会立即 queueJob 将父组件 Job 加入队列
- 父组件有更高优先级,先执行。
- 更新父组件 DOM,子组件由于 v-if,被卸载
- 子组件卸载时,将其 Job 失效,Job.active = false
要实现失效任务不执行,非常简单,参考如下实现(伪代码):
for(const job of queue){
if(job.active !== false){
job()
}
}
删除任务
组件 DOM 更新(instance.update),是 深度更新,会递归的对所有子组件执行 instance.update。
因此, 在父组件深度更新完成之后,不需要再重复更新子组件,更新前,需要将组件的 Job 从队列中删除
下图是任务删除的示意图:
在一个组件 DOM 更新时,会先把该组件的 Job,从队列中删除。因为即将更新该组件,就不需要再排队执行了。
要实现删除 Job,非常简单:
export function invalidateJob(job) {
// 找到 job 的索引
const i = queue.indexOf(job)
// 删除 Job
queue.splice(i, 1)
}
// 在 instance.udpate 中删除当前组件的 Job
const job = instance.update = function(){
invalidateJob(job)
// 组件 DOM 更新
}
删除和失效,都是不执行该 Job,它们有什么使用上的区别?
Job 递归
递归这个特性,是 vue 调度中比较复杂的情况。如果暂时理解不了的,可以先继续往下看,不必过于扣细节。
Job 递归,就是 Job 在更新组件 DOM 的过程中,依赖的响应式变量发生变化,又调用 queueJob 把自身的 Job 加入到队列中。
为什么会需要递归?
先做个类比,应该就大概明白了:
你刚拖好地,你儿子就又把地板踩脏了,你只有重新再拖一遍。
如果你一直拖,儿子一直踩,就是无限递归了。。。这时候就应该把儿子打一顿。。。
在组件 DOM 更新(instance.update)的过程中,可能会导致自身依赖的响应式变量改变,从而调用 queueJob,将自身 Job 加入到队列。
由于响应式数据被改变(因为脏了),需要 整个组件重新更新(所以需要重新拖地)
下图就是一个组件 DOM 更新过程中,导致响应式变量变化的例子:
父组件刚更新完,子组件由于属性更新, 立即触发 watch,emit 事件,
修改了父组件的 loading 响应式变量,导致父组件需要重新更新。(watch 一般情况下,是加入到 Pre 队列等待执行, 【关注尚硅谷,轻松学IT】但在组件 DOM 更新时,watch也是加入队列,但会立即执行并清空 Pre 队列,暂时先记住有这个小特性即可)
Job 的结构是怎样的?
Job 的数据结构如下:
export interface SchedulerJob extends Function {
id?: number // 用于对队列中的 job 进行排序,id 小的先执行
active?: boolean
computed?: boolean
allowRecurse?: boolean // 表示 effect 是否允许递归触发本身
ownerInstance?: ComponentInternalInstance // 仅仅用在开发环境,用于递归超出次数时,报错用的
}
job 本身是一个函数,并且带有有一些属性。
- id,表示优先级,用于实现队列插队,id 小的先执行
- active:表示 Job 是否有效,失效的 Job 不执行。如组件卸载会导致 Job 失效
- allowRecurse:是否允许递归
其他属性,我们可以先不关注,因为跟调度机制的核心逻辑无关。
队列的结构是怎样的?
queue 队列的数据结构如下:
const queue: SchedulerJob[] = []
队列的执行:
// 按优先级排序
queue.sort((a, b) => getId(a) - getId(b))
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
// 执行 Job 函数,并带有 Vue 内部的错误处理,用于格式化错误信息,给用户更好的提示
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
// 清空 queue 队列
flushIndex = 0
queue.length = 0
}
在之前的图示讲解中,为了更好的理解队列,会把 Job 的执行,画成取出队列并执行。
而在真正写代码中,队列的执行,是不会把 Job 从 queue 中取出的,而是遍历所有的 Job 并执行,在最后清空整个 queue。
加入队列
queueJob
下面是 queue 队列的 Job,加入队列的实现:
export function queueJob(job: SchedulerJob) {
if (
(!queue.length ||
// 去重判断
!queue.includes(
job,
// isFlushing 表示正在执行队列
// flushIndex 当前正在执行的 Job 的 index
// queue.includes 函数的第二个参数,是表示从该索引开始查找
// 整个表达式意思:如果允许递归,则当前正在执行的 Job,不加入去重判断
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
))
) {
if (job.id == null) {
// 没有 id 的加入到队列末尾
queue.push(job)
} else {
// 在指定位置加入 job
// findInsertionIndex 是使用二分查找,找出合适的插入位置
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush() // 作用会在后面说
}
}
这里有几个特性:
- 去重
- 处理递归,如果允许递归,则正在运行的 job,不加入去重判断
- 优先级实现,按 id 从小到大,在队列合适的位置插入 Job;如果没有 id,则放到最后
queueCb
Pre 队列和 Post 队列的实现也大致相同,只不过是没有优先级机制(Post 队列的优先级在执行时处理):
function queueCb(
cb: SchedulerJobs,
activeQueue: SchedulerJob[] | null,
pendingQueue: SchedulerJob[],
index: number
) {
if (!isArray(cb)) {
if (
!activeQueue ||
// 去重判断
!activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
) {
pendingQueue.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
// 翻译:如果 cb 是一个数组,它只能是在一个 job 内触发的组件生命周期 hook(而且这些 cb 已经去重过了,可以跳过去重判断)
pendingQueue.push(...cb)
}
queueFlush()
}
export function queuePreFlushCb(cb: SchedulerJob) {
queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}
export function queuePostFlushCb(cb: SchedulerJobs) {
queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
}
小结
总的来说,加入队列函数,核心逻辑就都是如下:
function queueJob(){
queue.push(job)
queueFlush() // 作用会在后面说
}
在这个基础上,另外再加上一些去重判断、和优先级而已。
为什么组件异步队列 queue 跟 Pre 队列、Post 队列的入队方式还不一样呢?
因为一些细节上的处理不一致
- queue 队列有优先级
- 而 Pre 队列、Post 队列的入参,可能是数组
但其实我们也不需要过分关心这些细节,因为我们学习源码,其实是为了学习它的优良设计,我们把设计学到就好了,在现实的项目中,我们几乎不会遇到一模一样的场景,因此 掌握整体设计,比抠细节更重要
那么 queueFlush 有什么作用呢?
queueFlush 的作用,就好像是你第一个到饭堂打饭,阿姨在旁边坐着,你得提醒阿姨该给你打饭了。
队列其实并不是一直都在执行的, 当列队为空之后,就会停止, 等到又有新的 Job 进来的时候,队列才会开始执行
queueFlush 在这里的作用,就是告诉队列可以开始执行了。
我们来看看 queueFlush 的实现:
let isFlushing = false // 标记队列是否正在执行
let isFlushPending = false // 标记队列是否等待执行
function queueFlush() {
// 如果不是正在执行队列 / 等待执行队列
if (!isFlushing && !isFlushPending) {
// 用于标记为等待执行队列
isFlushPending = true
// 在下一个微任务执行队列
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
执行队列的方法,是 flushJob。
queueFlush 是 队列执行时机的实现 —— flushJob 会在下一个微任务时执行。
为什么执行时机为下一个微任务?为什么不能是 setTimeout(flushJob, 0)
我们目的,是延迟执行 queueJob, 等所有组件数据都更新完,再执行组件 DOM 更新(instance.update)。
要达到这一目的:我们只需要等在下一个浏览器任务,执行 queueJob 即可
因为,响应式数据的更新, 都在当前的浏览器任务中。当 queueJob 作为微任务执行时,就表明上一个任务一定已经完成了。
而在浏览器中, 微任务比宏任务有更高的优先级,因此 queueJob 使用微任务。
浏览器事件循环示意图如下:
每次循环,浏览器只会取一个宏任务执行,而微任务则是执行全部,在微任务执行 queueJob,能在最快时间执行队列,并且接下来浏览器就会执行渲染页面,更新UI。
否则,如果 queueJob 使用宏任务,极端情况下,可能会有多个宏任务在 queueJob 之前,而每次事件循环,只会取一个宏任务,则 queueJob 的执行时机会在非常的后,这对用户体验来说是有一定的伤害的
至此,我们已经把下图蓝色部分都解析完了:
剩下的是红色部分,即函数 flushJob 部分的实现了:
队列的执行 flushJob
function flushJobs() {
// 等待状态设置为 false
isFlushPending = false
// 标记队列为正在执行状态
isFlushing = true
// 执行 Pre 队列
flushPreFlushCbs()
// 根据 job id 进行排序,从小到大
queue.sort((a, b) => getId(a) - getId(b))
// 用于检测是否是无限递归,最多 100 层递归,否则就报错,只会开发模式下检查
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP
try {
// 循环组件异步更新队列,执行 job
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
// 仅在 active 时才调用 job
if (job && job.active !== false) {
// 检查无限递归
if (__DEV__ && check(job)) {
continue
}
// 调用 job,带有错误处理
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
// 收尾工作,重置这些用于标记的变量
flushIndex = 0 // 将队列执行的 index 重置
queue.length = 0 // 清空队列
// 执行 Post 队列
flushPostFlushCbs()
isFlushing = false
currentFlushPromise = null
// 如果还有 Job,继续执行队列
// Post 队列运行过程中,可能又会将 Job 加入进来,会在下一轮 flushJob 执行
if (
queue.length ||
pendingPreFlushCbs.length ||
pendingPostFlushCbs.length
) {
flushJobs()
}
}
}
flushJob 主要执行以下内容:
- 执行 Pre 队列
- 执行queue 队列
- 执行 Post 队列
- 循环重新执行所有队列,直到所有队列都为空
执行 queue 队列
queue 队列执行对应的是这一部分:
try {
// 循环组件异步更新队列,执行 job
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
// 仅在 active 时才调用 job
if (job && job.active !== false) {
// 检查无限递归
if (__DEV__ && check(job)) {
continue
}
// 调用 job,带有错误处理
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
// 收尾工作,重置这些用于标记的变量
flushIndex = 0 // 将队列执行的 index 重置
queue.length = 0 // 清空队列
}
}
循环遍历 queue,运行 Job,直到 queue 为空
queue 队列执行期间,可能会有新的 Job 入队,同样会被执行。
执行 Pre 队列
export function flushPreFlushCbs() {
// 有 Job 才执行
if (pendingPreFlushCbs.length) {
// 执行前去重,并赋值到 activePreFlushCbs
activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
// pendingPreFlushCbs 清空
pendingPreFlushCbs.length = 0
// 循环执行 Job
for (
preFlushIndex = 0;
preFlushIndex < activePreFlushCbs.length;
preFlushIndex++
) {
// 开发模式下,校验无限递归的情况
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
) {
continue
}
// 执行 Job
activePreFlushCbs[preFlushIndex]()
}
// 收尾工作
activePreFlushCbs = null
preFlushIndex = 0
// 可能递归,再次执行 flushPreFlushCbs,如果队列为空就停止
flushPreFlushCbs()
}
}
主要流程如下:
- Job 最开始是在 pending 队列中的
- flushPreFlushCbs 执行时,将 pending 队列中的 Job 去重,并改为 active 队列
- 循环执行 active 队列的 Job
- 重复 flushPreFlushCbs,直到队列为空
执行 Post 队列
export function flushPostFlushCbs(seen?: CountMap) {
// 队列为空则结束
if (pendingPostFlushCbs.length) {
// 去重
const deduped = [...new Set(pendingPostFlushCbs)]
pendingPostFlushCbs.length = 0
// #1947 already has active queue, nested flushPostFlushCbs call
// 特殊情况,发生了递归,在执行前 activePostFlushCbs 可能已经有值了,该情况可不必过多关注
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}
activePostFlushCbs = deduped
if (__DEV__) {
seen = seen || new Map()
}
// 优先级排序
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
// 循环执行 Job
for (
postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
// 在开发模式下,检查递归次数,最多 100 次递归
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
) {
continue
}
// 执行 Job
activePostFlushCbs[postFlushIndex]()
}
// 收尾工作
activePostFlushCbs = null
postFlushIndex = 0
}
}
主要流程如下:
- Job 最开始是在 pending 队列中的
- flushPostFlushCbs 执行时,将 pending 队列中的 Job 去重,然后跟 active 队列合并
- 循环执行 active 队列的 Job
为什么在队列最后没有像 Pre 队列那样,再次执行 flushPostFlushCbs?
Post 队列的 Job 执行时,可能会将 Job 继续加入到队列(Pre 队列,组件异步更新队列,Post 队列都可能)
新加入的 Job,会在下一轮 flushJob 中执行:
// postFlushCb 可能又会将 Job 加入进来,如果还有 Job,继续执行
if (
queue.length ||
pendingPreFlushCbs.length ||
pendingPostFlushCbs.length
) {
// 执行下一轮队列任务
flushJobs()
}
文章来源于全栈修仙之路