vue3 nextTick 原理分析

1,844 阅读12分钟

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")
    }
  },
})

单元测试

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

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 与调度器