Vue 首次渲染完整流程深度解析

Vue 首次渲染完整流程深度解析

本文基于 Vue 3 源码原理,详细梳理一个组件从实例创建到最终挂载到页面的全过程,帮助读者建立清晰的渲染心智模型。


1. 总体概览

Vue 组件的首次渲染可以概括为以下几个核心阶段:

  1. 实例创建与初始化
  2. 响应式数据建立
  3. 渲染准备(模板编译或获取 render 函数)
  4. 创建渲染副作用(ReactiveEffect)
  5. 同步执行渲染函数,生成虚拟 DOM,收集依赖
  6. 递归 patch,生成真实 DOM 并挂载
  7. 异步触发 mounted 生命周期钩子

下面将逐步拆解每个阶段,并说明关键代码与设计思想。


2. 实例创建与初始化

2.1 创建组件实例

无论是根组件(createApp().mount())还是子组件,Vue 首先会调用 createComponentInstance 创建一个内部实例对象(instance)。该对象包含了组件的所有重要属性:propsemitsslotsctxrendersubTreeupdate 等。

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 钩子

此时组件实例已创建,但尚未处理任何响应式数据(propsdatacomputed 等)。用户可以在 beforeCreate 中访问一些工具方法(如 $emit),但无法访问 dataprops


3. 响应式数据建立(数据观测)

beforeCreate 之后,Vue 会执行初始化流程:

  • 处理 props,将其定义为响应式(使用 reactiveshallowReactive
  • 处理 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(或从 elouterHTML 获取模板),则调用 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 用于控制当响应式数据变化时,如何触发更新。对于组件渲染,我们希望通过 异步队列 批量处理,因此 schedulerinstance.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() 时:

  1. activeEffect 被设置为当前 effect 实例。
  2. 调用 this.fn(),即 componentUpdateFn

7. 执行 componentUpdateFn 生成虚拟 DOM

7.1 调用 instance.render()

render 函数由模板编译而来(或用户直接提供)。执行过程中会访问响应式数据(如 state.countprops.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 函数的核心逻辑

  • 如果 n1null,则执行 挂载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. 整体流程图

vue初次渲染流程.png

12. 关键设计思想总结

设计点说明
响应式 + 虚拟 DOM组件级 Watcher/Effect 控制粒度,通过虚拟 DOM diff 实现精准更新
依赖收集时机仅在渲染函数执行时收集,利用 activeEffect 全局变量
首次渲染同步避免异步导致的闪烁,直接生成 DOM
更新渲染异步批量通过 queueJob + nextTick 合并多次数据变更,减少重复渲染
生命周期异步调用mounted / updated 在微任务中触发,保证组件树一致性
递归挂载子组件patch 中遇到组件 VNode 时,会递归执行子组件的完整初始化流程

13. 常见面试题速答

Q1:createdbeforeMount 之间发生了什么?
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 首次渲染的完整流程,不仅能帮助开发者更好地编写生命周期代码,还能在遇到性能问题或异常时快速定位根因。希望本文能成为你日常开发与面试复习的可靠参考资料。如果你对文中任何环节有疑问,欢迎继续探讨!


Vue 首次渲染完整流程深度解析
http://localhost:8090/archives/vue-shou-ci-xuan-ran-wan-zheng-liu-cheng-shen-du-jie-xi
作者
Administrator
发布于
2026年04月09日
许可协议