Skip to main content

vue3 nextTick 原理分析

nextTick 的作用

官方的介绍是

将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。

在 vue 中数据发生变化后,dom 的更新是需要一定时间的,而我们在数据更新之后就立即去操作或者获取 dom 的话,其实还是操作和获取的未更新的 dom ,而我们可以调用 nextTick 拿到最新的 dom

import { createApp, nextTick } from 'vue'

const app = createApp({
setup() {
const message = ref('Hello!')
const changeMessage = async newMessage => {
message.value = newMessage
await nextTick()
console.log('Now DOM is updated')
}
},
})

调度器的目标

浏览器为了实现异步任务,设计了 Event Loop 这个机制。在执行主执行栈的任务时,异步任务会被放入 Task Queue 中。当然,异步任务根据具体情况,会分别加入 Macro Task QueueMicro Task Queue 中。待主执行栈的任务清空后,就会依次执行 Task Queue 中的任务。

由此,调度器的实现就呼之欲出了。

我们只要实现一种机制,将需要异步执行的任务塞入 Task Queue 中就可以了。简单来说,就是借助 PromisesetTimeout 等。尽管他们二者一个属于 Micro Task,一个属于 Macro Task。但在这个主题下,他们从逻辑上讲是一致的。(vue 中使用的是 Promise 属于微任务 ,Preact 使用的是 requestAnimationFrame 和 setTimeout 属于宏任务,具体而言微任务优先级较高,宏任务优先级较低,但各有优势)

单元测试

我们先来阅读一下单测快速了解一下源码

nextTick 单元测试的目录位置:packages/runtime-core/__tests__/scheduler.spec.ts

nextTick

it('nextTick', async () => {
const calls: string[] = []
const dummyThen = Promise.resolve().then()
const job1 = () => {
calls.push('job1')
}
const job2 = () => {
calls.push('job2')
}
nextTick(job1)
job2()
expect(calls.length).toBe(1)
await dummyThen
// job1 will be pushed in nextTick
expect(calls.length).toBe(2)
expect(calls).toMatchObject(['job2', 'job1'])
})

nextTick 接收一个函数作为参数,加入到微任务队列,当宏任务执行完后,执行微任务队列中的函数,job1 执行

queueJob

基本用法

it('basic usage', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
}
const job2 = () => {
calls.push('job2')
}
queueJob(job1)
queueJob(job2)
expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['job1', 'job2'])
})

queueJob 接收一个函数作为参数,会将函数按顺序保存到一个队列中,它是一个微任务

刷新时应按 job 的 ID 的升序插入 job

it("should insert jobs in ascending order of job's id when flushing", async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')

queueJob(job2)
queueJob(job3)
}

const job2 = () => {
calls.push('job2')
queueJob(job4)
queueJob(job5)
}
job2.id = 10

const job3 = () => {
calls.push('job3')
}
job3.id = 1

const job4 = () => {
calls.push('job4')
}

const job5 = () => {
calls.push('job5')
}

queueJob(job1)

expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['job1', 'job3', 'job2', 'job4', 'job5'])
})

如果 queueJob 接收的函数有 id 属性的话,会按照 id 升序加入队列,不指定的话加入到最后

queueJob 会去重队列中的 job

it('should dedupe queued jobs', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
}
const job2 = () => {
calls.push('job2')
}
queueJob(job1)
queueJob(job2)
queueJob(job1)
queueJob(job2)
expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['job1', 'job2'])
})

queueJob 会将重复加入队列的 job 去重,应该是直接删除新加入的重复 job,这样可以保证顺序不变

刷新时的 queueJob

    it('queueJob while flushing', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
// job2 will be executed after job1 at the same tick
queueJob(job2)
}
const job2 = () => {
calls.push('job2')
}
queueJob(job1)

await nextTick()
expect(calls).toEqual(['job1', 'job2'])
})
})

如果 queueJob(job2)job1 内部调用,那么 job2 会在 job1 之后的同一时间执行,不会等到下一次微任务

queuePreFlushCb

基本用法

it('basic usage', async () => {
const calls: string[] = []
const cb1 = () => {
calls.push('cb1')
}
const cb2 = () => {
calls.push('cb2')
}

queuePreFlushCb(cb1)
queuePreFlushCb(cb2)

expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['cb1', 'cb2'])
})

