通过 scheduler 和微任务队列合并组件更新
2025-10-29 11:35:33
上一节中,组件已经可以通过 ReactiveEffect 完成响应式更新。
但是,目前每次修改响应式数据时,组件都会立即重新渲染。
import { createApp, h, ref } from '../dist/vue.esm.js'
const Com = {
setup() {
const count = ref(0)
btn.onclick = () => {
count.value++
count.value++
count.value++
}
return () => {
console.count('render')
return h('div', count.value)
}
}
}
createApp(Com, { msg: '消息' }).mount('#app')
点击一次按钮,count.value 连续修改了三次。
由于响应式更新默认是同步的,每次修改都会立即触发组件的渲染函数:
count.value++ -> render
count.value++ -> render
count.value++ -> render
最终页面只需要展示最后一次更新的结果,中间两次渲染实际上没有必要。
因此,组件更新不能在依赖触发时立即执行,而是应该先放入异步队列,在当前同步代码执行结束后统一处理。
延迟更新
ReactiveEffect 支持通过 scheduler 自定义依赖触发后的行为。
const effect = new ReactiveEffect(componentUpdateFn)
const update = effect.run.bind(effect)
effect.scheduler = () => {
Promise.resolve().then(() => {
update()
})
}
update()
在响应式数据发生变化时,不再立即执行 componentUpdateFn,而是由 scheduler 把更新放入 Promise 微任务中。
这样可以把更新推迟到当前同步任务结束之后执行。
实现 nextTick
组件更新是异步的,因此调用 count.value++ 后,DOM 不会立即完成更新:
count.value++
console.log(el.textContent)
此时读取到的可能仍然是旧内容。
如果需要等待本轮更新结束,所以提供 nextTick:
export function nextTick(fn) {
return resolvedPromise.then(() => fn.call(this))
}
使用时,将读取 DOM 的逻辑放到 nextTick 中:
count.value++
nextTick(() => {
console.log(el.textContent)
})
nextTick 本质上也是等待 Promise 微任务执行,因此可以在当前同步代码结束后执行回调。