目录
- 前言
- 一、「渲染调度机制」总览
- 二、为什么需要"调度"?
- [三、Vue 的队列](#三、Vue 的队列)
-
- [1、Vue 组件更新会立即执行吗?](#1、Vue 组件更新会立即执行吗?)
- [2、Vue 的三种核心队列](#2、Vue 的三种核心队列)
-
- (1)、调度总入口:queueJob()
- (2)、queueFlush():开启"异步更新"
- (3)、真正执行更新:flushJobs()
-
- [①、先执行 pre 队列](#①、先执行 pre 队列)
- [②、再执行组件更新(render + patch)](#②、再执行组件更新(render + patch))
- [③、最后执行 post 队列](#③、最后执行 post 队列)
- 3、为什么还要排序?
- 四、调度机制解决了哪些问题?
- [五、nextTick 本质是什么?(⭐️⭐️⭐️⭐️⭐️)](#五、nextTick 本质是什么?(⭐️⭐️⭐️⭐️⭐️))
- 六、整个调度流程串起来
前言
Vue3 在响应式数据触发更新时,不会立即执行组件渲染,而是通过调度器将更新任务放入队列,并利用微任务在当前事件循环结束后统一批量执行。调度器内部会对任务去重,并按照组件创建顺序排序,确保父子组件更新顺序正确。同时 Vue 还维护 pre 和 post 两类副作用队列,用于控制 watch 等副作用的执行时机,从而实现高性能、可预测的 UI 更新机制。
一、「渲染调度机制」总览
Vue3 渲染调度机制 = 把多次状态变化合并起来,按顺序、分批次、异步地执行组件更新
关键词就三个:
- 队列
- 去重
- 异步批处理
二、为什么需要"调度"?
看一段代码:
typescript
state.count++
state.count++
state.count++
如果每次修改都立刻:
typescript
trigger → effect → render → patch
那页面会 连续重渲染 3 次,纯纯浪费性能。
Vue 的目标是:
- 不管你一口气改多少次数据,一个组件一轮事件循环只更新一次
这就是调度器的使命。
三、Vue 的队列
1、Vue 组件更新会立即执行吗?
不会。
Vue 组件更新不会立刻执行,而是先进"更新队列"。
当响应式数据触发 trigger() 时:
typescript
triggerEffects(dep)
内部不会直接执行组件的副作用,而是:
typescript
queueJob(effect)
也就是说:
❗组件更新 = 被放进一个"待执行任务队列"
2、Vue 的三种核心队列
| 队列 | 存什么 | 什么时候执行 |
|---|---|---|
queue |
组件更新任务 | 主要渲染阶段 |
pendingPreFlushCbs |
watchEffect / watch (flush:'pre') |
渲染前 |
pendingPostFlushCbs |
watch(..., { flush:'post' }) |
DOM 更新后 |
(1)、调度总入口:queueJob()
源码逻辑(简化):
typescript
const queue: Job[] = []
let isFlushing = false
export function queueJob(job) {
if (!queue.includes(job)) { // ⭐ 去重
queue.push(job)
queueFlush()
}
}
关键点 1:去重
同一个组件不管触发多少次,只会保留一个更新任务。
(2)、queueFlush():开启"异步更新"
typescript
function queueFlush() {
if (!isFlushing) {
isFlushing = true
Promise.resolve().then(flushJobs)
}
}
关键点 2:微任务异步执行
Vue 使用的是:
typescript
Promise.then → 微任务
这就解释了为什么:
typescript
state.count++
console.log(dom) // 旧 DOM
await nextTick()
console.log(dom) // 新 DOM
因为 DOM 更新在 本轮同步代码执行完后才发生。
(3)、真正执行更新:flushJobs()
typescript
function flushJobs() {
try {
flushPreFlushCbs() // 1️⃣ 执行 pre 队列
queue.sort(sortJob) // 2️⃣ 排序(父 → 子)
for (job of queue) { // 3️⃣ 执行组件更新
job()
}
} finally {
flushPostFlushCbs() // 4️⃣ 执行 post 队列
resetSchedulerState()
}
}
执行顺序非常重要!
- 先执行 pre 队列
- 再执行组件更新(render + patch)
- 最后执行 post 队列
①、先执行 pre 队列
用于:
typescript
watchEffect(fn) // 默认 flush: 'pre'
watch(source, fn)
此时 DOM 还没更新,适合做:
- 读取旧 DOM 状态
- 计算下一步逻辑
②、再执行组件更新(render + patch)
就是我们熟悉的:
typescript
effect → render → patch
这一步才真正改 DOM。
③、最后执行 post 队列
用于:
typescript
watch(source, fn, { flush: 'post' })
此时 DOM 已经是最新的。
适合:
- 访问更新后的 DOM
- 操作第三方库
3、为什么还要排序?
typescript
queue.sort((a, b) => getId(a) - getId(b))
组件创建时有递增 id:
typescript
父组件 id < 子组件 id
排序的意义是:
- ✅ 保证父组件先更新,子组件后更新
否则可能出现:
- 子组件基于旧 props 更新
- 父组件晚更新导致数据错乱
四、调度机制解决了哪些问题?
| 问题 | Vue 怎么解决 |
|---|---|
| 多次数据变更频繁重渲染 | 队列去重 + 批量执行 |
| 更新顺序错乱 | 按组件创建顺序排序 |
| DOM 访问时机混乱 | pre / post 队列分离 |
| 同步更新阻塞 UI | 微任务异步更新 |
五、nextTick 本质是什么?(⭐️⭐️⭐️⭐️⭐️)
typescript
export function nextTick(fn?) {
return fn ? Promise.resolve().then(fn) : Promise.resolve()
}
它只是:
- 等待当前这轮"组件更新批处理"结束。
所以:
typescript
state.count++
await nextTick()
// 这里 DOM 一定是新的
六、整个调度流程串起来
typescript
响应式数据改变
│
▼
trigger()
│
▼
queueJob(组件effect)
│
▼
Promise.then (微任务)
│
▼
flushJobs()
│ │
▼ ▼
pre队列 组件render+patch
│
▼
post队列