
目标:从调度器视角理解 Vue3 的
nextTick,掌握其与微任务、更新队列的关系,并提供一个可运行的简化版实现。
nextTick 用于等待本轮视图刷新完成,再执行回调或继续代码await nextTick() 后读取 DOM 或进行尺寸计算nextTick 返回当前一次队列刷新的 Promise,实现“等这一次刷新完成再继续”isFlushing 与 isFlushPending 防止重复调度type Job = (() => void) & { id?: number }
const queue: Job[] = []
let isFlushing = false
let isFlushPending = false
let currentFlushPromise: Promise<void> | null = null
const resolved = Promise.resolve()
export function queueJob(job: Job) {
if (!queue.includes(job)) {
queue.push(job)
}
queueFlush()
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolved.then(flushJobs)
}
}
function flushJobs() {
isFlushing = true
isFlushPending = false
queue.sort((a, b) => (getId(a) - getId(b)))
for (let i = 0; i < queue.length; i++) {
const job = queue[i]
job()
}
queue.length = 0
isFlushing = false
currentFlushPromise = null
}
function getId(fn: Job) { return fn.id ?? 0 }
export function nextTick<T = void>(fn?: () => T | Promise<T>) {
const p = currentFlushPromise ?? resolved
return fn ? p.then(fn) : p
}const jobA: Job = Object.assign(() => { console.log('A') }, { id: 1 })
const jobB: Job = Object.assign(() => { console.log('B') }, { id: 2 })
queueJob(jobB)
queueJob(jobA)
await nextTick()queueJob 入队currentFlushPromise = resolved.then(flushJobs) 提交一次刷新nextTick 返回 currentFlushPromise 或 resolved,保证回调在刷新之后执行flushJobs 执行、清空队列、重置状态,currentFlushPromise 设为 nullid 排序保证父组件先于子组件,避免不一致setTimeout(fn) 属于宏任务,会延后到下一轮事件循环,性能与时机不理想Promise.resolve().then(fn) 是微任务,满足刷新后的最早可执行时机MutationObserver 或 MessageChannel 作为微任务降级方案pre 与 post 刷新回调队列,支持 before/after flush 钩子nextTick 本质是对一次“队列刷新微任务”的等待封装nextTick 的协作关系,可作为学习与调试模板pre 副作用 → 渲染 jobs → post 回调queuePreFlushCb、queueJob、queuePostFlushCbpre,再主队列 jobs,最后 posttype Fn = () => void
const preCbs: Fn[] = []
const postCbs: Fn[] = []
const jobs: Fn[] = []
let isFlushing = false
let isPending = false
let currentFlushPromise: Promise<void> | null = null
const resolved = Promise.resolve()
export function queuePreFlushCb(cb: Fn) { if (!preCbs.includes(cb)) preCbs.push(cb); schedule() }
export function queueJob(job: Fn) { if (!jobs.includes(job)) jobs.push(job); schedule() }
export function queuePostFlushCb(cb: Fn) { if (!postCbs.includes(cb)) postCbs.push(cb); schedule() }
function schedule() {
if (!isFlushing && !isPending) { isPending = true; currentFlushPromise = resolved.then(flushAll) }
}
function flushAll() {
isFlushing = true
isPending = false
for (let i = 0; i < preCbs.length; i++) preCbs[i]()
preCbs.length = 0
for (let i = 0; i < jobs.length; i++) jobs[i]()
jobs.length = 0
for (let i = 0; i < postCbs.length; i++) postCbs[i]()
postCbs.length = 0
isFlushing = false
currentFlushPromise = null
}
export function nextTick<T = void>(fn?: () => T | Promise<T>) {
const p = currentFlushPromise ?? resolved
return fn ? p.then(fn) : p
}queueMicrotask 或 Promise.resolve().thenMutationObserver、MessageChannel、setTimeouttype Scheduler = (fn: () => void) => void
export function createMicrotaskScheduler(): Scheduler {
if (typeof queueMicrotask === 'function') return queueMicrotask
if (typeof Promise !== 'undefined') {
const p = Promise.resolve()
return fn => p.then(fn)
}
return fn => setTimeout(fn, 0)
}await nextTick() 再读布局v-if 后在 nextTick 中执行 el.focus()import { ref, nextTick } from 'vue'
const visible = ref(false)
const top = ref(0)
async function openAndMeasure(container: HTMLElement) {
visible.value = true
await nextTick()
top.value = container.getBoundingClientRect().top
}import { ref, nextTick } from 'vue'
const count = ref(0)
async function inc3() {
count.value++
count.value++
count.value++
await nextTick()
}await nextTick()setTimeout 代替 nextTick,宏任务会延后到下一轮,可能影响交互nextTick 链式调用无需担心顺序,它们共享同一刷新 Promisetype Job = { run: () => void; prio: number }
const q: Job[] = []
export function enqueue(job: Job) { q.push(job); scheduleFlush() }
function scheduleFlush() { Promise.resolve().then(flush) }
function flush() {
q.sort((a, b) => b.prio - a.prio)
for (let i = 0; i < q.length; i++) q[i].run()
q.length = 0
}effect 与调度器协作,支持更细粒度的更新合并nextTick:当你依赖 DOM 的最新布局或需要在视图更新后继续逻辑watch 回调里省略:若回调仅处理数据无需 DOM,则不必使用nextTick 的场景,验证刷新时机同步代码开始
│
├─ 修改响应式状态
│ └─ 副作用入队 jobs
│
├─ 调用 nextTick
│ └─ 获取当前刷新 Promise p
│
└─ 事件回调结束
└─ 执行微任务队列
└─ flushAll/flushJobs
├─ preCbs
├─ jobs
└─ postCbs
└─ DOM 已更新
└─ nextTick 回调执行nextTick(cb):将回调安排在当前刷新后的微任务中执行await nextTick():等待刷新完成后继续执行后续逻辑await,普通回调使用 nextTick(cb)Promise.resolve().then(() => console.log('microtask'))
setTimeout(() => console.log('macrotask'))
console.log('sync')nextTick 不等待网络、图片等资源加载,只与渲染刷新相关nextTick,仅当依赖更新后的布局时才需要nextTicknextTickimport { ref, nextTick } from 'vue'
const items = ref<number[]>([])
async function addBatch(n: number) {
const base = items.value.length
for (let i = 0; i < n; i++) items.value.push(base + i)
await nextTick()
document.body.dispatchEvent(new Event('list-enter'))
}nextTick 共享同一刷新 PromisenextTick,会使用当前正在进行的 Promise,保证时序一致currentFlushPromise 重置,下次将指向新的刷新requestAnimationFrame 着重在下一帧执行,适合动画与布局测量的帧级协作setImmediate 与 MessageChannel 可用于宏任务近似场景,但平台兼容性差异较大