Appearance
Vue 3 渲染器驱动核心:从数据变化到 DOM 更新的完整链路
本章深入剖析 Vue 3 组件实例、渲染副作用与异步调度的完整机制,揭示"自动更新"背后的底层原理。
前言
在前面的章节中,我们已经学习了 VNode 结构、Patch 路由和 Diff 算法。但这些机制都是"被动执行"的——当你修改一个响应式数据时,究竟是谁在驱动这些机制运转?更新是如何触发的?为什么多次修改数据只会触发一次重渲染?
本章将打通组件实例、渲染副作用与异步调度的完整闭环。
组件挂载流程:从 VNode 到实例
processComponent:路由分发的入口
当 patch 函数遇到组件类型的 VNode 时,会调用 processComponent:
ts
// 伪代码
function processComponent(n1, n2, container) {
if (n1 == null) {
// 首次挂载
if (n2.shapeFlag & COMPONENT_KEPT_ALIVE) {
// KeepAlive 组件:激活缓存的实例
parentComponent.ctx.activate(n2, container, ...)
} else {
// 普通组件:创建新实例
mountComponent(n2, container, ...)
}
} else {
// 更新现有组件
updateComponent(n1, n2, ...)
}
}设计思路:KeepAlive 不需要创建新实例,而是复用已缓存的实例,这是性能优化的典型做法。
mountComponent:三步走的挂载策略
``ts // 伪代码 function mountComponent(vnode, container, parentComponent) { // Step 1:创建组件实例对象 const instance = createComponentInstance(vnode, parentComponent)
// Step 2:初始化 props、slots、setup() setupComponent(instance)
// Step 3:建立渲染副作用(连接响应式与渲染) setupRenderEffect(instance, vnode, container) }
三个清晰的步骤,体现了职责分离的设计哲学。
### createComponentInstance:实例的数据结构
一个组件实例本质上是一个包含所有信息的对象:组件实例的六大类别:
【核心身份】 uid: 唯一标识符 vnode: 组件的 VNode type: 组件定义 parent: 父组件实例
【渲染相关】 render: 渲染函数 subTree: render 返回的 VNode(真实渲染内容) effect: 响应式副作用 update: 触发重渲染的函数
【代理与访问】 proxy: 公开代理(模板中的 this) ctx: 渲染上下文对象 accessCache: 属性访问缓存
【状态管理】 props: 父传递的属性 data: data() 返回的状态 setupState: setup() 返回的状态 slots: 插槽对象 attrs: 未声明的属性
【生命周期】 isMounted: 是否已挂载 isUnmounted: 是否已卸载 bm/m/bu/u/bum/um: 生命周期钩子
**关键区分**:vnode vs subTree: vnode = ← 父组件中的表示 subTree =
← 组件 render 返回的内容
### setupComponent:状态初始化
``ts
// 伪代码
function setupComponent(instance) {
// 1. 解析 props 和 slots
initProps(instance, instance.vnode.props)
initSlots(instance, instance.vnode.children)
// 2. 创建属性访问缓存
instance.accessCache = Object.create(null)
// 3. 创建公开代理(this)
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// 4. 执行 setup() 函数
if (instance.type.setup) {
const setupResult = instance.type.setup(
instance.props, // 第一个参数:响应式 props
setupContext // 第二个参数:{ emit, slots, expose }
)
handleSetupResult(instance, setupResult)
}
}关键优化:accessCache 是一个性能优化。在模板中频繁访问 this.xxx 时,每次都需要判断属性来自 setupState、data、props 还是 ctx。accessCache 记录了每个属性的来源,避免重复的 hasOwn() 调用。
实例代理:this 的本质
属性访问的五层优先级
当你在模板中访问 this.msg 时,Vue 需要从多个位置查找这个属性。为了兼容 Composition API 和 Options API,Vue 设计了一个优先级链:
第一层:setupState(setup() 返回的状态)
↓ 如果找到,缓存并返回
第二层:data(data() 返回的状态)
↓ 如果找到,缓存并返回
第三层:props(父传递的属性)
↓ 如果找到,缓存并返回
第四层:ctx($attrs、$slots 等)
↓ 如果找到,缓存并返回
第五层:globalProperties(全局属性)为什么 setupState 优先级最高?
这是 Vue 3 推荐 Composition API 的设计体现。如果 Options API 的 data 优先级更高,同名属性会导致 setup 返回的状态被覆盖,造成困惑。
代理的 set 拦截:保护机制
``ts // 伪代码 proxy.set = function(key, value) { // 规则 1:setupState 可修改(会触发响应式更新) if (hasOwn(setupState, key)) { setupState[key] = value return true }
// 规则 2:data 可修改(会触发响应式更新) if (hasOwn(data, key)) { data[key] = value return true }
// 规则 3:props 只读,禁止修改 if (hasOwn(props, key)) { warn(不能修改 props "${key}",props 是只读的) return false }
// 规则 4:$ 开头的内置属性只读 if (key[0] === '$') { warn(不能修改 "${key}",这是 Vue 的内置属性) return false }
// 规则 5:其他属性允许动态添加 ctx[key] = value return true }
**核心约束**:
- Props 是单向数据流,禁止直接修改
- setupState 和 data 修改会触发响应式系统
- $ 开头的内置属性受保护
---
## 渲染副作用:自动更新的引擎
### setupRenderEffect:连接响应式与渲染
这是 Vue "自动更新"的核心。渲染函数被包装成一个响应式副作用,当依赖的数据变化时,副作用会自动运行。
``ts
// 伪代码
function setupRenderEffect(instance, vnode, container) {
// 第一步:定义渲染更新函数
const componentUpdateFn = () => {
if (!instance.isMounted) {
// ======= 首次挂载 =======
// 执行 beforeMount 钩子
invokeHooks(instance.bm)
// 执行渲染函数,生成 VNode
const subTree = renderComponentRoot(instance)
instance.subTree = subTree
// 递归 patch,转换为真实 DOM
patch(null, subTree, container, ...)
// 保存 DOM 引用
vnode.el = subTree.el
// 异步执行 mounted 钩子
queuePostRenderEffect(() => invokeHooks(instance.m))
instance.isMounted = true
} else {
// ======= 更新阶段 =======
// 如果父组件传入了新 VNode,需要更新 props
if (instance.next) {
updateComponentPreRender(instance, instance.next)
}
// 执行 beforeUpdate 钩子
invokeHooks(instance.bu)
// 重新执行渲染函数,生成新 VNode
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
// Diff 并更新 DOM
patch(prevTree, nextTree, container, ...)
// 异步执行 updated 钩子
queuePostRenderEffect(() => invokeHooks(instance.u))
}
}
// 第二步:将渲染函数包装成响应式副作用
const effect = new ReactiveEffect(componentUpdateFn)
// 第三步:设置调度器
// 关键:当依赖变化时,不直接运行 componentUpdateFn
// 而是将 job 推入调度器队列,实现批量更新
effect.scheduler = () => queueJob(instance.job)
// 第四步:保存引用供后续调用
instance.effect = effect
instance.update = effect.run.bind(effect)
instance.job = effect.runIfDirty.bind(effect)
// 第五步:首次执行,完成初始挂载
instance.update()
}时序总结:
mount 路径:
beforeMount → render → patch → DOM 插入 → queuePostRenderEffect(mounted)
update 路径:
beforeUpdate → render → patch → DOM 更新 → queuePostRenderEffect(updated)生命周期钩子的执行时机
钩子 执行时机 同/异步 说明
──────────────────────────────────────────────────
beforeMount render 前,首次 同步 在首次渲染前
mounted patch 后,首次 异步 DOM 已插入,可访问 DOM
beforeUpdate render 前,更新 同步 在重新渲染前
updated patch 后,更新 异步 DOM 已更新,可访问新 DOM为什么 mounted 和 updated 异步执行?
- 性能:同步执行会阻塞渲染流程
- DOM 保证:异步执行时,DOM 肯定已更新
- 批量处理:多个组件的钩子在同一 tick 统一执行
异步调度:批量更新的核心
问题:为什么多次修改数据只更新一次?
ts
// 用户代码
this.count++
this.count++
this.count++
// 如果没有调度器,会触发三次 render
// 但实际上只会 render 一次,原因就在调度器queueJob:去重与排序
当响应式数据变化时,会触发 effect.scheduler,它将 job 加入队列:
ts
// 伪代码
function queueJob(job) {
// 去重:同一个 job 不重复加入
if (job 已在队列中) {
return
}
// 排序:按 job.id(组件创建顺序)递增排列
queue.push(job)
queue.sort((a, b) => a.id - b.id)
// 触发 flush
queueFlush()
}两个关键机制:
- 去重:同一 tick 内,同一 job 只加入一次,多次状态变化合并为一次更新
- 排序:按 id 递增排列,保证父组件先于子组件更新
flushJobs:执行队列
微任务执行时,调用 flushJobs 来执行队列中的所有 job:
ts
// 伪代码
function flushJobs() {
try {
// 遍历并执行队列中的每个 job
for (let i = 0; i < queue.length; i++) {
const job = queue[i]
callJob(job) // 执行组件更新函数
}
} finally {
// 清理队列,防止内存泄漏
queue.length = 0
// 执行 post 队列(mounted、updated 钩子)
flushPostFlushCbs()
// 递归处理新入队的任务
if (queue.length > 0) {
flushJobs()
}
}
}三个队列的执行顺序
Vue 的调度器维护三个队列,执行顺序严格:
【Pre 队列】 watch 的 flush: 'pre'
↓
【Main 队列】组件更新(patch)
↓
【Post 队列】mounted、updated 钩子使用场景:
ts
// pre 队列:在组件更新前执行
watch(() => this.count, () => { ... }, { flush: 'pre' })
// main 队列:组件更新本身
// effect.scheduler → queueJob(instance.job)
// post 队列:在 DOM 更新后执行
this.$nextTick(() => {
// 访问最新的 DOM
})nextTick:微任务的应用
``ts // 伪代码 const resolvedPromise = Promise.resolve() let currentFlushPromise = null
function nextTick(fn) { // 如果 fn 不提供,返回当前的 flush Promise // 用户可以通过 await nextTick() 等待 DOM 更新 const p = currentFlushPromise || resolvedPromise return fn ? p.then(fn) : p }
function queueFlush() { if (!currentFlushPromise) { // 将 flushJobs 推入微任务队列 currentFlushPromise = resolvedPromise.then(flushJobs) } }
**关键理解**:用户代码执行: this.count++ this.$nextTick(() => console.log('DOM updated'))
JavaScript 事件循环: ┌─ Call Stack 执行(用户代码) │ ├─ this.count++ │ │ → trigger() → scheduler → queueJob() │ │ → queueFlush() → resolvedPromise.then(flushJobs) │ └─ nextTick 收集回调 │ └─ Microtask Queue 执行 ├─ flushJobs() 执行所有 job(patch、render) └─ nextTick 回调执行(此时 DOM 已更新)
**为什么用 Promise?**
Promise 的 `.then()` 回调进入**微任务队列**,执行时机是:
- 当前宏任务结束后
- 浏览器开始重绘前
这确保了 nextTick 的回调能在 DOM 更新后、浏览器重绘前执行。
---
## 完整流程演示
从数据变化到 DOM 更新的完整链路:【步骤 1】用户代码修改响应式数据 this.count++
【步骤 2】响应式系统 trigger() 收集依赖的 effects 调用 effect.scheduler() → queueJob(instance.job)
【步骤 3】调度器去重并排序 if (job 不在队列中) queue.push(job) queue.sort() // 按 id 递增排列
【步骤 4】触发 flush 机制 queueFlush() → Promise.resolve().then(flushJobs) 此时返回控制权给事件循环
【步骤 5】事件循环进入微任务队列 flushJobs() 执行所有排队的 job
【步骤 6】执行 job = instance.job = effect.runIfDirty ├─ 调用 componentUpdateFn() │ ├─ 执行 beforeUpdate 钩子 │ ├─ 调用 renderComponentRoot(instance) │ ├─ 执行 patch(prevTree, nextTree) │ └─ 执行 updated 钩子(异步) └─ 返回
【步骤 7】执行 post 队列 flushPostFlushCbs() 执行所有 post 钩子
【步骤 8】用户 nextTick 回调执行 此时 DOM 已完全更新
---
## shouldUpdateComponent:更新守门员
并非每次父组件更新都需要触发子组件重渲染。`shouldUpdateComponent` 判断是否真的需要更新:
``ts
// 伪代码
function shouldUpdateComponent(oldVNode, newVNode) {
// 规则 1:有指令或过渡效果,必须更新
if (newVNode.dirs || newVNode.transition) {
return true
}
// 规则 2:动态插槽必须更新
if (newVNode.patchFlag & DYNAMIC_SLOTS) {
return true
}
// 规则 3:只检查动态 props
if (newVNode.patchFlag & PROPS) {
for (const key of newVNode.dynamicProps) {
if (newVNode.props[key] !== oldVNode.props[key]) {
return true // props 有变化,需要更新
}
}
return false // props 没变,跳过更新
}
// 规则 4:完全没有优化标记,全量比较
return hasPropsChanged(oldVNode.props, newVNode.props)
}优化策略:
编译优化路径:只检查编译器标记为动态的 props,跳过静态 props 运行时路径:全量比较所有 props
总结:完整的驱动链路
graph TD
A["响应式数据变化"] -->|trigger| B["ReactiveEffect.scheduler()"]
B --> C["queueJob(instance.job)"]
C --> D["queueFlush() → Promise.then()"]
D --> E["微任务执行"]
E --> F["flushJobs()"]
F --> G["按 id 排序执行"]
G --> H["componentUpdateFn()"]
H --> I["beforeUpdate → render → patch → DOM"]
I --> J["flushPostFlushCbs()"]
J --> K["mounted/updated 钩子"]
K --> L["nextTick 回调"]核心机制:
- 响应式系统:自动追踪依赖,数据变化时 trigger effect
- 调度器:去重、排序、批量执行,避免重复更新
- 副作用:包装 render 函数,自动响应数据变化
- 异步处理:用微任务实现 nextTick,保证 DOM 更新后执行
下一章预告:本章我们完成了渲染系统的完整解析。从编译、VNode、Patch、Diff 到响应式驱动,Vue 3 的核心链路已全面展开。下一章,我们将总结整个系统的架构设计和优化思想。
