前言
想象一下,我们要建一栋房子:
- 有状态组件 = 精装修的套房,有独立的水电煤气、智能家居、报警系统
- 函数式组件 = 简易板房,只有最基本的结构
套房住着舒服,但建造成本高、维护复杂;板房简陋,但建造快、成本低。
在 Vue 应用中,我们每天都在做这样的选择:什么时候需要"套房",什么时候"板房"就够用了?
本文要解决的核心问题
- 两种组件到底有什么区别?
- 为什么 Vue 3 中函数式组件的地位变了?
- 在 5000 条数据的列表中,如何选择才能让页面不卡顿?
从一个真实的性能问题说起
问题重现
html
<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 | ❌ 太高 |
| 滚动帧率 | 15fps | 60fps | ❌ 卡顿 |
| 点击响应 | 200ms | < 50ms | ❌ 延迟 |
为什么会这样?
因为每个 ProductCard 都是一个有状态组件,它们:
- 有自己的组件实例(约 50-80KB 内存)
- 有自己的响应式系统(跟踪数据变化)
- 有自己的生命周期(onMounted、onUpdated 等)
- 有自己的事件监听器
- 5000 个组件 = 5000 × 60KB ≈ 300MB 内存!
两种组件的核心区别
有状态组件像什么?
有状态组件 类似于独立的"微应用":
typescript
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 }
}
函数式组件像什么?
函数式组件 类似于一个普通的函数:
typescript
// 函数式组件就是一个纯函数
function FunctionalComponent(props) {
// 没有实例、没有响应式、没有生命周期
// 只有:输入 props → 输出 VNode
return h('div', props.text)
}
维度对比
| 维度 | 有状态组件 | 函数式组件 |
|---|---|---|
| 实例 | 有独立组件实例 | 无实例,纯函数 |
| 响应式 | 完整的响应式系统 | 无响应式,只依赖传入的 props |
| 生命周期 | 完整的生命周期钩子 | 无生命周期 |
| this | 可访问 this | 无 this 上下文 |
| 性能开销 | 较高 | 极低 |
| 灵活性 | 高 | 低 |
| 创建速度 慢 | 快 |
Vue3 的重要变化
在 Vue3 中,有状态组件的性能已经大幅提升,使得函数式组件的性能优势不再那么显著:
typescript
// Vue2 时代
// 有状态组件: 100% 基准
// 函数式组件: 快 2-3 倍
// Vue3 时代
// 有状态组件: 性能提升 200%
// 函数式组件: 性能提升 300%
// 差距缩小到 20-30%
官方建议:除非有特殊需求(如大规模列表渲染、高频动态组件),否则优先使用有状态组件。
深入理解实现原理
有状态组件的内部机制
typescript
// 有状态组件的简化实现
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
}
}
函数式组件的内部机制
typescript
// 函数式组件的简化实现
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)
}
性能开销对比
typescript
// 性能测试代码
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 中函数式组件的正确用法
定义方式的变化
typescript
// 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
单文件组件中的变化
html
<!-- 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
typescript
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 支持
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[使用有状态组件
性能差异可忽略]
性能差异可忽略]
适用场景对照表
| 场景 | 推荐类型 | 理由 |
|---|---|---|
| 页面级组件 | 有状态 | 需要管理复杂状态和生命周期 |
| 基础 UI 组件(按钮、标签) | 均可 | 函数式性能略优,但差异小 |
| 长列表项(>500) | 函数式 | 减少实例化开销 60%+ |
| 高阶组件 | 函数式 | 无需状态,只需代理逻辑 |
| 动态渲染组件 | 函数式 | 轻量,频繁切换性能好 |
| 表单组件 | 有状态 | 需要 v-model 和内部状态 |
| 弹窗/抽屉 | 有状态 | 需要生命周期管理 |
| 图标组件 | 函数式 | 纯展示,实例化无意义 |
常见陷阱与注意事项
陷阱一:在函数式组件中使用响应式 API
typescript
// ❌ 错误:函数式组件中不能使用 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)
}
陷阱二:过度优化
typescript
// ❌ 过度:只有 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 类型丢失
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} />
陷阱四:生命周期需求
typescript
// ❌ 错误:函数式组件没有生命周期
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 中,大多数情况下有状态组件已经足够高效。函数式组件是工具箱里的精密工具,只在特定场景下才需要拿出来使用。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!