queuePreFlushCbqueueJob 类似,也是接收一个函数作为参数,按顺序地加入队列,在微任务队列执行

preFlushCb 会去重队列中的 preFlushCb

it('should dedupe queued preFlushCb', async () => {
const calls: string[] = []
const cb1 = () => {
calls.push('cb1')
}
const cb2 = () => {
calls.push('cb2')
}
const cb3 = () => {
calls.push('cb3')
}

queuePreFlushCb(cb1)
queuePreFlushCb(cb2)
queuePreFlushCb(cb1)
queuePreFlushCb(cb2)
queuePreFlushCb(cb3)

expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
})

preFlushCb 也会去重

链式 queuePreFlushCb

it('chained queuePreFlushCb', async () => {
const calls: string[] = []
const cb1 = () => {
calls.push('cb1')
// cb2 will be executed after cb1 at the same tick
queuePreFlushCb(cb2)
}
const cb2 = () => {
calls.push('cb2')
}
queuePreFlushCb(cb1)

await nextTick()
expect(calls).toEqual(['cb1', 'cb2'])
})

如果 queuePreFlushCb(job2)cb1 内部调用,那么 cb2 会在 cb1 之后的同一时间执行,不会等到下一次微任务

queueJob with queuePreFlushCb

preFlushCb 中的 queueJob

it('queueJob inside preFlushCb', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
}
const cb1 = () => {
// queueJob in postFlushCb
calls.push('cb1')
queueJob(job1)
}

queuePreFlushCb(cb1)
await nextTick()
expect(calls).toEqual(['cb1', 'job1'])
})

preFlushCb 中可以嵌套 job ,且 job 会立即执行

preFlushCb 中的 queueJob 和 preFlushCb

it('queueJob & preFlushCb inside preFlushCb', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
}
const cb1 = () => {
calls.push('cb1')
queueJob(job1)
// cb2 should execute before the job
queuePreFlushCb(cb2)
}
const cb2 = () => {
calls.push('cb2')
}

queuePreFlushCb(cb1)
await nextTick()
expect(calls).toEqual(['cb1', 'cb2', 'job1'])
})

preFlushCb 中嵌套的 queuePreFlushCb 会在嵌套的 queueJob 之前执行,即 queuePreFlushCb 优先级高于 queueJob

queueJob 中的 preFlushCb

it('preFlushCb inside queueJob', async () => {
const calls: string[] = []
const job1 = () => {
queuePreFlushCb(cb1)
queuePreFlushCb(cb2)
flushPreFlushCbs(undefined, job1)
calls.push('job1')
}
const cb1 = () => {
calls.push('cb1')
// a cb triggers its parent job, which should be skipped
queueJob(job1)
}
const cb2 = () => {
calls.push('cb2')
}
})

job 里可以嵌套 queuePreFlushCb ,如果在嵌套的 cb 中又调用了父 job,那么这次调用会被跳过

在 postFlushCb 队列中的 preFlushCb

it('queue preFlushCb inside postFlushCb', async () => {
const cb = jest.fn()
queuePostFlushCb(() => {
queuePreFlushCb(cb)
})
await nextTick()
expect(cb).toHaveBeenCalled()
})

postFlushCb 中可以嵌套 queuePreFlushCbqueuePreFlushCb 会立即执行

queuePostFlushCb

基本用法

describe('queuePostFlushCb', () => {
it('basic usage', async () => {
const calls: string[] = []
const cb1 = () => {
calls.push('cb1')
}
const cb2 = () => {
calls.push('cb2')
}
const cb3 = () => {
calls.push('cb3')
}

queuePostFlushCb([cb1, cb2])
queuePostFlushCb(cb3)

expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
})
})

queuePostFlushCb 可以接收一个函数或一个函数数组作为参数,按顺序加入队列中,在微任务队列中执行

queuePostFlushCb 会去重队列中的 postFlushCb

it('should dedupe queued postFlushCb', async () => {
const calls: string[] = []
const cb1 = () => {
calls.push('cb1')
}
const cb2 = () => {
calls.push('cb2')
}
const cb3 = () => {
calls.push('cb3')
}

queuePostFlushCb([cb1, cb2])
queuePostFlushCb(cb3)

queuePostFlushCb([cb1, cb3])
queuePostFlushCb(cb2)

expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
})

