Skip to content

实现组件的初始化流程

本节对标 Vue 3 源码 @vue/runtime-core 中的 component.tscomponentRenderUtils.tsrenderer.ts

组件系统概览

组件是 Vue 中最核心的抽象单位。一个组件从创建到渲染,经历以下流程:

VNode(type=组件) → createComponentInstance → setupComponent → 创建渲染 effect → render() → subTree → patch

组件的本质:将一段模板(或 render 函数)+ 响应式状态封装为可复用的 VNode 生产者。

createComponentInstance —— 创建组件实例

对标 packages/runtime-core/src/component.ts —— createComponentInstance

组件实例是整个组件系统的"数据中心",保存了组件运行所需的全部状态:

ts
// 组件内部实例的接口定义,描述了组件运行时需要的所有状态字段
export interface ComponentInternalInstance {
  uid: number                                // 组件唯一标识符,每个实例递增
  type: any                                  // 组件的选项对象(即用户定义的组件配置)
  vnode: VNode                               // 组件自身的 VNode(type 为组件选项对象)
  next: VNode | null                         // 更新时新的组件 VNode,用于 updateComponent 流程
  subTree: VNode                             // 组件 render() 返回的子树 VNode,即组件实际渲染的内容
  parent: ComponentInternalInstance | null    // 父组件实例,用于 provide/inject 链式查找

  // 状态相关
  props: Record<string, any>                 // 外部传入的属性,经 shallowReactive 包装后具有响应性
  setupState: Record<string, any>            // setup() 函数返回的状态对象
  slots: Record<string, Function>            // 插槽内容,key 为插槽名,value 为渲染函数
  attrs: Record<string, any>                 // 未在 props 中声明的属性会归入 attrs

  // 生命周期钩子数组(数组是因为同一个钩子可以注册多次)
  isMounted: boolean                         // 是否已挂载,用于区分 mount 和 update 流程
  bc: Function[] | null   // beforeCreate
  c: Function[] | null    // created
  bm: Function[] | null   // beforeMount
  m: Function[] | null    // mounted
  bu: Function[] | null   // beforeUpdate
  u: Function[] | null    // updated

  // 渲染相关
  render: Function | null                    // 组件的渲染函数,来自 setup 返回或组件选项
  proxy: any                                 // 组件的代理对象,作为 render 函数中的 this
  emit: Function                             // 触发自定义事件的函数,已绑定当前实例

  // provide/inject 机制
  provides: Record<string | symbol, any>     // 向子组件提供的数据,继承自父组件
}

// 全局自增 id,用于给每个组件实例分配唯一标识
let uid = 0

// 创建组件实例:根据 VNode 和父组件实例生成一个全新的组件内部实例对象
export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
): ComponentInternalInstance {
  const instance: ComponentInternalInstance = {
    uid: uid++,                              // 分配递增的唯一 id
    type: vnode.type,                        // 保存组件选项对象,后续用于访问 setup、render 等
    vnode,                                   // 保存组件自身的 VNode 引用
    next: null,                              // 初始无待更新的 VNode
    subTree: null!,                          // 初始无子树,挂载时才会赋值(null! 表示稍后一定会赋值)
    parent,                                  // 记录父组件实例,用于 provide/inject 向上查找

    props: {},                               // 初始为空,由 initProps 填充
    setupState: {},                          // 初始为空,由 handleSetupResult 填充
    slots: {},                               // 初始为空,由 initSlots 填充
    attrs: {},                               // 初始为空,由 initProps 中分离出未声明的属性填充

    isMounted: false,                        // 初始未挂载,首次渲染后置为 true
    bc: null,                                // 各生命周期钩子初始为 null,注册时才创建数组
    c: null,
    bm: null,
    m: null,
    bu: null,
    u: null,

    render: null,                            // 初始无渲染函数,由 setup 或组件选项提供
    proxy: null,                             // 初始无代理,在 setupStatefulComponent 中创建
    emit: null!,                             // 初始占位,下方立即绑定

    provides: parent ? parent.provides : {}, // 继承父组件的 provides,实现原型链式的 provide/inject
  }

  // 将 emit 函数绑定当前实例,这样用户调用 emit 时不需要手动传 instance
  instance.emit = emit.bind(null, instance)

  return instance
}

