这是一个非常核心的面试题。从表面上看,这只是两种编写组件的方式,但它们的区别远不止语法糖那么简单。下面我将从表象区别、本质区别和设计哲学三个层面来深入剖析。
一、表象区别(Syntax & Basic Usage)
| 特性 | Class 组件 | Function 组件 |
|---|---|---|
| 语法 | ES6 Class,继承 React.Component |
JavaScript 函数 |
| 状态 | 使用 this.state 和 this.setState() |
使用 useState Hook |
| 生命周期 | 使用 componentDidMount, componentDidUpdate 等 |
使用 useEffect Hook |
this 关键字 |
需要处理 this 绑定问题 |
没有 this |
| 性能 | 较大,因为类是完整的实例 | 较轻量 |
代码示例对比:
// Class 组件
class ClassComponent extends React.Component {constructor(props) {super(props);this.state = { count: 0 };// 需要绑定 thisthis.handleClick = this.handleClick.bind(this);}handleClick() {this.setState({ count: this.state.count + 1 });}componentDidMount() {console.log('Component mounted');}render() {return <button onClick={this.handleClick}>Count: {this.state.count}</button>;}
}// Function 组件
function FunctionComponent() {const [count, setCount] = useState(0);useEffect(() => {console.log('Component mounted');}, []);const handleClick = () => {setCount(count + 1);};return <button onClick={handleClick}>Count: {count}</button>;
}
二、本质区别(Fundamental Differences)
这才是理解两者区别的核心。
1. 心智模型与代码组织
-
Class 组件:面向生命周期 & 关注点分离不佳
- 你的逻辑是围绕着“时间点”组织的。你需要思考:“在组件挂载后(
componentDidMount)我该做什么?在更新后(componentDidUpdate)我该做什么?在卸载前(componentWillUnmount)我该做什么?” - 问题:一个功能相关的代码(比如数据订阅和取消订阅)被拆分到不同的生命周期方法中,而多个不相关的代码却可能挤在同一个方法里。
class Example extends Component {componentDidMount() {// 不相关的逻辑Adocument.title = `You clicked ${this.state.count} times`;// 不相关的逻辑BChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);}componentDidUpdate(prevProps) {// 逻辑A的更新部分if (prevProps.count !== this.props.count) {document.title = `You clicked ${this.state.count} times`;}// 逻辑B的更新部分if (prevProps.friend.id !== this.props.friend.id) {ChatAPI.unsubscribeFromFriendStatus(prevProps.friend.id, this.handleStatusChange);ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);}}componentWillUnmount() {// 逻辑B的清理部分ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);} } - 你的逻辑是围绕着“时间点”组织的。你需要思考:“在组件挂载后(
-
Function 组件:面向状态 & 关注点分离更佳
- 你的逻辑是围绕着“状态”和“副作用”组织的。你需要思考:“这个组件需要根据哪些状态,产生哪些副作用?”
- 优势:使用
useEffect,你可以将相关副作用的设置和清理代码放在一起,实现更好的关注点分离。
function Example() {const [count, setCount] = useState(0);// 逻辑A:与 document.title 相关的副作用useEffect(() => {document.title = `You clicked ${count} times`;}, [count]); // 依赖 countconst [friendId, setFriendId] = useState(null);// 逻辑B:与好友状态订阅相关的副作用useEffect(() => {if (friendId) {ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);// 清理函数与设置函数放在一起!return () => {ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange);};}}, [friendId]); // 依赖 friendId }
2. 状态与闭包
-
Class 组件:总是获取最新的状态
this.state和this.props都是可变的,并且总是指向最新的值。在某些异步操作中,这可能会导致出乎意料的结果。
handleClick() {setTimeout(() => {// 3秒后打印的是最新的 count,而不是点击时的 countconsole.log(this.state.count); }, 3000); } -
Function 组件:捕获了本次渲染的值
- 每一次渲染都是一个独立的“快照”,拥有它自己的 props、state 和事件处理函数。这就是闭包的本质。
useState的setter函数是稳定的,但 state 和 props 是捕获在定义它们的那次渲染中的。
function handleClick() {const currentCount = count; // 捕获了本次渲染的 countsetTimeout(() => {// 3秒后打印的仍然是点击时的 countconsole.log(currentCount); }, 3000); }- 如果需要读取最新的值,可以使用
useRef。
3. 逻辑复用
- Class 组件:主要通过 HOC(高阶组件) 和 Render Props 实现。容易导致“包装地狱”(Wrapper Hell),且代码结构复杂。
- Function 组件:通过 自定义 Hook 实现。它就像一个普通的函数,可以自由地组合状态和副作用逻辑,没有额外的组件层,代码更清晰。
三、设计哲学与未来
- Class 组件:是 React 早期拥抱 ES6 Class 的产物,它更命令式,强调生命周期。
- Function 组件 + Hooks:是 React 的未来,它更声明式。它让开发者更关注于“数据流”和“副作用”,而不是生命周期的执行时机。它更好地体现了 React 的函数式编程思想。
总结
| 维度 | Class 组件 | Function 组件 |
|---|---|---|
| 语法 | Class | Function |
| 状态/生命周期 | this.state, 生命周期方法 |
Hooks (useState, useEffect) |
| 心智模型 | 面向生命周期 | 面向状态与副作用 |
| 代码组织 | 关注点分离不佳 | 关注点分离更佳 |
| 状态捕获 | 总是最新值 | 捕获渲染时的值(闭包) |
| 逻辑复用 | HOC, Render Props | 自定义 Hook |
| 学习曲线 | 需要理解 this 和生命周期 |
需要理解闭包和 Hooks 规则 |
| 未来趋势 | legacy | 主流和未来 |
结论:Function 组件 + Hooks 不仅是语法上的简化,更是一次心智模型的升级。它解决了 Class 组件在逻辑复用、代码组织和理解上的诸多痛点,代表了 React 未来的发展方向。对于新项目,强烈建议使用 Function 组件。