React 面试题:函数式组件 vs 类组件,捕获渲染时状态、类组件生命周期 vs. 函数组件 Hooks
文章目录
React 面试题:函数式组件 vs 类组件
在 React 面试中,这属于必考题。它不仅考察你对 API 的熟悉程度,更考察你对 React 渲染机制和心智模型的理解。
1. 本质区别:心智模型的差异
这是最核心的回答点:类组件是基于面向对象(OOP)的,而函数式组件是基于函数式编程(FP)的。
A. 捕获渲染值(Capture Value)—— 最重要的技术区别
- 函数组件:具有捕获渲染时状态的特性。由于闭包机制,函数组件在渲染时,
props和state是快照。即使在异步操作(如setTimeout)中,它拿到的也是触发渲染那一刻的值。 - 类组件:通过
this.props或this.state获取数据。由于this是可变的(mutable),如果异步任务执行完成时组件已经重新渲染,this会指向最新的实例引用,从而可能导致逻辑不一致。
B. 状态管理与生命周期
- 类组件:通过继承
React.Component,使用state对象和一系列生命周期钩子(如componentDidMount,shouldComponentUpdate)。 - 函数组件:最初是“无状态”的。但在 Hooks(React 16.8+)引入后,通过
useState、useEffect等实现了状态管理和副作用处理。
2. 为什么更推荐使用函数式组件?
React 官方和社区目前全面向函数式组件倾斜,主要原因如下:
- 逻辑复用更高效(Hooks vs HOC/Render Props):
在类组件中,复用逻辑通常需要高阶组件 (HOC) 或 Render Props,容易导致“嵌套地狱”。Hooks 允许我们将逻辑提取到独立的自定义 Hooks 中,代码更加扁平、易于测试。 - 代码更简洁、更易读:
类组件充满了样板代码(如constructor、this.bind)。函数组件代码量更少,且useEffect允许我们将相关逻辑(如订阅和取消订阅)聚合在一起,而不是分散在不同的生命周期钩子中。 - 对编译器更友好(性能潜力):
函数组件本质是纯 JS 函数,更容易被工具(如 Babel, Terser)进行静态分析、代码压缩和 Tree-shaking。而类组件的方法名通常难以被安全地混淆。 - 符合 U I = f ( s t a t e ) UI = f(state) UI=f(state) 的本质:
函数组件更加纯粹地体现了“输入 -> 输出”的映射关系,每一轮渲染都是独立的函数调用。
3. 面试总结话术
“类组件和函数组件最本质的区别在于:类组件通过
this维护实例,而函数组件通过闭包捕获渲染快照。官方更推荐函数组件,主要是因为 Hooks 的出现彻底解决了逻辑复用的难题,避免了高阶组件带来的嵌套问题。同时,函数组件的代码更简洁、心智负担更轻,也更符合 React 声明式、数据流驱动的设计初衷。此外,从长远来看,函数式组件在性能优化和打包体积上具有更大的优势。”
💡 核心知识点速览
| 特性 | 类组件 (Class) | 函数组件 (Function) |
|---|---|---|
| 编程范式 | 面向对象 (OOP) | 函数式编程 (FP) |
| 内存占用 | 需创建类实例,开销稍大 | 普通函数调用,开销较小 |
| 数据读取 | this.props (易受引用变动影响) |
闭包快照 (捕获渲染值) |
| 逻辑复用 | HOC, Render Props (易嵌套) | Hooks (扁平化) |
| 生命周期 | 显式的生命周期钩子 | useEffect 模拟生命周期 |
React 核心特性:捕获渲染时状态(Capture Value)
“捕获渲染时状态”是 React 函数式组件最迷人也最容易让人困惑的特性。简单来说:函数组件的每一次渲染,都有它自己独立的 Props、State 和事件处理函数。
1. 闭包体现在哪儿?
在 React 中,组件函数每次执行(Render)都会创建一个新的作用域。
当你在组件内部定义一个函数(如点击回调)时,这个函数通过 JavaScript 闭包 锁定了当前那次渲染中的变量。即使这个回调函数在几秒钟之后才执行(比如 setTimeout),它访问的依然是“那个特定时刻”的变量值。
2. 代码对比:函数组件 vs 类组件
通过下面这个经典的“社交关注”例子,你可以一眼看出区别:
A. 函数组件:行为一致(捕获状态)
function ProfilePage(props) {
const showMessage = () => {
// 闭包锁定了【点击那一刻】的 props.user
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
- 操作流程:初始用户是 A,点击 Follow → \rightarrow → 3 秒内,将用户切换到 B。
- 结果:3 秒后弹出 “Followed A”。
- 原因:showMessage 闭包捕获了点击瞬间的 props 快照。
B. 类组件:行为不一致(引用丢失)
class ProfilePage extends React.Component {
showMessage = () => {
// this.props 是可变的,总是指向最新的实例引用
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
- 操作流程:初始用户是 A,点击 Follow → \rightarrow → 3 秒内,将用户切换到 B。
- 结果:3 秒后弹出 “Followed B”。
- 原因:this.props 随着组件更新变掉了,showMessage 读到了最新的 this 指向,导致数据不一致。
React 面试题:类组件生命周期 vs. 函数组件 Hooks
在 React 的演进过程中,生命周期管理从“基于类的显式钩子”演变为“基于函数的副作用同步”。以下是针对这一部分的详细补充和深度对比。
1. 类组件的生命周期 (Class Lifecycle)
类组件的生命周期就像一个人的成长过程:挂载(出生)、更新(成长)、卸载(死亡)。
A. 挂载阶段 (Mounting)
这是组件实例被创建并插入 DOM 的过程:
constructor():初始化state,绑定事件处理器。static getDerivedStateFromProps():根据props的变化来更新state(极少使用)。render():核心方法。返回 JSX,定义 UI 结构。componentDidMount():非常常用。组件挂载到 DOM 后立即调用。适合:发送网络请求、订阅事件、操作 DOM。
B. 更新阶段 (Updating)
当组件的 props 或 state 发生变化时:
shouldComponentUpdate():性能优化关键。返回true或false,决定 React 是否重新渲染该组件。render():重新生成虚拟 DOM。getSnapshotBeforeUpdate():在最近一次渲染提交到 DOM 之前调用,可以捕捉 DOM 信息(如滚动位置)。componentDidUpdate():常用。更新完成后调用。适合:根据数据变化执行 DOM 操作或发送新的请求。
C. 卸载阶段 (Unmounting)
componentWillUnmount():重要。组件销毁前调用。适合:清理定时器、取消网络请求、解绑事件监听(防止内存泄漏)。
2. 函数式组件的实现 (Hooks)
函数组件使用 useEffect 这一个 Hook 来模拟上述所有的生命周期行为。其核心哲学是**“同步”**(根据状态同步副作用),而不是单一的“生命周期”。
A. 模拟 componentDidMount
只需将 useEffect 的依赖项数组设置为空 []:
useEffect(() => {
console.log('组件挂载了(只执行一次)');
// 发送 API 请求
}, []);
B. 模拟 componentDidUpdate
不传递依赖项数组(每次渲染都执行),或传递特定的依赖项(仅当该变量改变时执行):
useEffect(() => {
console.log('数据改变了,执行更新逻辑');
}, [count]); // 仅在 count 变化时运行
C. 模拟 componentWillUnmount
在 useEffect 中返回一个清理函数 (Cleanup function):
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
// 返回的函数会在组件卸载前执行
return () => {
clearInterval(timer);
console.log('组件卸载,定时器已清理');
};
}, []);
3. 深度对比表
| 概念 | 类组件 (Class) | 函数组件 (Hooks) |
|---|---|---|
| 初始化状态 | constructor 中使用 this.state |
useState |
| 挂载后 | componentDidMount |
useEffect(fn, []) |
| 更新后 | componentDidUpdate |
useEffect(fn, [deps]) |
| 卸载前 | componentWillUnmount |
useEffect 的 return 清理函数 |
| 性能优化 | shouldComponentUpdate / PureComponent |
React.memo / useMemo / useCallback |
4. 为什么函数式的实现更高级?
- 关注点分离 (Separation of Concerns):
- 类组件:你可能需要在
componentDidMount里同时开启定时器和获取数据,这两个无关的逻辑被迫挤在一起。 - 函数组件:你可以写多个
useEffect,每个useEffect只处理一个独立的逻辑块,让代码更具模块化。
- 类组件:你可能需要在
- 代码复用:
- 你可以轻松地把一组
useEffect逻辑抽离成一个自定义 Hook(如useWindowSize),在不同组件间共享。而在类组件中,这需要通过高阶组件(HOC)或 Render Props 才能实现,结构非常复杂且容易造成“嵌套地狱”。
- 你可以轻松地把一组
- 心智模型简洁:
- 函数组件让你思考**“这个副作用依赖于哪些数据”**,而不是“我现在处于组件生命周期的哪个时刻”,这更符合声明式编程的直觉。
💡 总结话术(建议背诵)
“函数组件通过 Hooks 实现了关注点分离。它允许我们将同一业务逻辑的‘开始执行’与‘销毁清理’完整地写在同一个函数块中,而不是散落在不同的生命周期钩子函数里。这种高内聚的写法大大降低了代码的维护成本,并有效减少了因忘记在销毁阶段解绑事件而导致的内存泄漏风险。”
什么是高阶组件 (HOC)?
高阶组件(Higher-Order Component) 本质是一个 函数。它接收一个组件作为参数,并返回一个新的组件。
其原理是利用“包装”的思想,将通用的逻辑注入到被包装的组件中。
// 这是一个 HOC
function withLogger(WrappedComponent) {
return class extends React.Component {
componentDidMount() {
console.log(`${WrappedComponent.name} 挂载了`);
}
render() {
// 将原来的 props 传递下去,并增加一些新属性
return <WrappedComponent {...this.props} />;
}
};
}
// 使用
const EnhancedMyComponent = withLogger(MyComponent);
什么是 Render Props?
Render Props 指的是一种技术,通过一个值为函数的 prop,告知组件需要渲染什么内容。
其原理是将逻辑封装在父组件中,通过调用函数并将数据传回,让子组件决定如何渲染。
class MouseTracker extends React.Component {
state = { x: 0, y: 0 };
handleMouseMove = (event) => {
this.setState({ x: event.clientX, y: event.clientY });
};
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{/* 调用 props.render 并将 state 传出去 */}
{this.props.render(this.state)}
</div>
);
}
}
// 使用
<MouseTracker render={({ x, y }) => (
<h1>当前鼠标位置:{x}, {y}</h1>
)} />
深入理解函数式编程 (Functional Programming)
函数式编程是一种将计算视为数学函数计算的编程范式。在 React 的世界里,理解 FP 是掌握函数式组件、Hooks 以及状态管理的基础。
1. 函数式编程的核心特点
A. 声明式 (Declarative)
它关注 “要做什么” 而不是“怎么做”。
- 命令式:手动控制过程(循环、计数器、状态变更)。
- 函数式:描述逻辑(如使用
map、filter),由引擎处理具体实现。
B. 纯函数 (Pure Function)
这是 FP 的灵魂。纯函数必须满足两个条件:
- 确定性:相同的输入永远得到相同的输出。
- 无副作用:函数执行过程中不修改外部变量、不操作 DOM、不发送网络请求。
C. 不可变性 (Immutability)
数据一旦创建就不能修改。若需变更,应创建并返回一个包含新数据的新对象/数组,而不是在原数据上直接修改。
D. 函数是一等公民 (First-class Function)
函数可以像普通变量一样被使用:
- 作为参数传递(回调函数)。
- 作为返回值(闭包、高阶函数)。
- 赋值给变量。
2. 代码举例说明
A. 命令式 vs. 函数式
任务:过滤出数组中的偶数,并将它们乘以 2。
-
命令式写法(关注过程):
const arr = [1, 2, 3, 4]; const result = []; for (let i = 0; i < arr.length; i++) { if (arr[i] % 2 === 0) { result.push(arr[i] * 2); } } -
函数式写法(关注结果):
const arr = [1, 2, 3, 4]; const result = arr .filter(num => num % 2 === 0) .map(num => num * 2);
B. 纯函数 vs. 不纯函数
-
不纯函数(依赖且修改外部状态):
let factor = 2; function multiply(num) { factor = factor + 1; // 副作用:修改了外部变量 return num * factor; // 不确定性:结果取决于外部变量 } -
纯函数(只依赖参数):
function multiply(num, factor) { return num * factor; // 确定的输出,无副作用 }
3. 在 React 中的体现
React 的设计哲学深度拥抱了函数式编程:
- U I = f ( s t a t e ) UI = f(state) UI=f(state):UI 是数据的纯映射,组件本质上是一个将状态转换为视图的函数。
- 不可变状态:在 React 中禁止直接修改状态(如
state.push),必须使用setState([...state, item])。 - 单向数据流:数据像函数的参数一样从上向下传递,保证了链路的可追溯性。
- Hooks / 高阶组件:充分利用了函数作为“一等公民”的特性进行逻辑组合与复用。
💡 核心总结
- 纯函数 (Pure):相同输入 → \rightarrow → 相同输出,无副作用。
- 不可变性 (Immutability):不修改原数据,而是返回新数据。
- 高阶函数:函数可以作为参数或返回值(如
map,filter,reduce)。 - 声明式编程:描述“想要什么”,让代码更具可读性。
为什么 React 拥抱 FP?
因为纯函数和不可变性让 UI 的状态变得高度可预测。这极大地方便了调试和自动化测试,同时也为虚拟 DOM 的 Diff 算法提供了性能优化的基础(例如:只需通过简单的引用比较prevProps === nextProps即可判断是否需要更新)。
更多推荐


所有评论(0)