queuePostFlushCb 会去重队列中的函数,即使是通过数组传的函数,也会去重,应该是将数组拆开了

刷新时 queuePostFlushCb

it('queuePostFlushCb while flushing', async () => {
const calls: string[] = []
const cb1 = () => {
calls.push('cb1')
// cb2 will be executed after cb1 at the same tick
queuePostFlushCb(cb2)
}
const cb2 = () => {
calls.push('cb2')
}
queuePostFlushCb(cb1)

await nextTick()
expect(calls).toEqual(['cb1', 'cb2'])
})

嵌套的 queuePostFlushCb 会立即执行

queueJob with queuePostFlushCb

postFlushCb 内的 queueJob

it('queueJob inside postFlushCb', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
}
const cb1 = () => {
// queueJob in postFlushCb
calls.push('cb1')
queueJob(job1)
}

queuePostFlushCb(cb1)
await nextTick()
expect(calls).toEqual(['cb1', 'job1'])
})

postFlushCb 中能嵌套 queueJobqueueJob 会立即执行

postFlushCb 内的 queueJob 和 postFlushCb

it('queueJob & postFlushCb inside postFlushCb', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
}
const cb1 = () => {
calls.push('cb1')
queuePostFlushCb(cb2)
// job1 will executed before cb2
// Job has higher priority than postFlushCb
queueJob(job1)
}
const cb2 = () => {
calls.push('cb2')
}

queuePostFlushCb(cb1)
await nextTick()
expect(calls).toEqual(['cb1', 'job1', 'cb2'])
})

queueJobqueuePostFlushCb 先执行,即 queueJob 优先级高于 queuePostFlushCb

queueJob 内的 postFlushCb

it('postFlushCb inside queueJob', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
// postFlushCb in queueJob
queuePostFlushCb(cb1)
}
const cb1 = () => {
calls.push('cb1')
}

queueJob(job1)
await nextTick()
expect(calls).toEqual(['job1', 'cb1'])
})

可以在 job 中嵌套 queuePostFlushCbqueuePostFlushCb 立即执行

queueJob 和 postFlushCb 在 queueJob 中

it('queueJob & postFlushCb inside queueJob', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
// cb1 will executed after job2
// Job has higher priority than postFlushCb
queuePostFlushCb(cb1)
queueJob(job2)
}
const job2 = () => {
calls.push('job2')
}
const cb1 = () => {
calls.push('cb1')
}

queueJob(job1)
await nextTick()
expect(calls).toEqual(['job1', 'job2', 'cb1'])
})

queueJob 先于 queuePostFlushCb 执行,queueJob 优先级高于 queuePostFlushCb

嵌套的 queueJob 与 postFlush

it('nested queueJob w/ postFlushCb', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')

queuePostFlushCb(cb1)
queueJob(job2)
}
const job2 = () => {
calls.push('job2')
queuePostFlushCb(cb2)
}
const cb1 = () => {
calls.push('cb1')
}
const cb2 = () => {
calls.push('cb2')
}

queueJob(job1)
await nextTick()
expect(calls).toEqual(['job1', 'job2', 'cb1', 'cb2'])
})

job1 中调用 queueJob(job2)job2中的 queuePostFlushCb 会在和 queueJob(job2) 同级的 queuePostFlushCb 执行后执行

无效作业

test('invalidateJob', async () => {
const calls: string[] = []
const job1 = () => {
calls.push('job1')
invalidateJob(job2)
job2()
}
const job2 = () => {
calls.push('job2')
}
const job3 = () => {
calls.push('job3')
}
const job4 = () => {
calls.push('job4')
}
// queue all jobs
queueJob(job1)
queueJob(job2)
queueJob(job3)
queuePostFlushCb(job4)
expect(calls).toEqual([])
await nextTick()
// job2 should be called only once
expect(calls).toEqual(['job1', 'job2', 'job3', 'job4'])
})

invalidateJob 可以让一个 job 不执行

根据 id 对作业进行排序

test('sort job based on id', async () => {
const calls: string[] = []
const job1 = () => calls.push('job1')
// job1 has no id
const job2 = () => calls.push('job2')
job2.id = 2
const job3 = () => calls.push('job3')
job3.id = 1

queueJob(job1)
queueJob(job2)
queueJob(job3)
await nextTick()
expect(calls).toEqual(['job3', 'job2', 'job1'])
})

