函数式组件 vs 有状态组件:何时使用更高效?

21 阅读6分钟

前言

想象一下,我们要建一栋房子:

  • 有状态组件 = 精装修的套房,有独立的水电煤气、智能家居、报警系统
  • 函数式组件 = 简易板房,只有最基本的结构

套房住着舒服,但建造成本高、维护复杂;板房简陋,但建造快、成本低。

在 Vue 应用中,我们每天都在做这样的选择:什么时候需要"套房",什么时候"板房"就够用了?

本文要解决的核心问题

  • 两种组件到底有什么区别?
  • 为什么 Vue 3 中函数式组件的地位变了?
  • 在 5000 条数据的列表中,如何选择才能让页面不卡顿?

从一个真实的性能问题说起

问题重现

<template>
  <div>
    <div v-for="item in largeList" :key="item.id">
      <ComplexItem :data="item" @click="handleClick" />
    </div>
  </div>
</template>

<script setup>
// 一个包含 5000 条数据的列表
const largeList = ref(Array.from({ length: 5000 }, (_, i) => ({
  id: i,
  title: `Item ${i}`,
  content: `Content for item ${i}`
})))
</script>

问题表现

打开这个页面时发生了什么?

指标数值正常值问题
页面加载时间3.2秒< 1秒❌ 太慢
内存占用280MB< 100MB❌ 太高
滚动帧率15fps60fps❌ 卡顿
点击响应200ms< 50ms❌ 延迟

为什么会这样?

因为每个 ProductCard 都是一个有状态组件,它们:

  • 有自己的组件实例(约 50-80KB 内存)
  • 有自己的响应式系统(跟踪数据变化)
  • 有自己的生命周期(onMounted、onUpdated 等)
  • 有自己的事件监听器
  • 5000 个组件 = 5000 × 60KB ≈ 300MB 内存!

两种组件的核心区别

有状态组件像什么?

有状态组件 类似于独立的"微应用":

const componentInstance = {
  // 唯一标识
  uid: 12345,
  
  // 响应式数据
  data: reactive({ ... }),
  
  // 传入的属性
  props: shallowReactive({ ... }),
  
  // 生命周期钩子
  onMounted: [fn1, fn2],
  onUpdated: [fn3],
  onUnmounted: [fn4],
  
  // 侦听器
  watchers: [watcher1, watcher2],
  
  // 计算属性
  computed: { double: fn },
  
  // 事件监听器
  eventListeners: { click: [fn5] },
  
  // 模板引用
  refs: { input: domElement }
}

函数式组件像什么?

函数式组件 类似于一个普通的函数:

// 函数式组件就是一个纯函数
function FunctionalComponent(props) {
  // 没有实例、没有响应式、没有生命周期
  // 只有:输入 props → 输出 VNode
  return h('div', props.text)
}

维度对比

维度有状态组件函数式组件
实例有独立组件实例无实例,纯函数
响应式完整的响应式系统无响应式,只依赖传入的 props
生命周期完整的生命周期钩子无生命周期
this可访问 this无 this 上下文
性能开销较高极低
灵活性
创建速度

Vue3 的重要变化

在 Vue3 中,有状态组件的性能已经大幅提升,使得函数式组件的性能优势不再那么显著:

// Vue2 时代
// 有状态组件: 100% 基准
// 函数式组件: 快 2-3 倍

// Vue3 时代
// 有状态组件: 性能提升 200%
// 函数式组件: 性能提升 300%
// 差距缩小到 20-30%

官方建议:除非有特殊需求(如大规模列表渲染、高频动态组件),否则优先使用有状态组件。

深入理解实现原理

有状态组件的内部机制

// 有状态组件的简化实现
class VueComponent {
  // 1. 创建组件实例
  instance = {
    uid: uniqueId(),
    data: reactive({}),      // 响应式数据
    props: shallowReactive({}), // 传入的属性
    ctx: {},                  // 上下文
    proxy: null,              // 代理对象
    render: null,             // 渲染函数
    lifecycle: {              // 生命周期队列
      beforeCreate: [],
      created: [],
      beforeMount: [],
      mounted: [],
      beforeUpdate: [],
      updated: [],
      beforeUnmount: [],
      unmounted: []
    },
    watchers: new Set(),      // 侦听器
    computed: new Map(),      // 计算属性
    refs: {}                  // 模板引用
  }
  
