Skip to content

L36 · 组件实例与渲染流程 ​

🎯 本节目标:理解 Vue 3 组件从创建到挂载到更新的完整流程
📦 本节产出:完整的组件生命周期心智模型 + 理解 setup 的执行时机
🔗 前置钩子:L31-L35 的响应式 + VDOM + 调度器
🔗 后续钩子:L37 将讲 Composition API 的设计决策

本节继续以 Vue 3.5.43 的 VDOM 组件运行时为参照。普通应用通过公开 API 使用这些能力,内部字段不是稳定的业务接口。下面的流程图省略异步组件、Suspense 与 SSR 的额外分支。

1. 组件渲染全流程 ​


2. 组件实例内部结构 ​

每个组件实例都有自己的状态、渲染关系与副作用作用域。下面是选取的字段,不是供应用复制实现的完整接口:

字段作用
uid / type / parent实例编号、组件定义与父实例
vnode代表该组件的 VNode
subTree该组件 render 返回的 VNode 子树
props / attrs / slots / emit组件输入、未声明属性、插槽与事件出口
setupState / datasetup 返回对象的处理结果、Options API data;setup 也可以返回 render 函数
render / effect渲染函数与驱动组件更新的 ReactiveEffect
update / job同步运行 effect 的入口,与用于排队的更新任务;两者不应混为一谈
scope当前组件的 EffectScope
bm / m / bu / u / bum / um对应生命周期回调列表,可由多次注册或 mixin 等产生
provides / refs注入上下文与模板 ref

组件 job 的 id 通常取 uid,父组件通常先创建,所以更新队列可先处理父任务,再处理子任务。3.5.43 使用 queueJob 插入与 PRE 标记等规则,不只是刷新前做一次全队列 sort。component.ts、renderer.ts


3. 挂载流程详解 ​

关键细节 ​

  1. setup 对每个组件实例在创建时执行;该实例普通响应式更新只重跑 render,不重新运行 setup。销毁后重建或挂载另一实例会再次执行 setup。
  2. render 在渲染 effect 中读取响应式依赖;setup 中创建状态本身,不等于已经收集了 render 的所有依赖。
  3. 同步子组件的 mounted 先于父组件;异步组件和 Suspense 子树不包含在这一保证里。mounted 表示挂载流程完成,不表示页面所有资源已加载或已绘制。
普通同步父子组件:
Parent setup → Parent beforeMount → Parent render
  → Child setup → Child beforeMount → Child render + patch
  → Child mounted
Parent mounted

如果 mount 的容器本身不在 document 中,也不能仅凭 mounted 认定节点已显示在屏幕上。onMounted


4. 更新流程详解 ​

哪些情况下子组件可以跳过更新 ​

父组件重渲染不意味着每个子组件都要重渲染。运行时会结合 props 值、插槽、指令、编译提示等判断;子组件自身读取的响应式状态变化仍可独立触发更新。

vue
<!-- 固定的字符串属性值 -->
<ChildComp msg="固定消息" />
<!-- 动态绑定表达式;变量叫 staticString 也不自动等于编译期常量 -->
<ChildComp :msg="message" />

动态绑定重新求值后若值未变,通常可避免对应的 props 更新;但不能据此保证子组件在所有条件下都不更新。第 7 节用“父状态变化、子 props 稳定”和“子自身状态变化”分别观察。


5. 生命周期完整图 ​

NOTE

KeepAlive 从活动视图移出组件并保留缓存时是 deactivation,不是最终卸载;再次取出缓存会 activated。缓存被淘汰或 KeepAlive 自身卸载时,组件仍会真正 unmount。onActivated 也会在首次挂载触发,onDeactivated 也参与最终卸载,不应把它们当作 mounted/unmounted 的简单替代。KeepAlive 生命周期

各钩子的使用建议 ​

钩子典型用途注意事项
setup初始化状态、注册钩子当前组件 DOM 尚未挂载
onMounted操作 DOM、初始化第三方库同步子组件已 mounted;该钩子不在 SSR 执行
onBeforeUpdate读取旧 DOM 状态(如滚动位置)很少用
onUpdatedDOM 已更新后的操作修改影响自身渲染的状态可能造成更新循环
onBeforeUnmount清理准备DOM 还在
onUnmounted移除事件监听、定时器、WebSocket

6. setup 的特殊地位 ​

Options API 与 setup 同时存在时,setup 先于 beforeCreate / created 执行;在 setup 被调用之前,props 与 slots 已经初始化。不能把 setup 机械地写成“beforeCreate 与 created 之间”,也不能等同于这两个钩子的简单合并。beforeCreate 的执行顺序

