Vue 中同步赋值与异步赋值的渲染次数问题
一个反直觉的问题
下面的代码在 Vue 中分别会导致组件渲染多少次?
// 代码段 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 = 2 和 count = 3 时,watcher 已经存在于队列中,不会重复添加。
等到同步代码执行完毕,事件循环进入微任务阶段,Vue 从队列中取出唯一的渲染 watcher 执行——此时 count 的值已经是 3,一次渲染就完成了。
异步赋值:三次独立更新
B 场景的情况不同。三个 setTimeout 的回调被分发到了同一个宏任务队列,但它们分别在不同的任务中执行。
第一次 setTimeout 回调触发 count = 1,此时其他两个赋值尚未发生,Vue 判断数据确实变了,将 watcher 加入微任务队列,微任务执行后渲染一次。第二次 setTimeout 回调时,上一次的微任务早已执行完毕,count = 2 又触发新一轮的 watcher 入队与渲染。第三次同理。
所以异步场景下每次都产生了独立的渲染更新周期,共渲染了三次。
与 $nextTick 的关系
开发者经常使用 $nextTick 来获取更新后的 DOM:
this.count = 1
this.$nextTick(() => {
// 此时 DOM 已经更新为 count = 1
})$nextTick 本质上就是向 Vue 的异步更新队列注册一个回调,它在当前数据变更所触发的 DOM 更新完成后执行。在异步赋值场景中,每一个 setTimeout 回调内部都形成了一个独立的"数据变更 → nextTick → DOM 更新"周期。
实际开发中的影响
这个机制的存在意味着:在同一个同步执行上下文中多次修改同一个响应式数据,不会造成多余的渲染。但如果数据修改被分散在不同的异步任务中(比如多次请求的回调分别更新 state),每一次都可能触发一次渲染。对于需要批量更新多个关联状态的场景,可以考虑将修改收敛到同一个同步操作中,或者使用 nextTick 来协调时序。