说说对 React 中类组件和函数组件的理解?有什么区别?
一、是什么
在 React 中,组件是构建 UI 的核心抽象。历史上,React 提供了两种主要的组件定义方式:
- 类组件(Class Component):使用 ES6 的
class 语法,通过继承 React.Component 创建
- 函数组件(Function Component):使用普通函数或箭头函数,接收
props 返回 JSX
在 React 16.8 之前,函数组件被称为"无状态组件",只能用来做纯展示。Hooks 的引入彻底改变了这一局面,使函数组件获得了完整的状态管理和副作用处理能力,并成为 React 官方推荐的组件形式。
二、语法差异
类组件
import React, { Component } from 'react';
interface GreetingProps {
name: string;
}
interface GreetingState {
count: number;
}
class Greeting extends Component<GreetingProps, GreetingState> {
state: GreetingState = { count: 0 };
handleClick = () => {
this.setState(prev => ({ count: prev.count + 1 }));
};
render() {
return (
<div>
<h1>你好,{this.props.name}</h1>
<p>点击次数:{this.state.count}</p>
<button onClick={this.handleClick}>点击</button>
</div>
);
}
}
函数组件
import { useState } from 'react';
interface GreetingProps {
name: string;
}
function Greeting({ name }: GreetingProps) {
const [count, setCount] = useState(0);
return (
<div>
<h1>你好,{name}</h1>
<p>点击次数:{count}</p>
<button onClick={() => setCount(c => c + 1)}>点击</button>
</div>
);
}
三、状态管理
类组件的状态管理
类组件通过 this.state 和 this.setState 管理状态。setState 在事件处理中是批量异步更新的,多次调用会合并。
class Form extends Component<{}, { name: string; email: string }> {
state = { name: '', email: '' };
handleChange = (field: string, value: string) => {
this.setState({ [field]: value } as any);
};
handleSubmit = () => {
// this.state 始终指向最新的状态对象
console.log(this.state);
};
render() {
return (
<form onSubmit={e => { e.preventDefault(); this.handleSubmit(); }}>
<input value={this.state.name} onChange={e => this.handleChange('name', e.target.value)} />
<input value={this.state.email} onChange={e => this.handleChange('email', e.target.value)} />
<button type="submit">提交</button>
</form>
);
}
}
函数组件的状态管理
函数组件通过 useState、useReducer 等 Hooks 管理状态。对于复杂状态逻辑,useReducer 提供了更清晰的管理方式。
import { useReducer } from 'react';
type Action =
| { type: 'SET_FIELD'; field: string; value: string }
| { type: 'RESET' };
interface FormState {
name: string;
email: string;
}
function formReducer(state: FormState, action: Action): FormState {
switch (action.type) {
case 'SET_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return { name: '', email: '' };
default:
return state;
}
}
function Form() {
const [state, dispatch] = useReducer(formReducer, { name: '', email: '' });
return (
<form onSubmit={e => { e.preventDefault(); console.log(state); }}>
<input
value={state.name}
onChange={e => dispatch({ type: 'SET_FIELD', field: 'name', value: e.target.value })}
/>
<input
value={state.email}
onChange={e => dispatch({ type: 'SET_FIELD', field: 'email', value: e.target.value })}
/>
<button type="submit">提交</button>
</form>
);
}
四、生命周期 vs Hooks
类组件的生命周期
类组件拥有明确的生命周期方法:
class DataFetcher extends Component<{ id: string }, { data: any; loading: boolean }> {
state = { data: null, loading: true };
componentDidMount() {
this.fetchData();
}
componentDidUpdate(prevProps: { id: string }) {
if (prevProps.id !== this.props.id) {
this.fetchData();
}
}
componentWillUnmount() {
// 清理工作,如取消请求
}
async fetchData() {
this.setState({ loading: true });
const res = await fetch(`/api/data/${this.props.id}`);
const data = await res.json();
this.setState({ data, loading: false });
}
render() {
if (this.state.loading) return <p>加载中...</p>;
return <pre>{JSON.stringify(this.state.data, null, 2)}</pre>;
}
}
函数组件的 Hooks
函数组件使用 useEffect 统一处理副作用,不再按生命周期阶段拆分逻辑:
import { useState, useEffect } from 'react';
function DataFetcher({ id }: { id: string }) {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(`/api/data/${id}`)
.then(res => res.json())
.then(result => {
if (!cancelled) {
setData(result);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [id]);
if (loading) return <p>加载中...</p>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
Hooks 的一个核心优势是相关逻辑聚合在一起,而不是按生命周期阶段分散。类组件中 componentDidMount 和 componentWillUnmount 里的配对逻辑可能隔得很远,Hooks 的清理函数就在 effect 内部。
五、this 绑定问题
类组件中 this 的指向是一个常见的坑:
class BuggyButton extends Component {
state = { count: 0 };
// ❌ 普通方法,this 指向可能丢失
handleClick() {
this.setState({ count: this.state.count + 1 });
}
// ✅ 箭头函数属性,this 自动绑定
handleClickSafe = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<>
{/* ❌ 运行时报错:Cannot read properties of undefined */}
<button onClick={this.handleClick}>Buggy</button>
{/* ✅ 正常工作 */}
<button onClick={this.handleClickSafe}>Safe</button>
{/* ✅ 也可以在 render 中绑定,但每次渲染创建新函数 */}
<button onClick={this.handleClick.bind(this)}>Bind</button>
</>
);
}
}
函数组件完全没有 this 的概念,根本不存在绑定问题:
function SafeButton() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(c => c + 1);
};
return <button onClick={handleClick}>{count}</button>;
}
六、Capture 语义——函数组件捕获渲染时的值
这是函数组件和类组件之间一个非常重要的行为差异。函数组件每次渲染都会捕获当次渲染的 props 和 state 值,而类组件的 this.props 和 this.state 始终指向最新值。
// 类组件:this.props 指向最新值
class ClassProfilePage extends Component<{ user: string }> {
showMessage = () => {
alert('当前用户:' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>查看用户</button>;
}
}
如果在点击后 3 秒内切换了用户,类组件会弹出新用户的名字,因为 this.props 是可变的引用。
// 函数组件:捕获渲染时的值
function FunctionProfilePage({ user }: { user: string }) {
const showMessage = () => {
alert('当前用户:' + user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>查看用户</button>;
}
函数组件会弹出点击时的用户名,因为闭包捕获了那次渲染的 user 值。这种行为通常更符合用户预期,也更容易推理。
如果确实需要在函数组件中读取最新值,可以使用 useRef:
function ProfilePage({ user }: { user: string }) {
const latestUser = useRef(user);
latestUser.current = user;
const handleClick = () => {
setTimeout(() => {
alert('最新用户:' + latestUser.current);
}, 3000);
};
return <button onClick={handleClick}>查看用户</button>;
}
七、性能差异
从性能角度看,两者的差异主要体现在:
- 包体积:函数组件编译后代码更少,没有类继承的额外开销
- 实例化:类组件每次创建都需要
new 一个实例,函数组件只是一次函数调用
- 优化潜力:React Compiler(React 19+)主要针对函数组件进行自动记忆化优化
// React Compiler 可以自动优化函数组件
// 开发者无需手动添加 useMemo / useCallback
function TodoList({ todos, filter }: { todos: Todo[]; filter: string }) {
const filtered = todos.filter(t => t.text.includes(filter));
return (
<ul>
{filtered.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
八、逻辑复用
类组件时代的复用方式
类组件时代主要通过高阶组件(HOC)和 Render Props 实现逻辑复用,两者都存在"嵌套地狱"问题:
// HOC 嵌套
export default withRouter(withTheme(withAuth(MyComponent)));
// Render Props 嵌套
<ThemeContext.Consumer>
{theme => (
<AuthContext.Consumer>
{auth => (
<MouseTracker>
{mouse => <MyComponent theme={theme} auth={auth} mouse={mouse} />}
</MouseTracker>
)}
</AuthContext.Consumer>
)}
</ThemeContext.Consumer>
函数组件的自定义 Hooks
函数组件通过自定义 Hooks 实现逻辑复用,代码扁平且直观:
function useAuth() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => { /* 认证逻辑 */ }, []);
return { user, isLoggedIn: !!user };
}
function useTheme() {
return useContext(ThemeContext);
}
function useMousePosition() {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return pos;
}
function MyComponent() {
const { user } = useAuth();
const theme = useTheme();
const mouse = useMousePosition();
// 扁平结构,逻辑清晰
return <div>...</div>;
}
九、对比总结
十、行业趋势与建议
React 官方文档自 2023 年起已全面使用函数组件作为示例。社区主流库(React Router、React Query、Zustand 等)也全部基于 Hooks API 设计。
实践建议:
- 新项目一律使用函数组件 + Hooks
- 老项目可以逐步将类组件迁移为函数组件
- Error Boundary 是目前唯一必须使用类组件的场景
- 面试中需要理解两者区别,但日常开发以函数组件为主
// 2024+ 年的标准 React 组件写法
import { useState, useEffect, useCallback } from 'react';
interface Props {
userId: string;
}
function UserProfile({ userId }: Props) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
let active = true;
fetchUser(userId).then(data => {
if (active) setUser(data);
});
return () => { active = false; };
}, [userId]);
if (!user) return <Skeleton />;
return (
<section>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</section>
);
}
十一、总结
类组件和函数组件在功能上已经几乎等价,但函数组件在语法简洁性、逻辑复用能力、值捕获语义和未来优化潜力上都具有明显优势。理解两者的差异——特别是 Capture 语义和 this 绑定问题——是 React 开发者的必备知识。在新项目中应当毫不犹豫地选择函数组件 + Hooks 方案。