人人都会AI编程

6.3 组件首次渲染全流程:初始化 → 模板编译 → VNode 生成 → 真实 DOM 挂载

更新时间:2026-07-11

当我们把一个 Vue 组件挂载到页面上时,框架内部会经历一条清晰的流水线。理解这个过程有助于排查首次渲染问题、优化加载性能,以及更准确地把握生命周期钩子的执行时机。

整个首次渲染可以分为四个阶段:初始化组件实例模板编译为渲染函数生成虚拟 DOM 树挂载为真实 DOM


1. 初始化组件实例

当调用 createApp(App).mount('#app') 时,Vue 首先创建根组件实例。对于每个组件(包括嵌套的子组件),初始化步骤大致如下:

  • 解析组件选项(propsdatasetup 等),处理默认值和类型校验。
  • 建立响应式系统:为 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章):

  1. 解析(parse):将模板字符串解析为 AST(抽象语法树),识别出元素、属性、指令、插值表达式等。
  2. 优化(optimize):标记静态节点(不会变的 DOM 部分),并为动态节点打上 PatchFlags(如文本、类、属性),为后续的更新跳过静态比对做准备。
  3. 生成(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 元素并插入到容器中

主要步骤:

  1. 创建元素:根据 VNode 的类型(标签名)创建对应的真实 DOM 节点。
  2. 处理属性:将 VNode 上的 propsclassstyle、事件、普通属性)设置到 DOM 元素上。
  3. 递归处理子节点:对 children 列表执行相同的挂载流程,将子 DOM 插入到父元素中。
  4. 处理组件 VNode:如果遇到组件类型的 VNode,则创建该子组件实例,并重复上述四个阶段,将子组件挂载到对应的 DOM 位置。
  5. 插入页面:当整棵 DOM 树构建完成后,最后将其一次性(或分批)插入到根容器中,触发浏览器的渲染。

挂载完毕后,Vue 还会执行组件的 mounted 生命周期钩子,表示组件已经出现在屏幕上,可以安全地进行 DOM 操作(如获取元素尺寸、初始化第三方插件)。

首次渲染流程示意图

createApp
   │
   ▼
创建根组件实例
   │
   ▼
编译模板 → 生成 render 函数       ←─── 若为构建时编译则跳过运行时编译
   │
   ▼
执行 render() → 返回 VNode 树
   │
   ▼
patch(container, VNode)
   │
   ├→ 创建真实 DOM 节点
   ├→ 递归挂载子组件
   └→ 插入到容器中
   │
   ▼
触发 mounted 生命周期

对开发者的实际影响

  • 模板越简单,编译开销越小:虽然构建时编译已经消除了运行时负担,但过于复杂的模板嵌套会增加 VNode 创建和挂载的递归深度。合理拆分组件有助于提升首次渲染速度。
  • 白屏时间:首次渲染的时间包括 JavaScript 的下载、执行、模板编译(如果有)、VNode 生成和 DOM 挂载。优化方向通常放在代码分割、资源预加载,以及避免在 mounted 中执行耗时同步操作。
  • 调试技巧:使用 Vue DevTools 的组件树可以直观看到组件实例化的嵌套层级和时间。如果想度量首次渲染性能,可以在 created 打点开始,mounted 结束,但要注意异步子组件可能拉长这个区间。

组件首次渲染是一条“数据到视图”的单向流水线,Vue 做了大量优化让这个过程足够轻量,让开发者不必担心框架级性能开销。