Skip to content

第 10 章 组件系统

本章要点

  • 组件实例的完整数据结构:从 ComponentInternalInstance 的 40+ 字段到它们各自的职责
  • 组件创建的全流程:从 createComponentInstance 到 setupComponent 再到 setupRenderEffect
  • Props 系统的深层机制:声明、解析、校验、响应式代理的四阶段流水线
  • Emit 事件系统的实现:命名规范化、验证、监听器查找的完整链路
  • Slots 的编译与运行时协作:静态 slots、动态 slots、作用域 slots 的统一处理
  • expose 的安全边界:如何控制组件的公共 API 表面
  • 异步组件与 Suspense 的协作机制

在前面的章节中,我们花了大量篇幅讨论响应式系统和编译器。但 Vue 的核心抽象不是 ref,不是模板,而是组件。组件是 Vue 开发者日常工作的基本单元——你创建组件、组合组件、在组件之间传递数据。响应式系统和编译器都是为组件服务的基础设施。

本章将深入组件系统的内部机制。我们不是在讨论"如何使用组件",而是在追问"组件是如何被创建、初始化、更新和销毁的"。当你在模板中写下 <MyComponent :msg="hello" /> 时,背后到底发生了什么?

10.1 组件实例的数据结构

ComponentInternalInstance:组件的"身份证"

每个 Vue 组件在运行时都对应一个 ComponentInternalInstance 对象。这个对象是组件系统的核心数据结构,它承载了组件从诞生到销毁的全部状态:

typescript
// packages/runtime-core/src/component.ts
export interface ComponentInternalInstance {
  uid: number                           // 全局唯一 ID
  type: ConcreteComponent               // 组件定义(选项对象或 setup 函数)
  parent: ComponentInternalInstance | null  // 父组件实例
  root: ComponentInternalInstance       // 根组件实例
  appContext: AppContext                 // 应用级上下文

  // ---- VNode 相关 ----
  vnode: VNode                          // 组件自身的 VNode
  subTree: VNode                        // 组件渲染输出的 VNode 子树
  next: VNode | null                    // 待更新的 VNode(父组件触发的更新)

  // ---- 渲染相关 ----
  render: InternalRenderFunction | null // 编译后的渲染函数
  proxy: ComponentPublicInstance | null // 模板中的 `this` 代理
  withProxy: ComponentPublicInstance | null // 带缓存的渲染代理

  // ---- 状态相关 ----
  setupState: Data                      // setup() 返回的状态
  props: Data                           // 解析后的 props
  attrs: Data                           // 非 prop 的 attributes(透传)
  slots: InternalSlots                  // 插槽
  refs: Data                            // 模板 ref 引用

  // ---- 副作用相关 ----
  effect: ReactiveEffect               // 组件的渲染 effect
  scope: EffectScope                    // 组件的 effect 作用域
  update: SchedulerJob                  // 组件更新函数

  // ---- 生命周期 ----
  isMounted: boolean
  isUnmounted: boolean
  isDeactivated: boolean

  // ---- 生命周期钩子 ----
  bc: LifecycleHook                     // beforeCreate
  c: LifecycleHook                      // created
  bm: LifecycleHook                     // beforeMount
  m: LifecycleHook                      // mounted
  bu: LifecycleHook                     // beforeUpdate
  u: LifecycleHook                      // updated
  bum: LifecycleHook                    // beforeUnmount
  um: LifecycleHook                     // unmounted

  // ---- 其他 ----
  emit: EmitFn                          // 事件发射函数
  emitted: Record<string, boolean> | null
  provides: Data                        // provide/inject 数据
  exposed: Record<string, any> | null   // expose 暴露的 API
  exposeProxy: Record<string, any> | null
}

40 多个字段,每一个都有明确的职责。这个结构体就像一个生物细胞——外表是统一的组件接口,内部是精密协作的功能模块。

为什么需要这么多字段?

你可能会疑问:一个组件真的需要这么多状态吗?答案是肯定的,因为组件身兼数职:

10.2 组件创建流程

从 VNode 到组件实例

当渲染器遇到一个组件类型的 VNode 时,会调用 mountComponent

typescript
// packages/runtime-core/src/renderer.ts
const mountComponent = (
  initialVNode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  optimized: boolean
) => {
  // 第一步:创建组件实例
  const instance: ComponentInternalInstance =
    (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

  // 第二步:初始化组件(处理 props、slots、执行 setup)
  setupComponent(instance)

  // 第三步:建立渲染 effect
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    namespace,
    optimized
  )
}

