当我们把一个 Vue 组件挂载到页面上时,框架内部会经历一条清晰的流水线。理解这个过程有助于排查首次渲染问题、优化加载性能,以及更准确地把握生命周期钩子的执行时机。
整个首次渲染可以分为四个阶段:初始化组件实例 → 模板编译为渲染函数 → 生成虚拟 DOM 树 → 挂载为真实 DOM。
1. 初始化组件实例
当调用 createApp(App).mount('#app') 时,Vue 首先创建根组件实例。对于每个组件(包括嵌套的子组件),初始化步骤大致如下:
- 解析组件选项(
props、data、setup等),处理默认值和类型校验。 - 建立响应式系统:为
data返回的对象(或reactive/ref定义的数据)创建响应式代理,并初始化依赖收集容器。 - 调用
setup函数(如果存在),并将返回值整合到组件上下文中;对于选项式 API,则将data函数执行结果挂载到组件实例。 - 注册生命周期钩子、事件处理等。
此时,组件实例已经拥有了响应式数据、计算属性、方法等,但还没有开始渲染 DOM,因为还没有调用渲染函数。
2. 模板编译为渲染函数
Vue 支持多种定义模板的方式:单文件组件的 <template>、内联字符串 template、甚至手写 render 函数。对于最常见的字符串模板(.vue 文件中的 <template> 区域),Vue 需要在运行时或构建时将其编译成 JavaScript 渲染函数。
- 构建时编译(推荐)
使用 Vite/Webpack 时,vue-loader 或 @vitejs/plugin-vue 会在打包阶段就把模板编译好,最终生成的代码中只保留渲染函数,省去了运行时的编译器体积。这是生产环境的默认行为。
- 运行时编译
如果通过 CDN 引入 Vue 完整版(包含编译器),模板会在浏览器中被实时编译。这种方式适合轻量级试水,但不建议用于正式项目。
编译过程主要完成三件事(详见第6章):
- 解析(parse):将模板字符串解析为 AST(抽象语法树),识别出元素、属性、指令、插值表达式等。
- 优化(optimize):标记静态节点(不会变的 DOM 部分),并为动态节点打上 PatchFlags(如文本、类、属性),为后续的更新跳过静态比对做准备。
- 生成(generate):将优化后的 AST 转换成渲染函数的代码字符串,最终通过
new Function生成真正的渲染函数render。
最终,每个组件都获得一个 render 函数,它返回一个 VNode(虚拟节点),描述组件当前应该渲染成什么样的虚拟 DOM 树。
3. 生成虚拟 DOM 树(VNode)
当组件需要渲染时(首次渲染必然触发),Vue 调用组件的 render 函数,传入上下文,得到一棵 VNode 树。
- VNode 本质是普通的 JavaScript 对象,用它来描述真实的 DOM 节点:
// 形如这样的结构
{
type: 'div',
props: { id: 'box' },
children: [
{ type: Text, children: 'hello' }
]
}
- 对于组件标签(如
<MyComponent>),VNode 的type是组件定义对象本身,children 会通过插槽等方式传入。 render函数执行时,会访问响应式数据,从而触发依赖收集,建立起数据与视图的关联。
这个过程完全是 JavaScript 层面的对象构建,不涉及任何 DOM 操作,所以速度极快。
4. 挂载为真实 DOM(patch)
得到 VNode 树后,Vue 进入挂载阶段(patch 过程)。由于是首次渲染,目标容器通常是一个空的 DOM 节点(如 <div id="app"></div>),所以 patch 过程本质是把 VNode 递归地转换成 DOM 元素并插入到容器中。
主要步骤:
- 创建元素:根据 VNode 的类型(标签名)创建对应的真实 DOM 节点。
- 处理属性:将 VNode 上的
props(class、style、事件、普通属性)设置到 DOM 元素上。 - 递归处理子节点:对
children列表执行相同的挂载流程,将子 DOM 插入到父元素中。 - 处理组件 VNode:如果遇到组件类型的 VNode,则创建该子组件实例,并重复上述四个阶段,将子组件挂载到对应的 DOM 位置。
- 插入页面:当整棵 DOM 树构建完成后,最后将其一次性(或分批)插入到根容器中,触发浏览器的渲染。
挂载完毕后,Vue 还会执行组件的 mounted 生命周期钩子,表示组件已经出现在屏幕上,可以安全地进行 DOM 操作(如获取元素尺寸、初始化第三方插件)。
首次渲染流程示意图
createApp
│
▼
创建根组件实例
│
▼
编译模板 → 生成 render 函数 ←─── 若为构建时编译则跳过运行时编译
│
▼
执行 render() → 返回 VNode 树
│
▼
patch(container, VNode)
│
├→ 创建真实 DOM 节点
├→ 递归挂载子组件
└→ 插入到容器中
│
▼
触发 mounted 生命周期
对开发者的实际影响
- 模板越简单,编译开销越小:虽然构建时编译已经消除了运行时负担,但过于复杂的模板嵌套会增加 VNode 创建和挂载的递归深度。合理拆分组件有助于提升首次渲染速度。
- 白屏时间:首次渲染的时间包括 JavaScript 的下载、执行、模板编译(如果有)、VNode 生成和 DOM 挂载。优化方向通常放在代码分割、资源预加载,以及避免在
mounted中执行耗时同步操作。 - 调试技巧:使用 Vue DevTools 的组件树可以直观看到组件实例化的嵌套层级和时间。如果想度量首次渲染性能,可以在
created打点开始,mounted结束,但要注意异步子组件可能拉长这个区间。
组件首次渲染是一条“数据到视图”的单向流水线,Vue 做了大量优化让这个过程足够轻量,让开发者不必担心框架级性能开销。