【vue篇】Vue 响应式更新揭秘:数据变了,DOM 为何不立即刷新?

在 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 的更新策略

  1. 数据变化 → 触发 setter
  2. Watcher 被推入 异步队列
  3. 使用 Promise.then(微任务)延迟执行;
  4. 在当前宏任务结束前,清空队列,批量更新 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 queueMicrotaskPromise(微任务)

💥 两者均为异步更新,行为一致。


七、如何强制"同步"更新?

❌ 不推荐: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 的真正意义。

相关推荐
To_OC18 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic19 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy19 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志19 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble20 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯21 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜1 天前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司1 天前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g1 天前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森1 天前
Web 开发基础:HTTP 与 REST
前端·网络协议·http