vue3.x 组件渲染机制

22 阅读7分钟

image.png

组件热更新 rerender

image.png

function rerender(id: string, newRender?: Function): void {
  const record = map.get(id)
  // 防御性检查:确保组件记录存在,避免空指针异常
  if (!record) {
    return
  }

  // update initial record (for not-yet-rendered component)
  // 更新初始记录的 render 方法,确保尚未渲染的组件也能使用新的渲染函数
  record.initialDef.render = newRender

  // Create a snapshot which avoids the set being mutated during updates
  ;[...record.instances].forEach(instance => {
    if (newRender) {
      // 新实例的 render 方法
      instance.render = newRender as InternalRenderFunction
      // 更新组件类型定义的 render 方法,确保一致性
      normalizeClassComponent(instance.type as HMRComponent).render = newRender
    }
    // 清空实例的渲染缓存
    instance.renderCache = []
    // this flag forces child components with slot content to update
    isHmrUpdating = true
    // #13771 don't update if the job is already disposed
    if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
      // 强制重新渲染
      instance.update()
    }
    isHmrUpdating = false
  })
}
enum SchedulerJobFlags {
  QUEUED = 1 << 0, // 1 标记已经加入队列
  PRE = 1 << 1, // 2 标记任务在 DOM 更新前 执行
  ALLOW_RECURSE = 1 << 2, // 4 允许自身递归
  DISPOSED = 1 << 3, // 已被销毁或已取消
}

setupRenderEffect

setupRenderEffect 函数是组件渲染副作用的核心入口。其核心作用是:为组件实例创建一个响应式渲染副作用(effect),并立即执行一次,完成组件的首次挂载;后续当组件依赖的响应式数据发生变化时,通过调度器重新执行该副作用,触发组件更新。

image.png

componentUpdateFn 执行流程

负责在正确的时机调用生命周期钩子、生成虚拟 DOM(VNode)、调用 patch 进行 DOM 操作,并将异步钩子(如 mountedupdated)放入后置渲染队列。

image.png

instance.update 是组件的渲染副作用函数(即 effect.run),主要负责执行完整的渲染/更新流程。它在以下时机触发:

  1. 首次挂载时:在 setupRenderEffect 函数创建 effect 后,立即同步执行 instance.update() ,从而触发组件的首次渲染。
  2. 响应式数据变化时:当组件依赖的响应式数据(refreactivecomputed 等)被修改时,会触发渲染副作用的调度器(scheduler),将 instance.update 推入 Vue 的异步更新队列(微任务队列)。随后在下一个 tick 执行,从而实现批量更新。
  3. 强制手动更新:可以显式调用 instance.update() 强制组件重新渲染(例如在一些特殊场景中使用了非响应式数据)。

renderComponentRoot

renderComponentRoot 负责 执行组件渲染函数并生成根 VNode 的核心函数。

  1. 从组件实例中提取渲染所需的所有上下文(如 render 函数、propsslotsattrs 等)。
  2. 调用 render 函数得到组件子树。
  3. 对返回的 VNode 进行规范化、属性透传、指令继承和过渡钩子绑定等处理。
  4. 最终返回一个可供 patch 阶段使用的完整 VNode。

image.png

image.png

renderComponentRoot 执行的 render 来源 instance.render

  • 模板编译生成的渲染函数。当组件使用 <template> 时,无论是构建时,还是运行时,都会将模板编译成 render 函数,并挂载到组件定义上,最终赋值给 instance.render
  • 用户直接定义的 render 选项。在选项式 API 或组合式 API 的 defineComponent 中,可以显式提供 render 函数,它会直接成为 instance.render
  • setup 函数返回的渲染函数。当 setup 函数返回一个函数时,该函数被视为组件的渲染函数。

instance.render 是组件的渲染函数,由模板编译或用户直接提供,其唯一职责是返回 VNode 树。它只在 instance.update 执行期间被调用,而且每次 update 执行时都会重新调用 render(无论挂载还是更新)。具体来说:

  • 首次挂载instance.update 执行挂载分支 → 调用 renderComponentRoot(instance) → 内部调用 instance.render() 生成根 VNode 树(subTree)。
  • 响应式更新instance.update 执行更新分支 → 再次调用 renderComponentRoot(instance) → 内部再次调用 instance.render() 生成新的 VNode 树。
  • 注意:如果 setup 返回了一个渲染函数,那么这个渲染函数最终也会被赋值给 instance.render,因此执行时机相同。

processComponent

patch 阶段,针对 shapeFlag & ShapeFlags.COMPONENT处理

image.png

🍒 mountComponent 组件实例挂载

  1. 创建或获取组件实例:若兼容模式下已有实例则复用,否则调用 createComponentInstance 新建。
  2. 特殊组件处理:若为 <KeepAlive> 组件,注入渲染器内部方法以便管理缓存。
  3. 初始化组件:通过 setupComponent 标准化 props/slots、执行 setup 函数、合并选项式 API。
  4. 处理 HMR 更新(开发环境):清空已有 DOM 引用,避免水合错误。
  5. 区分同步/异步 setup
    • 若存在 asyncDep(异步 setup):将组件注册到父级 <Suspense>,并先渲染一个注释节点作为占位符。
    • 否则:直接调用 setupRenderEffect 创建渲染副作用并立即执行首次挂载。

setupComponent 组件初始化

  1. 初始化 props 和 slots
    • 调用 initProps 将 VNode 上的 props 标准化并挂载到组件实例上。
    • 调用 initSlots 将 VNode 的 children 转换为规范的插槽(slots)结构。
  2. 根据组件类型执行 setup
    • 判断当前组件是否为有状态组件(isStatefulComponent)。
    • 若是,调用 setupStatefulComponent 执行 setup 函数(组合式 API 入口),并返回其结果(同步返回值或异步 Promise)。
    • 若是函数式组件,直接返回 undefined