如果 queueJob 接收的函数有 id 属性的话,会按照 id 升序加入队列,不指定的话加入到最后

根据 id 对 SchedulerCbs 进行排序

test('sort SchedulerCbs based on id', async () => {
const calls: string[] = []
const cb1 = () => calls.push('cb1')
// cb1 has no id
const cb2 = () => calls.push('cb2')
cb2.id = 2
const cb3 = () => calls.push('cb3')
cb3.id = 1

queuePostFlushCb(cb1)
queuePostFlushCb(cb2)
queuePostFlushCb(cb3)
await nextTick()
expect(calls).toEqual(['cb3', 'cb2', 'cb1'])
})

如果 queuePostFlushCb 接收的函数有 id 属性的话,会按照 id 升序加入队列,不指定的话默认为Infinity加入到最后

避免重复的 postFlushCb 调用

test('avoid duplicate postFlushCb invocation', async () => {
const calls: string[] = []
const cb1 = () => {
calls.push('cb1')
queuePostFlushCb(cb2)
}
const cb2 = () => {
calls.push('cb2')
}
queuePostFlushCb(cb1)
queuePostFlushCb(cb2)
await nextTick()
expect(calls).toEqual(['cb1', 'cb2'])
})

重复调用的 postFlushCb 不会执行

nextTick 应捕获调度程序刷新错误

test('nextTick should capture scheduler flush errors', async () => {
const err = new Error('test')
queueJob(() => {
throw err
})
try {
await nextTick()
} catch (e) {
expect(e).toBe(err)
}
expect(
`Unhandled error during execution of scheduler flush`
).toHaveBeenWarned()

// this one should no longer error
await nextTick()
})

nextTick 会捕获错误

默认情况下应防止自触发作业

test('should prevent self-triggering jobs by default', async () => {
let count = 0
const job = () => {
if (count < 3) {
count++
queueJob(job)
}
}
queueJob(job)
await nextTick()
// only runs once - a job cannot queue itself
expect(count).toBe(1)
})

默认情况下,job 不能递归调用自己

应该允许明确标记的作业触发自身

test('should allow explicitly marked jobs to trigger itself', async () => {
// normal job
let count = 0
const job = () => {
if (count < 3) {
count++
queueJob(job)
}
}
job.allowRecurse = true
queueJob(job)
await nextTick()
expect(count).toBe(3)

// post cb
const cb = () => {
if (count < 5) {
count++
queuePostFlushCb(cb)
}
}
cb.allowRecurse = true
queuePostFlushCb(cb)
await nextTick()
expect(count).toBe(5)
})

如果将 jobpostFlushCballowRecurse 属性指定为 true,那么它们可以递归调用自己

应该防止重复队列

test('should prevent duplicate queue', async () => {
let count = 0
const job = () => {
count++
}
job.cb = true
queueJob(job)
queueJob(job)
await nextTick()
expect(count).toBe(1)
})

防止重复调用 job

flushPostFlushCbs

test('flushPostFlushCbs', async () => {
let count = 0

const queueAndFlush = (hook: Function) => {
queuePostFlushCb(hook)
flushPostFlushCbs()
}

queueAndFlush(() => {
queueAndFlush(() => {
count++
})
})

await nextTick()
expect(count).toBe(1)
})

flushPostFlushCbs 会让 queuePostFlushCb 中的递归只执行一次

不运行 stopped reactive effects

test('should not run stopped reactive effects', async () => {
const spy = jest.fn()

// simulate parent component that toggles child
const job1 = () => {
// @ts-ignore
job2.active = false
}
// simulate child that's triggered by the same reactive change that
// triggers its toggle
const job2 = () => spy()
expect(spy).toHaveBeenCalledTimes(0)

queueJob(job1)
queueJob(job2)
await nextTick()

// should not be called
expect(spy).toHaveBeenCalledTimes(0)
})

如果 jobactive 属性为 false,那么 job 不会被执行