三步曲,清晰明了。让我们逐一深入。

第一步:createComponentInstance

typescript
// packages/runtime-core/src/component.ts
export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
): ComponentInternalInstance {
  const type = vnode.type as ConcreteComponent
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext

  const instance: ComponentInternalInstance = {
    uid: uid++,
    vnode,
    type,
    parent,
    appContext,
    root: null!,            // 稍后设置
    subTree: null!,         // 首次渲染时设置
    effect: null!,          // setupRenderEffect 中设置
    update: null!,          // setupRenderEffect 中设置
    scope: new EffectScope(true /* detached */),

    render: null,
    proxy: null,
    withProxy: null,

    provides: parent ? parent.provides : Object.create(appContext.provides),

    // 状态
    setupState: EMPTY_OBJ,
    props: EMPTY_OBJ,
    attrs: EMPTY_OBJ,
    slots: EMPTY_OBJ,
    refs: EMPTY_OBJ,

    // 生命周期标记
    isMounted: false,
    isUnmounted: false,
    isDeactivated: false,

    // 生命周期钩子
    bc: null, c: null, bm: null, m: null,
    bu: null, u: null, bum: null, um: null,

    emit: null!,            // 稍后设置
    emitted: null,
    exposed: null,
    exposeProxy: null,

    next: null,
  }

  // 设置 root 引用
  instance.root = parent ? parent.root : instance

  // 创建 emit 函数
  instance.emit = emit.bind(null, instance)

  return instance
}

注意 provides 的初始化策略:Object.create(parent.provides)。通过原型链继承,子组件可以访问所有祖先组件提供的值,同时自己 provide 的值只影响后代。

第二步:setupComponent

typescript
// packages/runtime-core/src/component.ts
export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
): Promise<void> | void {
  const { props, children } = instance.vnode

  // 1. 初始化 props
  initProps(instance, props, isStatefulComponent(instance), isSSR)

  // 2. 初始化 slots
  initSlots(instance, children)

  // 3. 如果是有状态组件,执行 setup
  const setupResult = isStatefulComponent(instance)
    ? setupStatefulComponent(instance, isSSR)
    : undefined

  return setupResult
}

setupStatefulComponent:执行 setup 函数

typescript
function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions

  // 创建渲染代理的缓存
  instance.accessCache = Object.create(null)

  // 创建公共实例代理
  // 这个代理就是模板中的 `this` 和 setup 中不应该直接使用的上下文
  instance.proxy = markRaw(
    new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  )

  // 执行 setup
  const { setup } = Component
  if (setup) {
    // 如果 setup 接受参数,创建 setupContext
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    // 设置当前实例(让 onMounted 等 API 知道它们属于哪个组件)
    setCurrentInstance(instance)
    pauseTracking()

    // 执行 setup 函数
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [
        __DEV__ ? shallowReadonly(instance.props) : instance.props,
        setupContext
      ]
    )

    resetTracking()
    unsetCurrentInstance()

    // 处理 setup 的返回值
    if (isPromise(setupResult)) {
      // 异步 setup——交给 Suspense 处理
      setupResult.then(
        result => handleSetupResult(instance, result, isSSR),
        err => handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
      )
      return setupResult
    } else {
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    // 没有 setup,使用选项式 API
    finishComponentSetup(instance, isSSR)
  }
}

这里有几个关键细节:

  1. pauseTracking():在执行 setup 期间暂停响应式追踪,防止 setup 函数本身被当作一个 effect 追踪
  2. setCurrentInstance():设置全局的"当前组件实例",让 onMounted() 等 Composition API 知道自己被注册到哪个组件
  3. setup 的 props 参数:在开发模式下是 shallowReadonly,防止用户意外修改 props

handleSetupResult:处理 setup 的返回值

typescript
export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  if (isFunction(setupResult)) {
    // setup 返回了渲染函数
    instance.render = setupResult as InternalRenderFunction
  } else if (isObject(setupResult)) {
    // setup 返回了状态对象
    instance.setupState = proxyRefs(setupResult)
  }

  finishComponentSetup(instance, isSSR)
}

proxyRefs 是一个精巧的设计:它让模板中访问 ref 时不需要写 .value。当 setup 返回 { count: ref(0) } 时,模板中可以直接写 {{ count }} 而不是 {{ count.value }}

第三步:setupRenderEffect

基于 VitePress 构建