image.png

setupStatefulComponent

  1. 创建组件的渲染代理(instance.proxy),使模板能够通过 this 访问 propsdatasetupState 等上下文;
  2. 执行组件的 setup 函数(组合式 API 入口),并根据返回值类型分别处理:
    • 同步返回值立即合并为 setupState 或渲染函数,
    • 异步返回值(Promise)则存储到 instance.asyncDep 中交给 <Suspense> 或 SSR 等待,同时暂停/恢复响应式追踪以避免副作用;
  3. 若没有 setup 函数,则回退到 finishComponentSetup 完成选项式 API 的初始化。

image.png

image.png

handleSetupResult 执行

负责处理 setup() 函数的返回值,并将其转换为组件实例的内部状态

  1. 若 setupResult 是函数
    • 在 SSR 环境下且组件标记了 __ssrInlineRender,将该函数设置为 instance.ssrRender
    • 否则,将该函数设置为 instance.render,作为组件的渲染函数。
  2. 若 setupResult 是对象
    • 如果对象是 VNode(开发环境),会警告应该返回渲染函数而非 VNode。
    • 将该对象通过 proxyRefs 包装为响应式对象,并保存到 instance.setupState
    • 开发环境下,调用 exposeSetupStateOnRenderContext 将 setupState 的属性暴露到渲染上下文中。
  3. 若 setupResult 是其他非 undefined 值(开发环境):
    • 发出警告,提示 setup() 应该返回对象或函数。

🍒 updateComponent 更新组件实例

shouldUpdateComponent 通过多层优化策略判断组件是否需要更新:

  1. 快速路径:HMR、指令、过渡等场景直接返回 true
  2. 优化路径:利用编译时 patchFlag 精确检测变化
  3. 降级路径:手动渲染函数使用更保守的判断逻辑

app.mount

在 Vue 3 项目初始化时,app.mount 先执行,然后内部触发首次渲染(render 和 patch

image.png

  const render: RootRenderFunction = (vnode, container, namespace) => {
    let instance
    if (vnode == null) {
      if (container._vnode) {
        // 卸载
        unmount(container._vnode, null, null, true)
        instance = container._vnode.component
      }
    } else {
      // 渲染/更新场景
      patch(
        container._vnode || null, // 旧的虚拟节点(如果存在)
        vnode, // 新的虚拟节点
        container, // 渲染目标容器
        null, // 元素命名空间
        null, // 子节点
        null, // 元素命名空间
        namespace,
      )
    }
    container._vnode = vnode // 更新容器引用
    if (!isFlushing) {
      isFlushing = true
      // 执行预刷新回调(如 watch 的 flush: 'pre')
      flushPreFlushCbs(instance)
      // 执行后刷新回调(如 nextTick、过渡效果)
      flushPostFlushCbs()
      isFlushing = false
    }
  }

vue模板可以使用的变量

  • $attrs, 父组件传入的非 prop 属性(class、style、事件等)
  • $slots, 组件接收的插槽对象
  • $refs, 通过 ref 属性注册的 DOM 元素或子组件实例
  • $el, 组件根 DOM 元素(仅在挂载后可用)
  • $options, 组件选项对象(包含 name, components, data 等)
  • $parent, 父组件实例
  • $root, 根组件实例
  • $data, 组件的整个数据对象
  • $props, 当前组件的 props 对象
  • $nextTick, 等待 DOM 更新后的回调函数
  • $emit, 触发父组件监听的事件(模板中较少直接使用)
  • $forceUpdate, 强制组件重新渲染(不推荐常规使用)

示例 定义的响应式变量 、props、methods、

<template>
  <div>
    <div>TemplateA</div>
    <div>count: {{ count }}</div>
    <p>路由名称 {{ $route.name }}</p>
    <button @click="($event) => handleClick($event)">click</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);

const handleClick = (data: MouseEvent) => {
  console.log(data);
};
</script>

image.png

示例 父组件透传的 props,子组件未声明的 props

<template>
  <div>
    <div>TemplateA</div>
    <div>count: {{ count }}</div>
    <p>{{ $.attrs.message }}</p>
    <!-- <button @click="() => handleClick($data, $options, $route)">click</button> -->
    <button @click="$router.push('/cloud/group')">click</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);

// const handleClick = (data: any, options: any, route: any) => {
//   console.log("data", data);
//   console.log("options", options);
//   console.log("route", route);
// };
</script>

image.png

示例 路由 $router$route

<template>
  <div>
    <div>TemplateA</div>
    <div>count: {{ count }}</div>
    <!-- <p>路由名称 {{ $route.name }}</p> -->
    <!-- <button @click="() => handleClick($data, $options, $route)">click</button> -->
    <button @click="$router.push('/cloud/group')">click</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);

// const handleClick = (data: any, options: any, route: any) => {
//   console.log("data", data);
//   console.log("options", options);
//   console.log("route", route);
// };
</script>

image.png

示例 事件 $event

<template>
  <div>
    <div>TemplateA</div>
    <div>count: {{ count }}</div>
    <p>路由名称 {{ $route.name }}</p>
    <button @click="() => handleClick($data, $options, $route)">click</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleClick = (data: any, options: any, route: any) => {
  console.log("data", data);
  console.log("options", options);
  console.log("route", route);
};
</script>

image.png

image.png

示例 组件接收的插槽对象$slots

<template>
  <div>
    <div>TemplateA</div>
    <div>count: {{ count }}</div>
    <p>{{ $.attrs.message }}</p>
    <div>
      <component :is="$slots.default" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
</script>

最后