小结

  • queueJob 接收一个函数 job 作为参数,若 job 设置了 id ,那么按 id升序排序,否则按顺序保存到一个队列中,会去除重复的 jobjob1 中嵌套的 job2 会立即执行
  • queuePreFlushCb 接收一个函数 cb 作为参数,其他性质和 queueJob 相同
  • queuePostFlushCb 接收一个函数 cb 或一个函数数组 cbs 作为参数,其他性质和 queueJob 相同
  • queuePreFlushCbqueueJobqueuePostFlushCb 可以互相调用,且会立即执行
  • 如果在嵌套的 preFlushCb 中又调用了父 job,那么这次调用会被跳过
  • job1 中调用 queueJob(job2)job2中的 queuePostFlushCb 会在和 queueJob(job2) 同级的 queuePostFlushCb 执行后执行
  • invalidateJob 可以让一个 job 不执行
  • nextTick 会捕获错误
  • 默认情况下不允许递归的job等,除非指定了 allowRecursetrue
  • flushPostFlushCbs 会让 queuePostFlushCb 中的递归只执行一次
  • 优先级:queuePreFlushCb > queueJob > queuePostFlushCb
  • 如果 jobactive 属性为 false,那么 job 不会被执行

源码解析

数据结构

被调度的任务的数据结构 SchedulerJob

export interface SchedulerJob extends Function {
id?: number
active?: boolean
computed?: boolean
allowRecurse?: boolean
ownerInstance?: ComponentInternalInstance
}
  • id :让任务保持唯一性,队列中的任务按 id 升序排序
  • active :任务是否执行
  • computed
  • allowRecurse : 是否允许递归调用自身
  • ownerInstance

需要调度的任务队列 queue

const queue: SchedulerJob[] = []

两类四种回调函数的数据结构

// 异步任务队列中任务执行前的回调函数队列
const pendingPreFlushCbs: SchedulerJob[] = []
let activePreFlushCbs: SchedulerJob[] | null = null
let preFlushIndex = 0

// 异步任务队列中任务执行完成后的回调函数队列
const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerJob[] | null = null
let postFlushIndex = 0

nextTick

export function nextTick<T = void>(
this: T,
fn?: (this: T) => void
): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}

为了方便理解,我们可以将代码简化一下

const p = Promise.resolve()
export function nextTick(fn?: () => void): Promise<void> {
return fn ? p.then(fn) : p
}

其实就是用 Promise.resolve().thenfn 转换成一个微任务,加入微任务队列

queueJob 入队异步任务

export function queueJob(job: SchedulerJob) {
if (
(!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) &&
job !== currentPreFlushParentJob
) {
if (job.id == null) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}

在默认情况下,搜索的起始位置为当前任务,即不允许递归调用和重复添加

job.allowRecurse 的值为 true 时,将搜索起始位置加一,无法搜索到自身,也就是允许递归调用了。

然后根据有无 job.id 属性判断把任务放到最后还是按 id 升序排序,保证了队列刷新时任务能按照 id 升序正确排序

最后调用 queueFlush() 处理队列

queuePreFlushCb / queuePostFlushCb 处理回调

export function queuePreFlushCb(cb: SchedulerJob) {
queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}

export function queuePostFlushCb(cb: SchedulerJobs) {
queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
}

可以看到queuePreFlushCbqueuePostFlushCb其实是对queueCb的封装。它们之间的区别仅有传递进去的参数的不同。下面我们来看一下 queueCb 这个函数:

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 {
pendingQueue.push(...cb)
}
queueFlush()
}

入队的逻辑和异步任务的处理基本上是一致的。一方面做了去重,另一方面依照配置处理了递归的逻辑。 另外的,如果回调是一个数组,它会是组件的生命周期钩子函数。这组函数仅可被异步任务调用,且已经完成去重了。所以这里直接将数组拉平为一维,推入 pendingQueue 中。这部分是 Vue 自身的设计。

queueFlush 推入微任务队列

入队完成后,我们纠结着需要开始处理异步任务了。我们先来看两个全局变量,它们控制着刷新逻辑:

let isFlushing = false
let isFlushPending = false

在这里,如果没有正在等待或正在执行的任务,我们就会将 flushJobs 塞入引擎的微任务队列:

const resolvedPromise: Promise<any> = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null

function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}

通过这样的设计,确保了你可以在一个 tick 内可以多次添加任务。同时引擎在执行完主调用栈的函数后,一定会调用一次微任务队列中的 flushJobs