字段解析

字段用途
vnode组件自身的 VNode(type 为组件选项对象)
subTree组件 render() 返回的 VNode 子树
next更新时,新的组件 VNode(用于 updateComponent)
props外部传入的属性,shallowReactive 包装
setupStatesetup() 返回的状态对象
slots插槽内容
proxy组件的代理对象,render 函数中的 this
providesprovide/inject 数据,继承自 parent
isMounted是否已挂载,区分 mount 和 update

setupComponent —— 初始化流程

对标 packages/runtime-core/src/component.ts —— setupComponent

ts
// 组件初始化的入口函数,依次完成 props、slots、setup 的初始化
export function setupComponent(instance: ComponentInternalInstance) {
  // 从组件 VNode 中解构出 props(外部传入的属性)和 children(子节点/插槽内容)
  const { props, children } = instance.vnode

  // 1. 初始化 props:将 VNode 上的 props 分离为 instance.props 和 instance.attrs
  initProps(instance, props)

  // 2. 初始化 slots:将 children 规范化并挂载到 instance.slots 上
  initSlots(instance, children)

  // 3. 执行 setup 函数(针对有状态组件),创建 proxy 并处理 setup 返回值
  setupStatefulComponent(instance)
}

这三步是组件初始化的核心:

setupComponent
    ├── initProps      → 处理 props 和 attrs
    ├── initSlots      → 处理插槽
    └── setupStatefulComponent → 执行 setup()

setupStatefulComponent —— 执行 setup() 函数

ts
// 处理有状态组件的 setup 逻辑
function setupStatefulComponent(instance: ComponentInternalInstance) {
  // 取出组件选项对象(即用户定义的 { setup, render, ... })
  const Component = instance.type

  // 创建代理对象,作为 render 函数中的 this
  // PublicInstanceProxyHandlers 会将属性访问代理到 setupState、props 等位置
  instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers)

  // 从组件选项中取出 setup 函数
  const { setup } = Component
  if (setup) {
    // 在执行 setup 之前,将 currentInstance 设置为当前实例
    // 这样 setup 内部调用 onMounted、provide 等 API 时能找到当前组件
    setCurrentInstance(instance)
    // 调用 setup,传入只读的 props 和上下文对象
    // shallowReadonly 确保用户不能在 setup 中直接修改 props
    const setupResult = setup(
      shallowReadonly(instance.props),
      {
        emit: instance.emit,     // 事件触发函数
        slots: instance.slots,   // 插槽内容
        attrs: instance.attrs,   // 非 props 属性
        expose: () => {},        // 暴露公共方法(简化实现)
      },
    )
    // setup 执行完毕后清除 currentInstance,防止在 setup 外部误用
    setCurrentInstance(null)

    // 根据 setup 的返回值类型(函数或对象)进行不同处理
    handleSetupResult(instance, setupResult)
  } else {
    // 如果组件没有定义 setup 函数,直接完成组件设置(使用选项中的 render)
    finishComponentSetup(instance)
  }
}

PublicInstanceProxyHandlers

组件的 proxy 是一个 Proxy,统一代理对 setupStateprops$ 开头的公共属性的访问:

ts
// 组件公共实例的 Proxy 处理器,统一拦截对组件属性的读取和设置操作
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
  // 拦截属性读取:按优先级从 setupState、props、公共属性中查找
  get({ _: instance }, key: string) {
    const { setupState, props } = instance

    // 非 $ 开头的属性:先查 setupState,再查 props
    if (key[0] !== '$') {
      // setupState 优先级高于 props,这意味着如果两者都有同名属性,setupState 胜出
      if (hasOwn(setupState, key)) {
        return setupState[key]
      } else if (hasOwn(props, key)) {
        return props[key]
      }
    }

    // $ 开头的公共属性(如 $el、$slots 等),从预定义的映射表中查找
    const publicGetter = publicPropertiesMap[key]
    if (publicGetter) {
      return publicGetter(instance)
    }
  },
  // 拦截属性设置:只允许设置 setupState 中的属性(props 是只读的)
  set({ _: instance }, key: string, value: any) {
    const { setupState } = instance
    if (hasOwn(setupState, key)) {
      setupState[key] = value  // 修改 setupState 中的属性,会触发响应式更新
      return true
    }
    return true  // 即使属性不存在也返回 true,避免抛出错误
  },
}

