React从2019年引入Hooks之后,函数组件与类组件的边界被彻底重写。但这并不意味着类组件已经退出历史舞台——错误边界、需要暴露实例方法的场景仍然依赖类组件。选择的核心不是语法偏好,而是组件在运行时是否需要持有一个可变实例,以及团队对逻辑内聚度的要求。下文从渲染机制、状态管理、性能特征三个角度展开,最后给出一套可落地的选型清单。

一、渲染机制与实例模型:this到底带来了什么
类组件的执行模型建立在实例之上。每次使用<MyClassComponent />时,React会调用new操作创建组件实例,实例上保存着this.state、this.props以及各个生命周期方法。渲染过程只是调用实例的render方法返回JSX,更新状态必须通过this.setState触发,React随后重新执行render。整个生命周期内实例保持不变,因此开发者可以在类组件中定义任意实例属性,用来存储不需要触发渲染的数据。
函数组件则完全不同。它没有实例,每次渲染就是一次普通的函数调用,传入新的props,返回新的React元素。状态通过useState等Hooks存储在React内部的Fiber节点上,而不是组件实例上。函数执行完毕后局部变量全部销毁,只有被Hooks捕获的值通过闭包保留下来。这种模型让函数组件更接近纯函数,天然避免了this绑定带来的歧义,但也意味着无法在组件外部通过ref调用实例方法。
下面的示例对比了同样的窗口尺寸监听逻辑在两种写法下的代码组织差异:
class WindowSize extends React.Component {
state = { width: window.innerWidth };
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
handleResize = () => {
this.setState({ width: window.innerWidth });
};
render() {
return <div>{this.state.width}px</div>;
}
}
function WindowSize() {
const [width, setWidth] = React.useState(window.innerWidth);
React.useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>{width}px</div>;
}
类组件把副作用分散在componentDidMount和componentWillUnmount两个生命周期中,函数组件则把注册和清理写在同一个useEffect回调里,逻辑更加内聚。
二、状态逻辑复用:从生命周期到自定义Hook
类组件时代,复用有状态逻辑只能依赖高阶组件(HOC)或render props。这两种模式都会引入额外的组件层级,容易造成“包装地狱”,并且不同HOC之间的props命名可能冲突,TypeScript类型推断也会变得复杂。例如一个简单的数据请求逻辑,使用HOC需要创建一个withRequest函数,返回一个类组件,在内部管理loading和data状态,再通过props传给被包裹组件。
函数组件借助自定义Hook解决了这一痛点。自定义Hook本质上是一个以use开头的普通函数,内部可以使用useState、useEffect等Hooks,并返回任意数据。每个调用自定义Hook的组件都会获得独立的state和effect,互不干扰。下面是一个useFetch Hook的完整实现:
function useFetch(url) {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(json => {
if (!cancelled) {
setData(json);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [url]);
return { data, loading };
}
function UserList() {
const { data, loading } = useFetch('/api/users');
if (loading) return <p>加载中</p>;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
可以看出,useFetch把请求状态、副作用和取消逻辑封装在一个函数里,组件内部只需一行调用即可获得data和loading。如果使用类组件实现同样功能,需要在componentDidMount发起请求、在componentDidUpdate处理url变化、在componentWillUnmount取消请求,逻辑被撕扯到多个生命周期中。
自定义Hook还可以自由组合。例如可以基于useFetch再封装出useUser、useArticle等业务Hook,或者将通用的防抖、节流逻辑抽离成useDebounce。这种分层复用在类组件中几乎无法优雅实现,因为生命周期方法无法像普通函数那样组合和调用。
三、性能特征与并发渲染下的差异
在性能优化方面,类组件通过继承React.PureComponent或手动实现shouldComponentUpdate来阻止不必要的渲染。但这种方式需要开发者明确知道哪些props或state发生了变化,容易遗漏嵌套对象的比较。函数组件则使用React.memo对props进行浅比较,再配合useMemo和useCallback缓存引用,可以将跳过渲染的粒度控制到单个值级别。
并发渲染(Concurrent Rendering)放大了两者的差异。React 18中,渲染过程可以被中断和恢复,类组件的生命周期方法(如componentDidMount、componentDidUpdate)可能被多次调用,因为它们依赖实例状态。函数组件虽然没有生命周期方法,但useEffect同样遵循“渲染后执行”的语义,在严格模式下会被双重调用来暴露副作用问题。区别在于,函数组件的依赖数组让React明确知道何时需要重新执行副作用,而类组件只能通过componentDidUpdate中的手动比较prevProps和this.props来实现类似效果。
下面是一个函数组件的性能优化示例,通过React.memo和useMemo避免子组件在父组件状态变化时重新渲染:
const ExpensiveChild = React.memo(function ExpensiveChild({ items }) {
return <ul>{items.map(item => <li key={item.id}>{item.text}</li>)}</ul>;
});
function Parent() {
const [count, setCount] = React.useState(0);
const items = React.useMemo(() => [{ id: 1, text: '静态数据' }], []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>点击 {count}</button>
<ExpensiveChild items={items} />
</div>
);
}
值得注意的是,类组件在需要实现错误边界时仍然不可替代。getDerivedStateFromError和componentDidCatch目前没有对应的Hook,因此任何需要捕获子组件渲染错误的场景都必须编写类组件。这也是类组件在未来一段时间内继续存在的核心理由之一。
四、选型决策框架与迁移策略
综合前面的分析,可以总结出一套实用的选型清单。以下情况应该使用类组件:实现错误边界、需要暴露命令式实例方法给父组件通过ref调用、依赖的第三方库只支持类组件、项目现有代码完全基于类组件且团队没有迁移计划。以下情况推荐使用函数组件:新建业务组件、需要复用有状态逻辑、需要精细控制副作用执行时机、团队已经启用Hooks规范并配置了eslint-plugin-react-hooks。
如果项目正在从类组件向函数组件迁移,切忌一次性重写所有组件。比较稳妥的策略是:先迁移纯展示组件,这类组件没有内部状态,重写成本低、风险小;再处理有状态组件,优先把组件拆分为容器组件和展示组件,容器组件用函数组件重写,展示组件保持不变。对于类组件中的生命周期逻辑,可以逐步抽取为自定义Hook,然后由新的函数组件调用。类组件本身无法直接使用Hook,但可以保留为错误边界,外层包裹函数组件。
最后需要强调的是,工具链对两类组件都有完整支持。React DevTools可以正常调试两种组件,TypeScript在函数组件中的类型推断通常更简单,因为不需要处理this的类型。团队层面的规范比个人偏好更重要:只要选型标准统一、代码评审严格执行,无论选择哪种组件风格,都能保证项目的可维护性。真正应该避免的是在同一个文件中混合多种模式,导致新人难以理解数据流和生命周期。
React组件设计模式函数组件类组件修改时间:2026-09-26 20:57:07