D02 · 调度器与 nextTick
对应主课: L35 调度器原理 最后核对: 2026-04-01
1. 为什么需要异步更新
javascript
const count = ref(0)
// 连续修改 3 次
count.value = 1
count.value = 2
count.value = 3
// 如果同步更新 → render 执行 3 次 → DOM 更新 3 次
// 异步批量更新 → render 只执行 1 次,直接用最终值 3Vue 的策略:数据变化不立即更新 DOM,而是在当前同步代码执行完后,通过微任务批量更新。
2. 更新队列
typescript
const queue = new Set<Function>() // Set 自动去重
let isFlushing = false
function queueJob(job: Function) {
queue.add(job) // 同一个组件的 update 函数只会入队一次
if (!isFlushing) {
isFlushing = true
Promise.resolve().then(flushJobs) // 微任务中执行
}
}
function flushJobs() {
for (const job of queue) { job() }
queue.clear()
isFlushing = false
}3. nextTick 详解
typescript
const resolvedPromise = Promise.resolve()
function nextTick(fn?: () => void): Promise<void> {
return fn ? resolvedPromise.then(fn) : resolvedPromise
}执行顺序:
同步代码(修改数据)
↓
微任务 1:flushJobs(DOM 更新)
↓
微任务 2:nextTick 回调
↓
浏览器渲染使用场景
typescript
// 1. 修改数据后读取 DOM
count.value = 100
await nextTick()
console.log(el.textContent) // '100'
// 2. 列表新增后滚动到底部
items.value.push(newItem)
await nextTick()
container.scrollTop = container.scrollHeight
// 3. v-if 切换后聚焦
showInput.value = true
await nextTick()
inputRef.value?.focus()4. 事件循环中的位置
5. watch 的 flush 选项
typescript
// flush: 'pre'(默认)— DOM 更新之前
watch(source, callback)
// flush: 'post' — DOM 更新之后
watch(source, callback, { flush: 'post' })
// 简写:watchPostEffect(() => { /* DOM 已更新 */ })
// flush: 'sync' — 同步执行(不推荐)
watch(source, callback, { flush: 'sync' })6. 动手实验:mini 调度器
在浏览器控制台运行,体验"批量异步更新":
javascript
// ===== Mini Scheduler(可直接运行) =====
const queue = new Set()
let isFlushing = false
let flushCount = 0
function queueJob(job) {
queue.add(job) // Set 自动去重
if (!isFlushing) {
isFlushing = true
Promise.resolve().then(() => {
flushCount++
console.log(`\n🔄 第 ${flushCount} 次 flush,队列中有 ${queue.size} 个任务`)
for (const job of queue) job()
queue.clear()
isFlushing = false
})
}
}
// 模拟组件 A 的 update 函数
const updateA = () => console.log(' 🖥️ 组件 A 更新,count =', countA)
// 模拟组件 B 的 update 函数
const updateB = () => console.log(' 🖥️ 组件 B 更新,name =', nameB)
// 模拟连续修改
let countA = 0, nameB = 'Vue'
console.log('--- 开始同步修改 ---')
countA = 1; queueJob(updateA) // 入队
countA = 2; queueJob(updateA) // Set 自动跳过(已存在)
countA = 3; queueJob(updateA) // Set 自动跳过
nameB = 'Vue 3'; queueJob(updateB) // 入队(不同任务)
console.log('--- 同步修改结束,此时 DOM 尚未更新 ---')
// 微任务执行后才会看到:
// 🔄 第 1 次 flush,队列中有 2 个任务
// 🖥️ 组件 A 更新,count = 3 ← 只更新一次,直接用最终值
// 🖥️ 组件 B 更新,name = Vue 37. 组件更新顺序
Vue 按组件的 uid(创建顺序) 排序更新队列,确保父组件先于子组件更新:
typescript
// Vue 源码简化:flush 前排序
function flushJobs() {
queue.sort((a, b) => a.id - b.id) // uid 升序 → 父先子后
for (const job of queue) {
job()
}
}8. Pre 队列 vs Post 队列
Vue 内部实际有两个队列:
这就是为什么 flush: 'post' 的 watch 可以安全地操作 DOM。
9. 常见陷阱
陷阱 1:修改数据后立即读 DOM
typescript
const count = ref(0)
count.value = 100
// ❌ 此时 DOM 还没更新!
console.log(el.textContent) // 仍然是 '0'
// ✅ 等待 DOM 更新
await nextTick()
console.log(el.textContent) // '100'陷阱 2:在 nextTick 里修改数据引发连锁更新
typescript
watch(count, async () => {
await nextTick()
count.value++ // ⚠️ 再次触发更新 → 再次 nextTick → 再次修改...
// 不会死循环(Vue 有递归限制),但性能差
})陷阱 3:flush: 'sync' 的性能问题
typescript
// ❌ 同步 watch — 每次修改都立即执行回调
watch(count, callback, { flush: 'sync' })
count.value = 1 // callback 执行 1 次
count.value = 2 // callback 执行 1 次
count.value = 3 // callback 执行 1 次
// 共 3 次!没有批量优化
// ✅ 默认异步 — 批量执行
watch(count, callback)
count.value = 1
count.value = 2
count.value = 3
// 只执行 1 次 callback,值为 310. 总结
- 批量更新 = Set 去重 + Promise 微任务
- nextTick = 在 DOM 更新之后的微任务
- 父组件先于子组件更新(uid 排序)
- Vue 有 Pre 和 Post 两个队列,分别在 DOM 更新前后执行
flush: 'post'的 watch 可以安全访问更新后的 DOM- 避免在 nextTick 中再修改数据,避免使用
flush: 'sync'