flushJobs 处理异步任务

我们之前通过

resolvedPromise.then(flushJobs)

flushJobs加入到了微任务队列,那么flushJobs就会在引擎处理下一个微任务队列时执行

首先看一下回调的处理时机:

type CountMap = Map<SchedulerJob | SchedulerCb, number>
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
// ...
flushPreFlushCbs(seen)

// 处理异步任务

flushPostFlushCbs(seen)
isFlushing = false
}

事实上就是通过这两个函数,分别执行回调函数队列的。

另外的,在实际处理异步任务队列前,我们还需要对任务队列做一次排序,使队列的任务按 id 升序排序

const getId = (job: SchedulerJob): number =>
job.id == null ? Infinity : job.id!
function flushJobs(seen?: CountMap) {
flushPreFlushCbs(seen)
queue.sort((a, b) => getId(a) - getId(b))
// 处理异步任务
}

这么做的原因有二,源码上的注释是这么说的:

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,即更高的优先级)
  • 如果一个组件在其父组件的更新过程中被卸载,它的更新可以被跳过

现在让我们来看看异步任务处理的部分

主要代码如下:

for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}

遍历队列,并执行这些任务

另外有些异步任务在执行的时候也会添加新的异步任务进去,那么我们就将它们也执行完

if (queue.length || pendingPreFlushCbs.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}

flushPreFlushCbs 处理异步任务前时的回调

export function flushPreFlushCbs(
seen?: CountMap,
parentJob: SchedulerJob | null = null
) {
if (pendingPreFlushCbs.length) {
currentPreFlushParentJob = parentJob
activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
pendingPreFlushCbs.length = 0

for (
preFlushIndex = 0;
preFlushIndex < activePreFlushCbs.length;
preFlushIndex++
) {
activePreFlushCbs[preFlushIndex]()
}
activePreFlushCbs = null
preFlushIndex = 0
currentPreFlushParentJob = null
// recursively flush until it drains
flushPreFlushCbs(seen, parentJob)
}
}

逻辑很清楚,就是遍历 activePreFlushCbs 队列,依次执行函数。 注意最后递归调用了 flushPreFlushCbs 函数,用来处理递归。在递归的过程中,可能会改变队列,所以我们在正式处理前,拷贝了一份队列的副本:

activePreFlushCbs = [...new Set(pendingPreFlushCbs)]

flushPostFlushCbs 处理异步任务处理完成后的回调

export function flushPostFlushCbs(seen?: CountMap) {
if (pendingPostFlushCbs.length) {
const deduped = [...new Set(pendingPostFlushCbs)]
pendingPostFlushCbs.length = 0

// #1947 already has active queue, nested flushPostFlushCbs call
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}

activePostFlushCbs = deduped

activePostFlushCbs.sort((a, b) => getId(a) - getId(b))

for (
postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
activePostFlushCbs[postFlushIndex]()
}
activePostFlushCbs = null
postFlushIndex = 0
}
}

flushPostFlushCbsflushPreFlushCbs 逻辑大同小异,flushPostFlushCbs 其中还会处理嵌套的情况,让嵌套的函数执行一次

if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}

也就是这个用例

// #1947 flushPostFlushCbs should handle nested calls
// e.g. app.mount inside app.mount
test('flushPostFlushCbs', async () => {
let count = 0

const queueAndFlush = (hook: Function) => {
queuePostFlushCb(hook)
flushPostFlushCbs()
}

queueAndFlush(() => {
queueAndFlush(() => {
count++
})
})

await nextTick()
expect(count).toBe(1)
})

另外的,在 flushJob 函数调用 flushPostFlushCbs 函数后,还将 isFlushing 重置为了 false。这是为了处理新添加的异步任务。如果有的话,flushJob 会继续递归,直到处理完所有的异步任务。

flushPostFlushCbs(seen)
isFlushing = false

总结

总的来说 nextTick 的实现主要利用了

  • 利用Promise.resolve().then()将任务推入 Micro Task Queue ,借助引擎的 Event Loop 机制处理队列中的任务
  • 处理异步任务与回调,对于新添加的异步任务也递归的处理完成。这与引擎处理 Task Queue 的逻辑是一致的

refs

Vue3 源码解析:nextTick

Vue3 源码阅读笔记(六)—— nextTick 与调度器