Vue 首次渲染完整流程深度解析
Vue 首次渲染完整流程深度解析
本文基于 Vue 3 源码原理,详细梳理一个组件从实例创建到最终挂载到页面的全过程,帮助读者建立清晰的渲染心智模型。
1. 总体概览
Vue 组件的首次渲染可以概括为以下几个核心阶段:
- 实例创建与初始化
- 响应式数据建立
- 渲染准备(模板编译或获取 render 函数)
- 创建渲染副作用(ReactiveEffect)
- 同步执行渲染函数,生成虚拟 DOM,收集依赖
- 递归 patch,生成真实 DOM 并挂载
- 异步触发 mounted 生命周期钩子
下面将逐步拆解每个阶段,并说明关键代码与设计思想。
2. 实例创建与初始化
2.1 创建组件实例
无论是根组件(createApp().mount())还是子组件,Vue 首先会调用 createComponentInstance 创建一个内部实例对象(instance)。该对象包含了组件的所有重要属性:props、emits、slots、ctx、render、subTree、update 等。
const instance = {
// 组件唯一标识
uid: 0,
// 组件选项
type: Component,
// 父组件实例
parent: null,
// 虚拟节点相关
vnode: null,
subTree: null,
// 渲染更新函数
update: null,
// 响应式数据
props: null,
data: null,
computed: null,
// 生命周期钩子
[LifecycleHooks.BEFORE_CREATE]: null,
[LifecycleHooks.CREATED]: null,
// ... 其他内部属性
}
2.2 调用 beforeCreate 钩子
此时组件实例已创建,但尚未处理任何响应式数据(props、data、computed 等)。用户可以在 beforeCreate 中访问一些工具方法(如 $emit),但无法访问 data 或 props。
3. 响应式数据建立(数据观测)
在 beforeCreate 之后,Vue 会执行初始化流程:
- 处理
props,将其定义为响应式(使用reactive或shallowReactive) - 处理
inject注入 - 处理
data,调用reactive包装成响应式对象 - 处理
computed,创建ComputedRefImpl实例(懒执行,带缓存) - 处理
watch,创建watch副作用(ReactiveEffect) - 处理
methods,绑定this到组件实例
关键点:此时只是为每个属性定义了 getter / setter(基于 Proxy),但尚未发生依赖收集。依赖收集发生在首次渲染函数执行时。
3.1 调用 created 钩子
created 执行时,组件实例已经拥有了完整的响应式数据、计算属性、方法等,用户可以访问和修改它们。但此时尚未生成虚拟 DOM,无法操作真实 DOM(因为还没有挂载点)。
4. 渲染准备:从模板到渲染函数
created 之后到 beforeMount 之间,Vue 的核心任务是 确保组件拥有一个可用的渲染函数。
- 工程化预编译:若使用
vue-loader或@vitejs/plugin-vue,模板在构建时已被编译成render函数,直接挂载到组件选项上。 - 运行时编译:如果使用包含编译器的 Vue 版本(如
vue.global.js),且用户没有提供render但提供了template(或从el的outerHTML获取模板),则调用compile函数将模板字符串编译为render函数。
这一步完成后,instance.render 被赋值为最终的渲染函数。
4.1 调用 beforeMount 钩子
此时组件已拥有渲染函数,即将进入挂载阶段。beforeMount 是挂载前最后一个可同步访问数据但尚未操作 DOM 的钩子。
5. 创建渲染副作用(ReactiveEffect)
Vue 使用 ReactiveEffect 类包装任何需要响应式追踪的函数。对于组件渲染,会创建如下 effect:
// 组件更新函数(首次及后续更新都会执行)
const componentUpdateFn = () => {
// 执行渲染函数,生成新虚拟 DOM
const subTree = instance.render()
// 调用 patch,对比旧树与新树,更新真实 DOM
patch(instance.subTree, subTree, container, anchor, instance)
// 保存新树,供下次更新使用
instance.subTree = subTree
}
// 创建渲染 effect
const effect = new ReactiveEffect(
componentUpdateFn,
// scheduler:当数据变化时,通过 trigger 调用此 scheduler
() => queueJob(instance.update)
)
// instance.update 绑定 effect.run,供外部调用
instance.update = effect.run.bind(effect)
关键设计:
componentUpdateFn是真正执行渲染和更新的函数。- 第二个参数
scheduler用于控制当响应式数据变化时,如何触发更新。对于组件渲染,我们希望通过 异步队列 批量处理,因此scheduler将instance.update推入queue。 - 首次渲染时,Vue 会 直接同步调用
instance.update()(即effect.run()),而不是通过scheduler。
6. 首次渲染:同步执行 effect.run()
6.1 为什么首次渲染不走调度器?
- 首次渲染只有一次,不存在重复更新,无需合并。
- 同步执行可以立即生成 DOM 并完成挂载,避免异步导致的空白闪烁。
- 保证
mounted钩子执行前 DOM 已就绪。
6.2 effect.run() 内部发生了什么?
class ReactiveEffect {
run() {
try {
// 设置全局活跃 effect
activeEffect = this
// 执行原始函数
return this.fn()
} finally {
// 清理全局标记
activeEffect = undefined
}
}
}
当执行 effect.run() 时:
activeEffect被设置为当前effect实例。- 调用
this.fn(),即componentUpdateFn。
7. 执行 componentUpdateFn 生成虚拟 DOM
7.1 调用 instance.render()
render 函数由模板编译而来(或用户直接提供)。执行过程中会访问响应式数据(如 state.count、props.name),触发这些数据的 get 拦截器。
依赖收集:
- 在
get拦截器中,调用track(target, key)。 track函数检查activeEffect是否存在,若存在则将该effect添加到当前属性对应的依赖集合(Set)中。- 这样就建立了 数据 → 组件渲染 effect 的映射关系。
例如:
// 模板中使用了 {{ count }}
// 首次渲染执行 render 函数时,访问 state.count
// track 函数将当前 activeEffect(即组件的渲染 effect)添加到 count 的依赖集合中
// 此后 count 发生变化时,就能找到该 effect 并触发更新
7.2 得到新的虚拟 DOM 树 subTree
render 函数返回一个 VNode 树,描述了组件的整个视图结构。
8. 调用 patch 生成真实 DOM
patch(instance.subTree, subTree, container, anchor, instance)
instance.subTree是上一次渲染的 VNode 树(首次为null)。subTree是刚刚生成的新 VNode 树。container是挂载点 DOM 元素(如div#app)。anchor用于插入定位(首次为null)。
8.1 patch 函数的核心逻辑
- 如果
n1为null,则执行 挂载(mount):根据 VNode 创建真实 DOM 元素,并添加到container。 - 如果
n1不为null,则执行 更新(update):比较新旧 VNode,只更新变化的部分(属性、子节点等)。 - 遇到组件类型 VNode 时,会递归调用子组件的整个初始化流程(从创建实例到挂载)。
8.2 patch 完成后的 DOM 状态
- 所有必要的 DOM 元素已被创建、插入或更新。
- 页面已经呈现出最新的视图。
重要:此时 mounted 钩子尚未执行,因为 Vue 将生命周期钩子推迟到微任务中调用,以确保整个组件树渲染完毕后再执行用户代码。
9. 保存新虚拟 DOM 树
instance.subTree = subTree
将本次渲染生成的新 VNode 树保存到实例的 subTree 属性中,供下次更新时作为“旧树”使用。
10. 异步触发 mounted 钩子
patch 完成后,Vue 会将当前组件的 mounted 钩子函数推入一个微任务队列(通过 nextTick)。
Promise.resolve().then(() => {
invokeArrayFns(instance.mounted)
})
- 父组件的
mounted会在所有子组件mounted执行完毕后调用(因为子组件在父组件的patch过程中已递归挂载,它们的mounted也会被推入队列,且由于深度优先,子组件的mounted会先于父组件被调用)。 - 所有
mounted钩子执行时,DOM 已经完全就绪,可以安全地进行 DOM 操作(如获取元素尺寸、添加第三方库等)。
11. 整体流程图

12. 关键设计思想总结
| 设计点 | 说明 |
|---|---|
| 响应式 + 虚拟 DOM | 组件级 Watcher/Effect 控制粒度,通过虚拟 DOM diff 实现精准更新 |
| 依赖收集时机 | 仅在渲染函数执行时收集,利用 activeEffect 全局变量 |
| 首次渲染同步 | 避免异步导致的闪烁,直接生成 DOM |
| 更新渲染异步批量 | 通过 queueJob + nextTick 合并多次数据变更,减少重复渲染 |
| 生命周期异步调用 | mounted / updated 在微任务中触发,保证组件树一致性 |
| 递归挂载子组件 | patch 中遇到组件 VNode 时,会递归执行子组件的完整初始化流程 |
13. 常见面试题速答
Q1:created 和 beforeMount 之间发生了什么?
A:Vue 确保组件拥有可用的渲染函数(运行时编译或取用预编译的 render 函数),并准备挂载环境。
Q2:首次渲染为什么不走 scheduler?
A:首次渲染只有一次,直接同步执行可以立即完成 DOM 挂载,避免异步造成的空白或延迟。
Q3:依赖收集具体发生在哪个步骤?
A:发生在首次执行 componentUpdateFn 中的 instance.render() 时,响应式数据的 get 拦截器调用 track,将当前 activeEffect(即渲染 effect)加入依赖集合。
Q4:patch 执行完后,DOM 是否已更新?mounted 是否已执行?
A:DOM 已同步更新,但 mounted 钩子被推迟到微任务中执行,尚未调用。
Q5:子组件的 mounted 和父组件的 mounted 谁先执行?
A:子组件先执行。因为父组件的 patch 过程中递归创建子组件,子组件完成渲染后将自身的 mounted 推入队列,深度优先,所以子组件的 mounted 会先于父组件被调用。
14. 结语
理解 Vue 首次渲染的完整流程,不仅能帮助开发者更好地编写生命周期代码,还能在遇到性能问题或异常时快速定位根因。希望本文能成为你日常开发与面试复习的可靠参考资料。如果你对文中任何环节有疑问,欢迎继续探讨!