// $ 开头的公共属性映射表,将 $el、$slots 等映射到实例的对应字段
const publicPropertiesMap: Record<string, (i: ComponentInternalInstance) => any> = {
  $el: (i) => i.vnode.el,    // 组件根 DOM 元素
  $slots: (i) => i.slots,    // 插槽对象
  $props: (i) => i.props,    // props 对象
  $emit: (i) => i.emit,      // 事件触发函数
}

这样在 render 函数或模板中:

  • this.count → 先查 setupState.count,再查 props.count
  • this.$slots → 返回 instance.slots
  • this.$emit → 返回 instance.emit

handleSetupResult —— 处理 setup 返回值

ts
// 处理 setup() 函数的返回值,支持两种返回类型
function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: any,
) {
  if (typeof setupResult === 'function') {
    // setup 返回函数 → 将其作为组件的 render 函数
    instance.render = setupResult
  } else if (typeof setupResult === 'object' && setupResult !== null) {
    // setup 返回对象 → 存入 setupState,作为模板/render 中可访问的状态
    // proxyRefs 包装后,模板中使用 ref 无需写 .value(自动解包)
    instance.setupState = proxyRefs(setupResult)
  }

  // 最终确保 instance.render 有值(如果 setup 没返回 render 函数,则从组件选项中取)
  finishComponentSetup(instance)
}

两种返回值

ts
// 返回对象 —— 暴露状态给模板使用
setup() {
  const count = ref(0)
  return { count }  // 模板中可以直接 {{ count }} 而非 {{ count.value }}
}

// 返回函数 —— 直接作为 render 函数
setup() {
  const count = ref(0)
  return () => h('div', count.value)
}

proxyRefs 的作用:在模板中使用 ref 时不需要写 .value。它会自动解包 ref:

ts
const state = proxyRefs({ count: ref(0) })
state.count // 0(自动解包,不需要 .value)
state.count = 1 // 自动设置 ref.value

finishComponentSetup —— 设置 render 函数

ts
// 完成组件设置:确保 instance.render 有值
function finishComponentSetup(instance: ComponentInternalInstance) {
  const Component = instance.type  // 取出组件选项对象

  if (!instance.render) {
    // 如果 setup 没有返回 render 函数,则使用组件选项中定义的 render 方法
    instance.render = Component.render
  }
}

在完整的 Vue 3 中,这里还会处理 template 编译:

ts
// 完整版 Vue 3 中的 finishComponentSetup,额外处理 template 编译
function finishComponentSetup(instance) {
  const Component = instance.type
  if (!instance.render) {
    // 如果组件定义了 template 但没有预编译的 render 函数
    if (Component.template && !Component.render) {
      // 在运行时调用编译器将 template 字符串编译为 render 函数
      Component.render = compile(Component.template)
    }
    // 将编译后(或用户定义)的 render 赋值给实例
    instance.render = Component.render
  }
}

currentInstance 机制

对标 packages/runtime-core/src/component.ts —— currentInstance

ts
// 全局变量,记录当前正在执行 setup 的组件实例
// Composition API(如 onMounted、provide)通过它访问当前组件
export let currentInstance: ComponentInternalInstance | null = null

// 设置当前实例:在 setup 执行前调用传入 instance,执行后传入 null 清除
export function setCurrentInstance(instance: ComponentInternalInstance | null) {
  currentInstance = instance
}

// 获取当前实例:供用户在 setup 中调用,获取当前组件的内部实例
export function getCurrentInstance(): ComponentInternalInstance | null {
  return currentInstance
}

为什么需要 currentInstance?

Composition API 中的 onMountedprovideinject 等函数需要知道"当前正在初始化的是哪个组件实例"。通过全局变量 currentInstance,这些函数可以在 setup() 执行期间访问到当前组件实例:

ts
// 用户代码
setup() {
  // 此时 currentInstance 指向当前组件实例
  onMounted(() => {
    console.log('mounted!')
  })
  // getCurrentInstance() 可以获取当前实例
  const instance = getCurrentInstance()
}

