面试里,当面试官把两段看似「都是改 5 次数据」的代码摆在你面前,却问「渲染了几次?」,如果你只回答「改了 5 次所以 5 次」,那大概率就踩坑了。本文用 100 行代码把同步批处理与异步微任务的底层机制拆开讲透,让你以后遇到同类问题直接秒答。
一、手写一个带合并渲染的 Component
需求
- 修改数据时触发
render
- 同步多次修改只触发一次
render
实现思路
利用 微任务队列 把同一次事件循环里的多次 set
合并到下一帧执行。
js
class Component {
data = { name: '' }
_pending = false // 标记位:是否有未 flush 的修改
constructor() {
// 通过 Proxy 拦截所有属性写入
this.data = new Proxy(this.data, {
set: (target, key, value) => {
target[key] = value
this.scheduleRender()
return true
}
})
}
scheduleRender() {
if (this._pending) return // 已在队列中,跳过
this._pending = true
queueMicrotask(() => {
this.render()
this._pending = false
})
}
render() {
console.log(`render - name: ${this.data.name}`)
}
}
// 测试
const com = new Component()
com.data.name = '张三'
com.data.name = '李四'
com.data.name = '王五'
setTimeout(() => com.data.name = '巷子', 0)
输出顺序:
arduino
render - name: 王五 // 第一帧批处理
render - name: 巷子 // setTimeout 宏任务
核心原理:queueMicrotask
把多次写操作合并到同一微任务阶段,只执行一次 render
。
二、Vue 中同步 vs 异步赋值到底渲染几次?
代码一:同步 for 循环
vue
<script setup>
import { ref } from 'vue'
const rCount = ref(0)
for (let i = 1; i <= 5; ++i) {
rCount.value = i
}
</script>
渲染次数:2 次
- 初始挂载:渲染
0
- 批处理队列:5 次赋值被合并,最终渲染
5
Vue 内部使用 异步队列(queueJob) 收集同步变更,下一事件循环统一 flush。同一代码块内无论改多少次,都只走一次 DOM diff。
代码二:setTimeout 异步循环
vue
<script setup>
import { ref } from 'vue'
const rCount = ref(0)
for (let i = 1; i <= 5; ++i) {
setTimeout(() => rCount.value = i, 0)
}
</script>
渲染次数:6 次
- 初始挂载:渲染
0
- 每个
setTimeout
回调都是一个独立宏任务,Vue 的批处理无法跨任务合并,于是 5 次回调触发 5 次独立渲染。
总结
同步代码 → 全部进入同一批处理队列 → 1 次渲染
异步代码 → 每次回调独立任务 → n 次渲染