Vue 组件渲染流程

Vue 组件从创建到更新,经历了哪些阶段?

Vue 组件的一次更新可以简化为:

创建组件实例
→ setup / 选项初始化
→ 创建 render effect
→ 执行 render
→ 生成 VNode
→ mount / patch
→ 执行生命周期和副作用

1. 创建组件实例

当 renderer 遇到组件 VNode 时,会创建组件实例。实例会保存:

  • props;
  • attrs;
  • slots;
  • emits;
  • setup 状态;
  • 生命周期和渲染相关信息。

Vue 3 中,组件的逻辑主要由 setup() 和 render function 组成;使用 <script setup> 时,编译器会把代码转换为 setup 相关实现。

2. setup 与 render

<script setup>
import { ref } from 'vue';

const count = ref(0);
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

模板会被编译为 render function。首次渲染时,render effect 执行,读取 count,响应式系统因此建立依赖关系。

setup() 本身通常只在组件创建时执行一次;render 则可能执行多次。不要把每次渲染都要做的逻辑误放进 setup,也不要在 render 中执行副作用。

3. Mount:首次挂载

首次渲染时,Vue 会:

  1. 执行组件的 render function;
  2. 生成组件子树的 VNode;
  3. 递归创建元素、文本、子组件;
  4. 将真实 DOM 插入宿主容器;
  5. 执行 mounted 等生命周期。

简化流程:

组件 render

组件子树 VNode

patch(null, vnode)

创建 DOM 并插入

mounted

多层嵌套组件的顺序

假设组件树是:

App
├── Header
├── Main
│   └── UserCard
│       └── Avatar
└── Footer

父组件必须先执行 setup 和 render,才能得到包含哪些子组件的 VNode。Renderer 随后按模板顺序深度优先递归 mount:

App setup / beforeMount
→ Header setup / beforeMount
→ Header mounted
→ Main setup / beforeMount
→ UserCard setup / beforeMount
→ Avatar setup / beforeMount
→ Avatar mounted
→ UserCard mounted
→ Main mounted
→ Footer setup / beforeMount
→ Footer mounted
→ App mounted

核心规律:

  • setupbeforeMount:父到子,先向下递归;
  • 同级兄弟组件:通常按模板出现顺序处理;
  • mounted:子到父,子树完成后再通知父组件;
  • 开始初始化是从外向内,完成回调是从内向外。

不要让业务正确性依赖兄弟组件的 mounted 先后,异步组件、SuspenseTeleport 和后续重构都可能改变完成时机。兄弟协作应由父组件、共享状态或明确的 Promise/事件调度。

DOM 挂载不等于浏览器已经绘制

Vue mount 完成表示真实 DOM 已经提交,但浏览器通常要等当前 JavaScript 任务和微任务结束后,才有机会执行样式计算、布局、绘制和合成:

Vue setup / render / patch DOM
→ mounted 回调
→ JavaScript 任务结束
→ Style
→ Layout
→ Paint
→ Composite

因此 mountednextTick 只能说明 DOM 已经就绪,不能证明用户已经在屏幕上看到这一帧。

条件与异步组件

  • v-if="false":不创建组件实例,不执行 setupmounted
  • v-show="false":组件仍会完整挂载,只通过 display: none 隐藏;
  • 异步组件:渲染到该分支时才执行 loader,先显示占位或 loading,Chunk 完成后再创建真实组件实例;
  • Vue 不会在 app.mount() 时挂载项目中的所有页面,只会递归处理当前渲染分支产生的组件树。

4. Update:响应式更新

当 render 中读取的响应式数据发生变化时,响应式系统会通知组件的 render effect。组件更新任务进入 scheduler 队列,刷新时重新执行 render,得到新的 VNode。

state 变化

触发组件 render effect

newVNode = render()

patch(oldVNode, newVNode)

只更新必要 DOM

组件重新执行 render 不代表所有 DOM 都重新创建。renderer 会比较新旧 VNode 的类型、props、子节点和 key,决定复用、更新、移动或删除。

5. 父子组件更新

父组件 render 产生新的子组件 VNode 后,Vue 会通过 props、slots 和 VNode 身份协调子组件:

  • props 变化时,子组件会收到新的 props 并更新;
  • props 没有变化时,子组件不一定需要重新渲染;
  • 子组件自己的响应式状态变化,可以独立触发子组件更新;
  • slot 内容由父组件生成,父组件更新可能导致 slot 内容重新计算。

组件更新和 DOM 更新不是同一个概念。组件 render 可能重新执行,但 patch 发现没有变化后,可以不修改 DOM。

6. 生命周期与渲染阶段

常见顺序可以简化为:

beforeCreate / setup
→ beforeMount
→ mounted
→ beforeUpdate
→ updated
→ beforeUnmount
→ unmounted

Composition API 中对应的生命周期注册方式包括 onMountedonUpdatedonBeforeUnmount 等。需要读取挂载后的 DOM 时使用 onMounted;需要读取更新后的 DOM 时,通常使用 onUpdated 或等待 nextTick

生命周期回调也应避免制造无限更新:在 updated 中再次无条件修改响应式状态,可能导致循环更新。

7. VNode、组件实例和 DOM 的关系

这三者不是同一个东西:

组件实例:保存 props、setup 状态和生命周期
VNode:描述某次渲染结果
DOM:浏览器中的真实宿主节点

一个组件实例可以在多次更新中生成多个 VNode;一个组件也可能渲染多个 DOM 节点。VNode 是协调过程的中间描述,不是 DOM 本身。

8. nextTick 在流程中的位置

count.value++;
await nextTick();
// 此时通常可以读取本轮更新后的 DOM

nextTick 等待 Vue 刷新当前更新队列,适合在状态修改后读取 DOM。它不表示网络请求完成,也不保证等待所有未来任务。

watch 默认是 flush: 'pre',回调可能早于当前组件 DOM 更新。需要在 DOM 提交后初始化依赖容器的第三方库时,可以等待 nextTick,或更明确地使用 flush: 'post'

watch(ready, initLibrary, { flush: 'post' })

如果不仅要等 DOM 提交,还希望先让 Loading 有机会绘制一帧,可以嵌套两次 requestAnimationFrame

await nextTick()

await new Promise((resolve) => {
  requestAnimationFrame(() => {
    requestAnimationFrame(resolve)
  })
})

startHeavyWork()

requestAnimationFrame 回调发生在当帧绘制之前。单次 rAF 里直接执行重任务,仍可能阻塞这一帧;嵌套第二次 rAF 会把重任务推迟到后续帧,让两次回调之间尽量留出一次绘制机会。这是工程调度技巧,不是严格的“屏幕绘制完成”通知。

9. 常见误区

  • setup() 不是每次组件更新都会执行,render 才会反复执行。
  • 组件重新渲染不等于 DOM 全量重建。
  • 响应式数据变化后通常是异步批量更新,不是立即修改 DOM。
  • mounted 只表示首次挂载完成,不会在每次更新后执行。
  • render 阶段应保持纯粹,网络请求和订阅应放在合适的 effect / 生命周期中。

面试总结

Vue 遇到组件 VNode 时会创建组件实例并执行 setup,随后建立 render effect。首次执行 render 生成 VNode 并完成 mount;响应式依赖变化后,更新任务进入 scheduler,组件重新执行 render,生成新 VNode,再通过 patch 对比新旧 VNode,只更新必要的 DOM。组件实例、VNode 和真实 DOM 是三个不同层次。