setCurrentInstance 的调用时机:

ts
setCurrentInstance(instance)   // setup 执行前设置
const setupResult = setup(...) // setup 执行中可以访问
setCurrentInstance(null)       // setup 执行后清除

mountComponent —— 挂载组件

对标 packages/runtime-core/src/renderer.ts —— mountComponent

ts
// 挂载组件:从创建实例到首次渲染的完整流程
function mountComponent(
  initialVNode: VNode,                                // 组件的初始 VNode
  container: any,                                     // 挂载的目标容器 DOM 元素
  anchor: any,                                        // 插入锚点(用于精确定位 DOM 插入位置)
  parentComponent: ComponentInternalInstance | null,   // 父组件实例,用于建立组件树关系
) {
  // 1. 创建组件实例,并将实例挂到 VNode 的 component 属性上
  // 这样后续可以通过 VNode 访问到组件实例
  const instance = (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent,
  ))

  // 2. 初始化组件:依次处理 props、slots、执行 setup
  setupComponent(instance)

  // 3. 创建渲染 effect:执行 render 函数并 patch 子树到 DOM
  // 同时建立响应式依赖追踪,后续数据变化会自动触发更新
  setupRenderEffect(instance, initialVNode, container, anchor)
}

setupRenderEffect —— 创建渲染 effect

ts
// 创建渲染 effect:建立响应式依赖追踪,实现数据变化自动触发重新渲染
function setupRenderEffect(
  instance: ComponentInternalInstance,
  initialVNode: VNode,
  container: any,
  anchor: any,
) {
  // 组件更新函数,根据 isMounted 状态区分首次挂载和后续更新
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // === MOUNT 首次挂载分支 ===
      const { bm, m } = instance  // 取出 beforeMount 和 mounted 钩子

      // 执行 beforeMount 生命周期钩子(同步执行)
      if (bm) {
        invokeArrayFns(bm)
      }

      // 执行组件的 render 函数,生成子树 VNode
      const subTree = (instance.subTree = renderComponentRoot(instance))

      // 递归 patch 子树:将子树 VNode 渲染为真实 DOM 并挂载到容器中
      // 第一个参数 null 表示这是首次挂载(无旧 VNode)
      patch(null, subTree, container, anchor, instance)

      // 将子树根节点的真实 DOM 元素赋值给组件 VNode 的 el
      // 这样外部可以通过 vnode.el 访问组件的根 DOM 元素
      initialVNode.el = subTree.el

      // 执行 mounted 生命周期钩子
      if (m) {
        // 放到 post 队列中执行,确保此时 DOM 已经完成更新
        queuePostFlushCb(m)
      }

      // 标记组件已挂载,后续更新将走 update 分支
      instance.isMounted = true
    } else {
      // === UPDATE 更新分支 ===(第 16 节详细实现)
      const { bu, u, next, vnode } = instance

      // 如果有 next,说明是父组件触发的更新(props/slots 变化)
      if (next) {
        next.el = vnode.el  // 复用旧的 DOM 元素引用
        updateComponentPreRender(instance, next)  // 更新 props、slots 等
      }

      // 执行 beforeUpdate 生命周期钩子
      if (bu) {
        invokeArrayFns(bu)
      }

      // 执行 render 生成新的子树 VNode
      const nextTree = renderComponentRoot(instance)
      const prevTree = instance.subTree  // 保存旧子树用于 diff
      instance.subTree = nextTree        // 更新为新子树

      // 对比新旧子树,执行最小化 DOM 更新(diff + patch)
      patch(prevTree, nextTree, container, anchor, instance)

      // 执行 updated 生命周期钩子(异步,确保 DOM 已更新)
      if (u) {
        queuePostFlushCb(u)
      }
    }
  }

  // 创建 ReactiveEffect 实例
  // 第一个参数是副作用函数,第二个参数是 scheduler(调度器)
  // scheduler 的作用:响应式数据变化时不直接执行副作用,而是将更新任务推入异步队列
  const effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),  // 调度器:将 update 推入微任务队列,实现异步批量更新
  )

  // 将 effect.run 封装为 update 函数,并挂到实例上供外部手动触发更新
  const update = (instance.update = () => effect.run())
  update()  // 立即执行一次,触发首次挂载(走 mount 分支)
}