typescript
import { defineComponent, ref, onMounted } from 'vue'
export default defineComponent({
  beforeCreate() { console.log('Options beforeCreate') },
  created() { console.log('Options created') },
  setup() {
    console.log('setup')
    const count = ref(0)
    onMounted(() => console.log('DOM 已挂载'))
    return { count }
  },
})
// 创建实例时,前三条日志依次是 setup → Options beforeCreate → Options created。

<script setup> 的编译原理 ​

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const count = ref(0)
onMounted(() => console.log('已挂载'))
</script>
<template><button @click="count++">{{ count }}</button></template>

script setup 的主体会进入实例的 setup 执行范围,模板可以使用顶层绑定。这并不意味着编译结果必定是 return { 所有变量 }:不同模式可能返回经过处理的绑定,也可能让内联 render 闭包直接访问它们。普通 script 的模块顶层则按模块执行,不是每个实例都重新运行。

另外,script setup 组件默认不把这些绑定全部公开给父组件的模板 ref;要公开特定成员,使用 defineExpose。模板可访问与外部实例公开接口是两件事。script setup


7. 从代码到像素:完整链路 ​

运行一个实例生命周期实验 ​

保存到 L31 的独立实验目录,把 index.html 的 script src 改成 /lifecycle-demo.ts,保留 L35 的 vite.config.ts(Vue 编译标志),继续使用 L33 的 Vite 命令。这里使用真实 Vue 3.5.43,不扩展 mini 的组件系统:

typescript
// labs/mini-reactivity/lifecycle-demo.ts
import {
  createApp, defineComponent, h, ref, watch,
  onBeforeMount, onMounted, onBeforeUpdate, onUpdated,
  onBeforeUnmount, onUnmounted, onScopeDispose,
} from 'vue'
const log = (message: string) => console.log(message)
const Child = defineComponent({
  props: { msg: { type: String, required: true } },
  setup(props) {
    log('Child setup')
    const count = ref(0)
    watch(count, value => log(`Child watch ${value}`))
    onBeforeMount(() => log('Child beforeMount'))
    onMounted(() => log('Child mounted'))
    onBeforeUpdate(() => log('Child beforeUpdate'))
    onUpdated(() => log('Child updated'))
    onBeforeUnmount(() => log('Child beforeUnmount'))
    onUnmounted(() => log('Child unmounted'))
    const onResize = () => log('Child resize')
    onMounted(() => window.addEventListener('resize', onResize))
    onScopeDispose(() => {
      window.removeEventListener('resize', onResize)
      log('Child scope disposed')
    })
    return () => {
      log('Child render')
      return h('section', [
        h('p', `${props.msg} / 子计数 ${count.value}`),
        h('button', { onClick: () => { count.value++ } }, '子组件增加'),
      ])
    }
  },
})
const Parent = defineComponent({
  beforeCreate() { log('Parent beforeCreate') },
  created() { log('Parent created') },
  setup() {
    log('Parent setup')
    const count = ref(0)
    const visible = ref(true)
    onBeforeMount(() => log('Parent beforeMount'))
    onMounted(() => log('Parent mounted'))
    onBeforeUpdate(() => log('Parent beforeUpdate'))
    onUpdated(() => log('Parent updated'))
    return () => {
      log('Parent render')
      return h('main', [
        h('h1', '组件实例实验'),
        h('p', `父计数 ${count.value}`),
        h('button', { onClick: () => { count.value++ } }, '父组件增加'),
        h('button', { onClick: () => { visible.value = !visible.value } }, visible.value ? '卸载子组件' : '重新创建子组件'),
        visible.value ? h(Child, { msg: '固定消息' }) : null,
      ])
    }
  },
})
createApp(Parent).mount('#app')

先观察 setup 与 Options 钩子的顺序。点“父组件增加”,子 props 不变且没有动态插槽,子 render 可跳过;点“子组件增加”,子自身依赖仍会触发更新。卸载后再创建会出现新的 Child setup,子计数重置,说明“setup 一次”只限同一个实例。卸载期间 scope 清理移除 resize listener;应当由谁创建外部资源,谁登记相应清理。


8. 本节总结 ​

检查清单 ​

  • [ ] 能画出组件从 setup → render → mount 的完整流程
  • [ ] 理解组件实例包含的核心属性(uid、subTree、effect、hooks)
  • [ ] 理解更新流程:数据变化 → queueJob → render → patch → DOM
  • [ ] 知道嵌套组件的 mounted 顺序(子先于父)
  • [ ] 理解每个生命周期钩子的调用时机和适用场景
  • [ ] 理解 setup 的实例执行范围及其先于 Options beforeCreate/created 的顺序
  • [ ] 理解 <script setup> 的编译原理

Git 提交 ​

bash
git add .
git commit -m "L36: 组件实例 + 渲染流程 + 生命周期 + script setup 编译"

🔗 → 下一节 ​

L37 将回答一个设计哲学问题:Composition API 如何组织复用逻辑,它与仍受支持的 Options API 各有什么适用场景?