Skip to content

Vue 视图更新学习笔记:为什么是异步的?

一、核心面试题

  1. Q: Vue 的视图更新是同步的还是异步的?

    • A: 异步的 (Asynchronous)。
  2. Q: 既然是异步的,如何在数据变化后立即获取更新后的 DOM 内容?

    • A: 使用 nextTick(callback),在 callback 回调函数中即可获取。
  3. Q: Vue 为什么要采用异步更新策略?

    • A: 核心是为了性能优化。将同一次事件循环中的多次数据变化合并成一次 DOM 更新,避免不必要的计算和渲染开销。

二、为什么需要异步更新:同步更新的性能问题

如果我们不采用异步更新,而是同步更新,会发生什么?

1. 场景

在一次操作中,我们连续多次修改同一个响应式数据。

javascript
// 伪代码示例
function updateData() {
  this.count++; // 第一次修改
  this.count++; // 第二次修改
}

2. 同步更新的流程

  • this.count++ (count 变为 1) -> 立即触发 render 函数,更新 DOM。
  • this.count++ (count 变为 2) -> 再次立即触发 render 函数,更新 DOM。

3. 存在的问题

  • 性能浪费:在上面的例子中,视图从 0 更新到 1,然后马上又从 1 更新到 2。第一次的 DOM 更新是完全没有必要的,因为它立刻就被下一次更新覆盖了。
  • 频繁操作 DOM:每次数据变更都直接操作 DOM,会引发大量的重排 (Reflow) 和重绘 (Repaint),这在浏览器中是性能开销极大的操作。

三、异步更新的实现原理

为了解决同步更新的性能问题,Vue 引入了异步更新机制,其核心是更新队列 (Update Queue)

1. 工作流程图解

2. 详细步骤

  1. 监听数据变化:当你修改一个响应式数据时(如 this.count++),setter 会被触发。

  2. 加入更新队列:Vue 并不会立即执行 render 函数来更新视图,而是将这个更新任务(watcher 或 render 函数)添加到一个更新队列中。

  3. 任务去重:这个队列有一个很重要的特性——去重。通常使用 Set 数据结构来实现。如果你在同一次事件循环中多次修改同一个数据,对应的更新任务只会被添加到队列中一次

  4. 异步执行更新:在当前同步代码执行栈清空后(即一个 "tick" 结束时),Vue 会通过一个异步任务(通常是微任务 Promise.resolve().then())来清空并执行队列中的所有更新任务。

  5. 一次性更新 DOM:此时,Vue 会执行一次 render 函数,计算出最终的 VNode,然后与旧的 VNode 进行 diff 运算,最后将差异部分一次性应用到真实 DOM 上。

3. 示例说明

javascript
function updateData() {
  // 1. count++ (变为1), 将 render 函数放入更新队列 Set
  this.count++;
  // 2. count++ (变为2), 尝试将同一个 render 函数放入队列,
  //    由于 Set 特性,队列内容不变
  this.count++;
}

// 3. 同步代码执行完毕

// 4. 微任务开始执行,从队列中取出唯一的 render 函数

// 5. 执行 render 函数,此时 this.count 已经是最终值 2,
//    DOM 直接从 0 更新到 2

优点:无论你在一个 "tick" 内对数据进行多少次修改,最终都只会触发一次 render 和一次 DOM 更新,极大地提升了性能。


四、nextTick 的原理

1. 新的问题

由于更新是异步的,当我们修改完数据后,如果立即去获取 DOM 的内容,获取到的是更新前的旧值。

javascript
this.count = 2;
// 此时 DOM 还没有更新,拿到的 innerText 仍然是旧值
console.log(this.$refs.myElement.innerText);

2. nextTick 的解决方案

nextTick 的本质也是创建一个微任务,并将其回调函数推入微任务队列。

  • 执行顺序Vue 的 DOM 更新微任务 -> nextTick 的回调微任务
  • Vue 将 DOM 更新操作放进了微任务队列。
  • 我们使用 nextTick,相当于在 DOM 更新的微任务之后,又追加了一个我们自己的微任务。
  • 这就保证了 nextTick 的回调函数执行时,视图已经完成了更新。
javascript
this.count = 2;

this.$nextTick(() => {
  // 在这个回调函数里,可以确保 DOM 已经更新完毕
  console.log(this.$refs.myElement.innerText); // 获取到的是新值 "2"
});

五、总结

特性同步更新 (Hypothetical)异步更新 (Vue's Approach)
核心思想数据变,DOM 立刻变。数据变,将更新任务放入队列,延迟统一处理。
性能低效,多次变更导致多次无效的 DOM 操作。高效,将多次变更合并为一次 DOM 操作。
实现机制直接调用 render更新队列 (Set去重) + 微任务 (Promise)
获取新值可直接获取。需使用 nextTick 在下次 DOM 更新循环后获取。