2025-11-23 16:04:26
import { createApp, h } from '../dist/vue.esm.js'

function App() {
  return h('div', 'hello world')
}

const app = createApp(App)
app.mount('#app')

这是函数式组件最简单的使用方式。

没有 setup,也没有组件选项对象,本身就是一个渲染函数:执行组件函数后,直接返回需要渲染的 vnode。

渲染流程

先重新梳理一下整体链路:

  1. 通过 createApp 创建
    1. 通过 createRenderer 创建渲染器:packages/runtime-core/src/renderer.ts
    2. 通过 createApp 创建应用对象:packages/runtime-core/src/apiCreateApp.ts
    3. 通过 h 函数创建根组件对应的 vnode
    4. 执行 render 进入渲染流程
  2. 通过 mount 挂载到页面

对于普通组件来说,type 通常是一个对象;对于函数式组件来说,type 就是函数本身。

所以核心要处理三件事:

  • 创建 vnode 时识别函数式组件
  • 渲染组件根节点时直接执行函数
  • 调用函数时传入 props、attrs、slots 和 emit

实现

1. 标记函数式组件

export function createVNode(type, props?, children = null) {
  // ...
  else if (isFunction(type)) {
    shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT
  }

  const vnode = {
    // ...
  }

  normalizeChildren(vnode, children)

  return vnode
}

createVNode 会根据 type 判断 vnode 类型。

当 type 是函数时,将它标记为 ShapeFlags.FUNCTIONAL_COMPONENT。

后续 patch 处理组件时,仍然会走组件的挂载流程,只是在渲染根节点时需要区分有状态组件和函数式组件。

2. 渲染函数式组件

export function renderComponentRoot(instance) {
  const { vnode } = instance

  // 有状态组件
  if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    setCurrentRenderingInstance(instance)
    const subTree = instance.render.call(instance.proxy)
    unsetCurrentRenderingInstance()
    return subTree
  }
  else {
    // 函数式组件
    return vnode.type()
  }
}

有状态组件的渲染入口是 instance.render。

函数式组件没有 render 选项,也不需要通过代理对象访问状态,所以可以直接执行 vnode.type()。

这里的 vnode.type 就是组件函数本身:

function App() {
  return h('div', 'hello world')
}

执行后的返回值就是组件的 subTree,后续继续交给 patch 处理。

3. 支持 props attrs emits

函数式组件也需要接收外部传入的数据。

不过它对 props 有一个特殊规则:

  • 没有声明 props 时,所有传入属性都会作为 props
  • 声明了 props 时,根据声明结果区分 props 和 attrs
function App(props, ctx) {
  return h('div', props.count)
}

App.props = ['count']

const app = createApp(App, {
  count: 1,
  msg: 'hello world'
})

在这个例子中:

  • count 会进入 props
  • msg 会进入 attrs

props 的设置逻辑在 packages/runtime-core/src/componentProps.ts -> setFullProps 中处理:

function setFullProps(instance, rawProps, props, attrs) {
  const { propsOptions, vnode } = instance

  const isFunctionalComponent
    = vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT

  const hasProps = Object.keys(propsOptions).length > 0

  if (rawProps) {
    for (const key in rawProps) {
      const value = rawProps[key]

      if (hasOwn(propsOptions, key) || (isFunctionalComponent && !hasProps)) {
        props[key] = value
      }
      else {
        attrs[key] = value
      }
    }
  }
}

判断条件里多了函数式组件的处理:

isFunctionalComponent && !hasProps

这表示:如果当前是函数式组件,并且组件没有声明 props,那么传入的属性都放到 props 上。

最后,在渲染函数式组件时,把这些数据作为参数传进去:

export function renderComponentRoot(instance) {
  const { vnode } = instance

  // 有状态组件
  if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    // ...
  }
  else {
    // 函数式组件
    return vnode.type(instance.props, {
      get attrs() {
        return instance.attrs
      },
      slots: instance.slots,
      emit: instance.emit
    })
  }
}

这样函数式组件就可以通过两个参数拿到数据:

function App(props, ctx) {
  return h('div', props.count)
}
  • 第一个参数是 props
  • 第二个参数是上下文对象,里面包含 attrs、slots 和 emit

优势

函数式组件没有状态管理、生命周期、实例属性和响应式数据的处理。

因此,它更适合用在无状态的 UI 组件,或者高频渲染但逻辑很轻的组件中。

但:

  • 函数式组件没有 this 上下文
  • props 的处理方式和普通组件也有区别