renderComponentRoot

ts
// 执行组件的 render 函数,生成子树 VNode
function renderComponentRoot(
  instance: ComponentInternalInstance,
): VNode {
  const { render, proxy } = instance
  // 调用 render 并将 this 绑定为 proxy,这样 render 中可以通过 this.xxx 访问状态和 props
  // 同时将 proxy 作为第一个参数传入,支持 render(ctx) 风格的写法
  const result = render!.call(proxy, proxy)
  // 规范化返回值:确保返回的一定是 VNode 对象
  return normalizeVNode(result)
}

// 规范化 VNode:将字符串/数字等简单类型转为 Text VNode
function normalizeVNode(child: any): VNode {
  if (typeof child === 'string' || typeof child === 'number') {
    // 字符串或数字会被包装为 Text 类型的 VNode
    return createVNode(Text, null, String(child))
  }
  // 如果已经是 VNode 对象则直接返回
  return child
}

render.call(proxy, proxy) —— this 绑定为 proxy,这样 render 函数中可以通过 this.xxx 访问状态和 props。

effect + scheduler 的协作

ts
// 创建响应式副作用:将组件更新函数与调度器关联
const effect = new ReactiveEffect(
  componentUpdateFn,     // 副作用函数:执行 render + patch
  () => queueJob(update), // scheduler 调度器:数据变化时不立即执行,而是将更新推入异步队列
)
  • 首次执行 update()effect.run() → 执行 componentUpdateFn → mount 分支
  • 响应式数据变化 → trigger → 调用 scheduler(不直接执行副作用) → queueJob(update) → 微任务中批量执行更新
  • 更新执行时 → effect.run() → 执行 componentUpdateFn → update 分支

这种设计实现了异步批量更新:多次数据变化只触发一次组件更新。

完整流程图

h(MyComponent, props)


createVNode(MyComponent, props)  → VNode { type: MyComponent, ... }


patch(null, vnode, container)
    │  shapeFlag & COMPONENT

processComponent(null, vnode, ...)
    │  n1 === null

mountComponent(vnode, container, ...)

    ├── 1. createComponentInstance(vnode, parent)
    │       → instance { type, vnode, props: {}, setupState: {}, ... }

    ├── 2. setupComponent(instance)
    │       ├── initProps(instance, vnode.props)
    │       ├── initSlots(instance, vnode.children)
    │       └── setupStatefulComponent(instance)
    │               ├── instance.proxy = new Proxy(...)
    │               ├── setCurrentInstance(instance)
    │               ├── setup(props, { emit, slots, attrs })
    │               ├── setCurrentInstance(null)
    │               ├── handleSetupResult → instance.setupState / instance.render
    │               └── finishComponentSetup → instance.render

    └── 3. setupRenderEffect(instance, vnode, container, ...)

            ├── componentUpdateFn (mount 分支)
            │       ├── render.call(proxy) → subTree VNode
            │       ├── patch(null, subTree, container)
            │       └── instance.isMounted = true

            └── new ReactiveEffect(fn, scheduler)
                    └── effect.run() → 首次渲染

对比 React

维度Vue 3React
组件实例ComponentInternalInstance 显式对象Fiber 节点
初始化入口setupComponentsetup()renderWithHooks → 函数体
状态存储instance.setupStateFiber 上的 hook 链表
当前实例currentInstance 全局变量currentlyRenderingFiber
渲染触发ReactiveEffect + schedulerscheduleUpdateOnFiber
异步更新queueJob + 微任务MessageChannel / setTimeout
this 访问Proxy 代理函数组件无 this

Vue 3 的组件实例是一个显式的、可操控的对象,而 React Fiber 是一个内部的、不对外暴露的树节点。Vue 的 getCurrentInstance() 允许用户在 setup 中获取实例,React 则通过 hooks 隐式访问 Fiber 状态。

测试用例

