在 Vue 开发中,你是否写过这样的代码:
js
this.message = 'New Value';
console.log(this.$el.textContent); // ❓ 为什么还是旧值?
"我明明改了数据,DOM 怎么没变?"
这背后是 Vue 异步更新队列的精妙设计。
本文将从 事件循环 到 源码级实现,彻底解析 Vue 为何不立即渲染。
一、核心结论
Vue 的 DOM 更新是异步的。
js
// 数据变化
this.message = 'Hello';
// ❌ 此时 DOM 尚未更新
console.log(this.$el.textContent); // 可能仍是旧值
// ✅ 在下一个 "tick" 后,DOM 才更新
this.$nextTick(() => {
console.log(this.$el.textContent); // ✅ 'Hello'
});
二、为什么异步更新?
✅ 1. 性能优化:避免重复渲染
js
// 同步更新:❌ 3 次 render
this.a = 1; // render()
this.b = 2; // render()
this.c = 3; // render()
// 异步更新:✅ 1 次 render
this.a = 1;
this.b = 2;
this.c = 3; // 批量更新
✅ 2. 避免无效计算
js
this.items.push(1);
this.items.push(2);
this.items.pop(); // 最终 items 未变
- 同步更新:执行 3 次
patch
; - 异步更新:去重后发现无需更新,跳过渲染。
✅ 3. 符合浏览器渲染机制
浏览器在 一次事件循环结束 后才进行重排(reflow)和重绘(repaint)。
Vue 的异步更新与浏览器节奏同步,避免强制同步布局(Forced Synchronous Layout)。
三、更新机制:微任务队列
🔄 事件循环(Event Loop)回顾
text
| 宏任务 (MacroTask) |
↓
| 微任务 (MicroTask) | → 清空所有微任务
↓
| 渲染 (Render) | ← DOM 更新在此发生
↓
| 下一个宏任务 |
✅ Vue 的更新策略
- 数据变化 → 触发
setter
; Watcher
被推入 异步队列;- 使用
Promise.then
(微任务)延迟执行; - 在当前宏任务结束前,清空队列,批量更新 DOM。
四、源码级解析:queueWatcher
📌 核心逻辑
js
const queue = [];
let waiting = false;
function queueWatcher(watcher) {
const id = watcher.id;
// ❌ 去重:同一 watcher 只入队一次
if (queue.indexOf(watcher) === -1) {
queue.push(watcher);
}
// ✅ 延迟刷新
if (!waiting) {
waiting = true;
// 使用微任务(如 Promise)延迟执行
nextTick(flushSchedulerQueue);
}
}
function flushSchedulerQueue() {
// 排序:父组件先于子组件
queue.sort((a, b) => a.id - b.id);
// 批量更新
for (let i = 0; i < queue.length; i++) {
const watcher = queue[i];
watcher.run(); // 执行组件更新
}
// 重置
queue.length = 0;
waiting = false;
}
五、实战演示
📌 场景 1:连续修改数据
js
methods: {
updateData() {
this.message = 'Step 1';
this.count = 1;
this.message = 'Final'; // 只触发一次更新
// ❌ DOM 未更新
console.log(this.$el.textContent); // 'Old Value'
// ✅ 在 nextTick 中,DOM 已更新
this.$nextTick(() => {
console.log(this.$el.textContent); // 'Final'
});
}
}
📌 场景 2:避免强制同步布局
js
// ❌ 反模式:强制同步布局
this.items.push(newItem);
console.log(this.listEl.scrollHeight); // 浏览器被迫同步计算布局
// ✅ 正确做法
this.items.push(newItem);
this.$nextTick(() => {
console.log(this.listEl.scrollHeight); // 在 DOM 更新后读取
});
六、Vue 2 vs Vue 3 的差异
版本 | 队列刷新时机 |
---|---|
Vue 2 | Promise.then (微任务) |
Vue 3 | queueMicrotask 或 Promise (微任务) |
💥 两者均为异步更新,行为一致。
七、如何强制"同步"更新?
❌ 不推荐:setTimeout
js
this.message = 'new';
setTimeout(() => {
console.log(this.$el.textContent); // 依赖宏任务,时机不可控
}, 0);
✅ 推荐:$nextTick
js
this.message = 'new';
this.$nextTick(() => {
// ✅ DOM 确保已更新
console.log(this.$el.textContent);
});
💡 结语
"Vue 的异步更新,是性能与正确性的完美平衡。"
问题 | 答案 |
---|---|
数据变,DOM 立即更新? | ❌ 异步 |
为何异步? | 避免重复渲染、提升性能 |
更新时机? | 当前事件循环的微任务阶段 |
如何获取更新后 DOM? | this.$nextTick() |
记住:
"不要假设数据变化后 DOM 立即更新。"
掌握异步更新机制,你就能:
✅ 避免 DOM 读取时机错误;
✅ 写出更高效的 Vue 代码;
✅ 理解 nextTick
的真正意义。