Skip to content

Vue 中同步赋值与异步赋值的渲染次数问题

一个反直觉的问题

下面的代码在 Vue 中分别会导致组件渲染多少次?

js
// 代码段 A:同步赋值
this.count = 1
this.count = 2
this.count = 3

// 代码段 B:异步赋值
setTimeout(() => { this.count = 1 }, 0)
setTimeout(() => { this.count = 2 }, 0)
setTimeout(() => { this.count = 3 }, 0)

直觉可能会认为 A 渲染三次、B 也渲染三次。实际结果是:A 只渲染一次,B 渲染三次。

同步赋值:一次更新

Vue 的响应式更新采用异步队列机制。当响应式数据发生变化时,Vue 不会立即更新 DOM,而是将 watcher 推入一个队列中。同一个 watcher 在队列中只会出现一次(自动去重)。

在上面 A 场景中,三次连续赋值发生在同一个同步代码块内。第一次赋值 count = 1 时,Vue 把该组件的渲染 watcher 放入微任务队列。紧接着 count = 2count = 3 时,watcher 已经存在于队列中,不会重复添加。

等到同步代码执行完毕,事件循环进入微任务阶段,Vue 从队列中取出唯一的渲染 watcher 执行——此时 count 的值已经是 3,一次渲染就完成了。

异步赋值:三次独立更新

B 场景的情况不同。三个 setTimeout 的回调被分发到了同一个宏任务队列,但它们分别在不同的任务中执行。

第一次 setTimeout 回调触发 count = 1,此时其他两个赋值尚未发生,Vue 判断数据确实变了,将 watcher 加入微任务队列,微任务执行后渲染一次。第二次 setTimeout 回调时,上一次的微任务早已执行完毕,count = 2 又触发新一轮的 watcher 入队与渲染。第三次同理。

所以异步场景下每次都产生了独立的渲染更新周期,共渲染了三次。

$nextTick 的关系

开发者经常使用 $nextTick 来获取更新后的 DOM:

js
this.count = 1
this.$nextTick(() => {
  // 此时 DOM 已经更新为 count = 1
})

$nextTick 本质上就是向 Vue 的异步更新队列注册一个回调,它在当前数据变更所触发的 DOM 更新完成后执行。在异步赋值场景中,每一个 setTimeout 回调内部都形成了一个独立的"数据变更 → nextTick → DOM 更新"周期。

实际开发中的影响

这个机制的存在意味着:在同一个同步执行上下文中多次修改同一个响应式数据,不会造成多余的渲染。但如果数据修改被分散在不同的异步任务中(比如多次请求的回调分别更新 state),每一次都可能触发一次渲染。对于需要批量更新多个关联状态的场景,可以考虑将修改收敛到同一个同步操作中,或者使用 nextTick 来协调时序。