  // 2. 初始化流程
  init() {
    callHook('beforeCreate')
    initProps()   // 初始化 props(响应式)
    initData()    // 初始化 data(响应式)
    initComputed() // 初始化计算属性
    initMethods() // 初始化方法
    initWatch()   // 初始化侦听器
    callHook('created')
    initRender()
  }
  
  // 3. 更新流程
  update() {
    callHook('beforeUpdate')
    
    // 重新计算依赖
    this.render()
    
    // 虚拟 DOM diff
    patch(prevVNode, newVNode)
    
    callHook('updated')
  }
  
  // 4. 销毁流程
  destroy() {
    callHook('beforeUnmount')
    
    // 清理所有侦听器
    this.watchers.forEach(watcher => watcher.stop())
    
    // 移除事件监听器
    removeEventListeners()
    
    callHook('unmounted')
    
    // 释放引用
    this.instance = null
  }
}

函数式组件的内部机制

// 函数式组件的简化实现
function FunctionalComponent(props, { slots, attrs, emit }) {
  // 1. 没有实例化过程
  // 2. 没有响应式系统
  // 3. 没有生命周期
  // 4. 直接返回 VNode
  return h('div', props.msg)
}

// Vue 内部处理
function mountFunctionalComponent(component, props, children) {
  // 直接调用函数,不创建实例
  const vnode = component(props, { 
    slots: children,
    attrs: extractAttrs(props),
    emit: createEmitFunction()
  })
  
  // 直接挂载返回的 VNode
  mount(vnode)
}

性能开销对比

// 性能测试代码
async function benchmark(count = 1000) {
  console.time('stateful')
  for (let i = 0; i < count; i++) {
    const vnode = h(StatefulComponent, { index: i })
    render(vnode, document.createElement('div'))
  }
  await nextTick()
  console.timeEnd('stateful') // Vue 2: ~120ms, Vue 3: ~35ms
  
  console.time('functional')
  for (let i = 0; i < count; i++) {
    const vnode = h(FunctionalComponent, { index: i })
    render(vnode, document.createElement('div'))
  }
  await nextTick()
  console.timeEnd('functional') // Vue 2: ~45ms, Vue 3: ~28ms
}

Vue 3 中函数式组件的正确用法

定义方式的变化

// Vue 2 语法(已废弃)
export default {
  functional: true,
  props: ['level'],
  render(h, { props, data, children }) {
    return h(`h${props.level}`, data, children)
  }
}

// Vue 3 语法(新)
import { h } from 'vue'

const DynamicHeading = (props, context) => {
  return h(`h${props.level}`, context.attrs, context.slots)
}

DynamicHeading.props = ['level']

export default DynamicHeading

单文件组件中的变化

<!-- Vue 2 语法:使用 functional 属性 -->
<template functional>
  <component
    :is="`h${props.level}`"
    v-bind="attrs"
    v-on="listeners"
  />
</template>

<script>
export default {
  props: ['level']
}
</script>

<!-- Vue 3 语法:移除 functional,改用普通组件 -->
<template>
  <component
    :is="`h${$props.level}`"
    v-bind="$attrs"
  />
</template>

<script>
export default {
  props: ['level']
}
</script>

参数详解:props 和 context

import { h } from 'vue'

const MyComponent = (props, context) => {
  // props: 传入的属性(普通对象,不是响应式的)
  console.log(props.msg)
  
  // context: 包含三个重要属性
  const { attrs, slots, emit } = context
  
  // attrs: 非 props 的属性(class, style, id 等)
  console.log(attrs.class)
  
  // slots: 插槽内容
  const defaultSlot = slots.default?.() // 渲染插槽
  
  // emit: 触发事件
  const handleClick = () => emit('click', 'data')
  
  return h('div', { onClick: handleClick }, [
    props.msg,
    defaultSlot
  ])
}

MyComponent.props = {
  msg: String
}

TypeScript 支持

import { h, FunctionalComponent } from 'vue'

interface Props {
  level: number
  title: string
}

interface Context {
  attrs: Record<string, any>
  slots: any
  emit: (event: string, ...args: any[]) => void
}

const DynamicHeading: FunctionalComponent<Props> = (
  props: Props,
  { attrs, slots }: Context
) => {
  return h(
    `h${props.level}`,
    { ...attrs, class: 'heading' },
    [props.title, slots.default?.()]
  )
}