ts
describe('component initialization', () => {
  // 测试组件实例创建和 setup 执行:验证 setup 返回的状态能在 render 中通过 this 访问
  it('should create component instance and run setup', () => {
    const Comp = {
      setup() {
        const count = ref(0)          // 创建响应式数据
        return { count }              // 返回对象,暴露给模板使用
      },
      render() {
        return h('div', {}, this.count) // 通过 this.count 访问 setupState(proxyRefs 自动解包 ref)
      },
    }

    const root = document.createElement('div')
    render(h(Comp), root)
    // 验证组件正确渲染:count 初始值为 0
    expect(root.innerHTML).toBe('<div>0</div>')
  })

  // 测试 setup 返回 render 函数的场景:setup 可以直接返回一个函数作为 render
  it('should support setup returning render function', () => {
    const Comp = {
      setup() {
        const msg = ref('hello')
        // 返回函数而非对象,该函数直接作为组件的 render 函数
        return () => h('p', {}, msg.value)
      },
    }

    const root = document.createElement('div')
    render(h(Comp), root)
    // 验证 setup 返回的 render 函数正确执行
    expect(root.innerHTML).toBe('<p>hello</p>')
  })

  // 测试 props 通过 this 访问:render 中可以通过 this 访问外部传入的 props
  it('should access props via this in render', () => {
    const Comp = {
      props: ['msg'],                   // 声明接收的 props
      setup(props: any) {
        return {}                       // 返回空对象,不提供额外状态
      },
      render() {
        return h('span', {}, this.msg)  // 通过 this.msg 访问 props(proxy 会自动查找 props)
      },
    }

    const root = document.createElement('div')
    render(h(Comp, { msg: 'world' }), root)  // 传入 props
    // 验证 props 能正确渲染到 DOM
    expect(root.innerHTML).toBe('<span>world</span>')
  })

  // 测试响应式更新:数据变化后组件应自动重新渲染
  it('should trigger update when reactive data changes', async () => {
    const Comp = {
      setup() {
        const count = ref(0)
        const increment = () => count.value++  // 修改响应式数据的方法
        return { count, increment }
      },
      render() {
        return h(
          'button',
          { onClick: this.increment },  // 绑定点击事件
          String(this.count),           // 显示 count 值
        )
      },
    }

    const root = document.createElement('div')
    render(h(Comp), root)
    // 验证初始渲染
    expect(root.innerHTML).toBe('<button>0</button>')

    const btn = root.querySelector('button')!
    btn.click()            // 模拟点击,触发 count.value++
    await nextTick()       // 等待异步更新完成(微任务队列执行)
    // 验证更新后的渲染结果
    expect(root.innerHTML).toBe('<button>1</button>')
  })

  // 测试 getCurrentInstance:验证在 setup 中可以获取当前组件实例
  it('should support getCurrentInstance in setup', () => {
    let inst: any = null
    const Comp = {
      setup() {
        inst = getCurrentInstance()  // 在 setup 中获取当前实例
        return () => h('div')
      },
    }

    render(h(Comp), document.createElement('div'))
    // 验证获取到的实例不为 null,且 type 指向组件选项对象
    expect(inst).not.toBeNull()
    expect(inst.type).toBe(Comp)
  })

  // 测试 getCurrentInstance 在 setup 外部应返回 null
  it('getCurrentInstance should be null outside setup', () => {
    // setup 执行完毕后 currentInstance 会被清除,所以在外部调用应返回 null
    expect(getCurrentInstance()).toBeNull()
  })
})

本节小结

  1. createComponentInstance — 创建组件实例对象,包含 props/setupState/slots/emit/render 等全部字段
  2. setupComponent — 三步初始化:initProps → initSlots → setupStatefulComponent
  3. setupStatefulComponent — 创建 proxy、执行 setup()、处理返回值
  4. handleSetupResult — setup 返回对象则存入 setupState(proxyRefs 包装),返回函数则作为 render
  5. currentInstance — 全局变量机制,让 Composition API 在 setup 执行期间访问当前实例
  6. setupRenderEffect — 创建 ReactiveEffect,componentUpdateFn 区分 mount/update 两个分支
  7. 异步批量更新 — effect.scheduler + queueJob 实现多次数据变化只触发一次渲染

下一节实现 Props 与 Emit 机制。

用心学习,用代码说话 💻