2026.01.22
组件异步更新 -> Scheduler 与 nextTick 的设计与实现
在上一章中,我们实现了 Setup 返回渲染函数的能力。然而,在响应式数据多次变更时,每次变更都会触发一次组件更新,这会带来不必要的性能开销。
在上一章中,我们实现了 Setup 返回渲染函数的能力。然而,在响应式数据多次变更时,每次变更都会触发一次组件更新,这会带来不必要的性能开销。
Vue 通过异步更新机制来解决这个问题,将多次状态变更合并为一次更新。
问题场景
让我们先看一个多次触发渲染的典型场景:
<button id="btn">count++</button>
<div id="app"></div>
<script type="module">
import { h, createApp, ref } from '../dist/vue.esm.js'
const Comp = {
setup(props, { attrs }) {
const count = ref(0)
btn.onclick = () => {
// 连续两次修改 count
count.value++
count.value++
}
return () => {
console.count('render') // 会打印两次
return h('div', count.value)
}
}
}
createApp(Comp, { msg: 'msg', count: 0, a: 1 }).mount('#app')
</script>
核心问题:
- 连续两次修改
count.value会触发两次渲染 - 控制台会打印两次
render - 第一次渲染是无效的,浪费性能
解决思路:
将同步的渲染调用改为异步,在同一个事件循环中多次触发更新时,只执行最后一次渲染。
当前渲染机制分析
在之前的实现中,mountComponent 的逻辑如下:
/**
* 挂载组件
*/
function mountComponent(vnode, container, anchor) {
const instance = createComponentInstance(vnode)
setupComponent(instance)
/**
* 组件更新函数
*/
const componentUpdateFn = () => {
if (!instance.isMounted) {
const subTree = instance.render.call(instance.proxy)
patch(null, subTree, container, anchor)
instance.subTree = subTree
instance.isMounted = true
}
else {
const prevSubTree = instance.subTree
const subTree = instance.render.call(instance.proxy)
patch(prevSubTree, subTree, container, anchor)
instance.subTree = subTree
}
}
/**
* 创建响应式 effect
* 1. 将 componentUpdateFn 传递给 ReactiveEffect
* 2. 在调用 run 时,会执行 componentUpdateFn
* 3. componentUpdateFn 调用 render,render 访问了响应式属性
* 4. 响应式属性变化时,render 会重新执行
* 5. 这个过程是同步的
*/
const effect = new ReactiveEffect(componentUpdateFn)
effect.run()
}
工作流程:
ReactiveEffect.run()
└─> componentUpdateFn()
└─> render()
└─> 访问响应式属性 (count.value)
└─> 触发依赖收集
响应式属性变化
└─> 立即执行 componentUpdateFn (同步)
└─> 重新渲染
问题所在:
- 响应式属性变化时,会立即执行
componentUpdateFn - 每次属性变化都会触发一次渲染
- 无法合并多次变更
引入调度器
Scheduler 基本概念
Vue 使用 Scheduler(调度器) 来控制组件更新的时机,将同步渲染改为异步渲染。
function mountComponent(vnode, container, anchor) {
const instance = createComponentInstance(vnode)
setupComponent(instance)
const componentUpdateFn = () => {
// ...
}
const effect = new ReactiveEffect(componentUpdateFn)
const update = effect.run.bind(effect)
/**
* 设置调度器
* 响应式属性变化时,不会立即执行 update
* 而是通过 Promise 微任务异步执行
*/
effect.scheduler = () => {
Promise.resolve().then(() => {
update()
})
}
update()
}
核心设计:
effect.scheduler:当响应式属性变化时,不会立即执行副作用函数,而是执行调度器Promise.resolve().then():使用微任务实现异步,确保在当前事件循环结束后执行- 同一事件循环中多次调用
scheduler,只会执行一次渲染
异步更新的优势
// 同步更新(之前)
count.value++ // 立即渲染 1
count.value++ // 立即渲染 2
// 异步更新(现在)
count.value++ // 调度渲染(未执行)
count.value++ // 调度渲染(未执行)
// 微任务队列执行:渲染 2(只渲染一次)
性能提升:
- 同一事件循环中的多次状态变更,只触发一次渲染
- 避免无效的中间状态渲染
新问题:DOM 访问时机
异步更新带来了新的问题:在修改响应式数据后立即访问 DOM,拿不到最新的内容。
问题场景
<button id="btn">count++</button>
<div id="app"></div>
<script type="module">
import { h, createApp, ref } from '../dist/vue.esm.js'
const Comp = {
setup(props, { attrs }) {
const count = ref(0)
btn.onclick = () => {
count.value++
count.value++
const container = document.querySelector('#container')
console.log(container.textContent) // 输出旧值,因为渲染是异步的
}
return () => {
return h('div', { id: 'container' }, count.value)
}
}
}
createApp(Comp, { msg: 'msg', count: 0, a: 1 }).mount('#app')
</script>
问题分析:
count.value++触发异步渲染console.log是同步执行的- 此时 DOM 还未更新,输出的是旧值
解决方案:nextTick
Vue 提供了 nextTick API,让开发者能够在 DOM 更新后执行回调函数。
btn.onclick = () => {
count.value++
count.value++
nextTick(() => {
const container = document.querySelector('#container')
console.log(container.textContent) // 输出新值
})
}
抽离代码
创建 scheduler 模块
我们将调度相关的代码抽取到独立模块 scheduler.ts 中:
/**
* 调度任务到微任务队列
* @param job - 要执行的任务函数
*/
export function queueJob(job) {
Promise.resolve().then(() => {
job()
})
}
/**
* 在下一个 DOM 更新周期后执行回调
* @param fn - 要执行的回调函数
*/
export function nextTick(fn) {
return Promise.resolve().then(() => fn.call(this))
}
核心设计:
queueJob:将任务调度到微任务队列nextTick:返回一个 Promise,在微任务队列中执行回调- 使用
fn.call(this)保留this指向
更新 mountComponent
/**
* 挂载组件
*/
function mountComponent(vnode, container, anchor) {
const instance = createComponentInstance(vnode)
setupComponent(instance)
const componentUpdateFn = () => {
if (!instance.isMounted) {
const subTree = instance.render.call(instance.proxy)
patch(null, subTree, container, anchor)
instance.subTree = subTree
instance.isMounted = true
}
else {
const prevSubTree = instance.subTree
const subTree = instance.render.call(instance.proxy)
patch(prevSubTree, subTree, container, anchor)
instance.subTree = subTree
}
}
const effect = new ReactiveEffect(componentUpdateFn)
const update = effect.run.bind(effect)
/**
* 将 update 保存到实例,供 $forceUpdate 使用
*/
instance.update = update
/**
* 设置调度器
* 响应式属性变化时,通过 queueJob 调度更新
*/
effect.scheduler = () => {
queueJob(update)
}
/**
* 首次挂载,立即执行更新
*/
update()
}
添加公共 API:$nextTick 与 $forceUpdate
Vue 提供了实例方法 $nextTick 和 $forceUpdate,我们需要在公共属性映射表中添加它们。
更新公共属性映射表
/**
* 公共属性映射表
* 将 $attrs、$slots、$refs、$nextTick、$forceUpdate 映射到实例对应的属性
*/
const publicPropertiesMap = {
$attrs: instance => instance.attrs,
$slots: instance => instance.slots,
$refs: instance => instance.refs,
$nextTick: (instance) => {
return nextTick.bind(instance)
},
$forceUpdate: (instance) => {
return () => instance.update()
}
}
API 说明:
this.$nextTick:在下一个 DOM 更新周期后执行回调- 通过
nextTick.bind(instance)保留this指向组件实例
- 通过
this.$forceUpdate:强制组件重新渲染- 返回一个函数,调用时执行
instance.update()
- 返回一个函数,调用时执行
使用示例
<script type="module">
import { h, createApp, ref } from '../dist/vue.esm.js'
const Comp = {
setup(props, { attrs }) {
const count = ref(0)
return {
count,
handleClick() {
this.count++
this.count++
// 使用 this.$nextTick
this.$nextTick(() => {
console.log('DOM 已更新:', this.count)
})
// 使用 this.$forceUpdate
// this.$forceUpdate()
}
}
},
render() {
return h('div', { id: 'container', onClick: this.handleClick }, this.count)
}
}
createApp(Comp, { msg: 'msg', count: 0, a: 1 }).mount('#app')
</script>
总结
至此,我们完成了 Vue 组件异步更新机制的核心实现:
1. 异步更新机制
- 使用
scheduler调度器控制渲染时机 - 响应式属性变化时,不立即执行渲染
- 通过
Promise.resolve().then()实现微任务调度
2. 性能优化
- 同一事件循环中的多次状态变更,只触发一次渲染
- 避免无效的中间状态渲染
- 提升组件更新性能
3. nextTick 实现
- 提供
nextTickAPI,在 DOM 更新后执行回调 - 使用微任务队列确保执行时机
- 保留
this指向,支持在回调中访问组件实例
4. 公共 API 扩展
$nextTick:组件实例的 nextTick 方法$forceUpdate:强制组件重新渲染- 通过
publicPropertiesMap映射表管理公共 API
5. 代码重构
- 将调度相关逻辑抽取到
scheduler.ts queueJob:调度任务到微任务队列nextTick:在DOM更新后执行回调
这套异步更新机制让 Vue 能够高效地处理状态变更,避免不必要的渲染开销,同时通过 nextTick 提供了在 DOM 更新后执行回调的能力,是 Vue 3 性能优化的重要的一部分。