DynamicHeading.props = {
  level: { type: Number, required: true },
  title: { type: String, default: '' }
}

选择决策指南

决策树

graph TD
    Start[开始选择组件类型] --> Q1{组件需要内部状态吗?}
    
    Q1 -->|是| Stateful[使用有状态组件]
    Q1 -->|否| Q2{需要生命周期钩子吗?}
    
    Q2 -->|是| Stateful
    Q2 -->|否| Q3{需要响应式数据吗?}
    
    Q3 -->|是| Stateful
    Q3 -->|否| Q4{实例数量超过500吗?}
    
    Q4 -->|是| Functional[考虑函数式组件]
    Q4 -->|否| Stateful[使用有状态组件<br>性能差异可忽略]

适用场景对照表

场景推荐类型理由
页面级组件有状态需要管理复杂状态和生命周期
基础 UI 组件(按钮、标签)均可函数式性能略优,但差异小
长列表项(>500)函数式减少实例化开销 60%+
高阶组件函数式无需状态,只需代理逻辑
动态渲染组件函数式轻量,频繁切换性能好
表单组件有状态需要 v-model 和内部状态
弹窗/抽屉有状态需要生命周期管理
图标组件函数式纯展示,实例化无意义

常见陷阱与注意事项

陷阱一:在函数式组件中使用响应式 API

// ❌ 错误:函数式组件中不能使用 ref/reactive
const BadComponent: FunctionalComponent = () => {
  const count = ref(0) // 不会生效!ref 只在组件实例中有效
  const state = reactive({}) // 也不会生效
  
  return h('div', count.value) // 永远显示 0
}

// ✅ 正确:所有数据通过 props 传入
const GoodComponent: FunctionalComponent<{ count: number }> = (props) => {
  return h('div', props.count)
}

陷阱二:过度优化

// ❌ 过度:只有 10 个列表项也使用函数式
<template>
  <div v-for="item in smallList" :key="item.id">
    <FunctionalItem :data="item" />
  </div>
</template>

// ✅ 适度:小列表使用有状态组件,代码更清晰
<template>
  <div v-for="item in smallList" :key="item.id">
    <NormalItem :data="item" />
  </div>
</template>

// 性能测试证明
async function testOverOptimization() {
  const smallList = Array.from({ length: 10 }, (_, i) => ({ id: i }))
  
  console.time('functional')
  // 渲染函数式组件...
  console.timeEnd('functional') // 1.2ms
  
  console.time('stateful')
  // 渲染有状态组件...
  console.timeEnd('stateful') // 1.3ms
  
  // 差异只有 0.1ms,完全可忽略
}

陷阱三:TypeScript 类型丢失

// ❌ 类型不安全
const BadComponent = (props: any) => h('div', props.msg)

// 使用时没有类型提示
<BadComponent msg="hello" /> // msg 可能拼错为 mesg

// ✅ 使用 FunctionalComponent 接口
import { FunctionalComponent } from 'vue'

interface Props {
  msg: string
  count?: number
}

const GoodComponent: FunctionalComponent<Props> = (props) => {
  return h('div', `${props.msg} - ${props.count || 0}`)
}

// 使用时获得完整类型提示
<GoodComponent msg="hello" count={5} />

陷阱四:生命周期需求

// ❌ 错误:函数式组件没有生命周期
const Component: FunctionalComponent = () => {
  onMounted(() => {})  // 不会执行!
  return h('div', '内容')
}

// ✅ 正确:如果生命周期是必须的,使用有状态组件
<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  console.log('组件已挂载')
})
</script>

最佳实践清单

选择决策清单

  • 组件是否需要内部状态?(ref、reactive)
  • 组件是否需要生命周期钩子?(onMounted等)
  • 组件是否需要响应式数据?(watch、computed)
  • 组件实例数量是否超过500?
  • 组件是否纯展示,只依赖props?
  • 组件是否频繁切换显示/隐藏?

优化检查清单

  • 长列表是否使用了函数式组件?
  • 函数式组件是否配合 v-memo 使用?
  • 是否避免了在函数式组件中使用响应式API?
  • 小列表是否过度优化?
  • 是否有性能基准数据支持优化决策?

结语

最好的优化就是不需要优化。在 Vue3 中,大多数情况下有状态组件已经足够高效。函数式组件是工具箱里的精密工具,只在特定场景下才需要拿出来使用。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!