组件范式与生命周期
Class 组件 vs 函数组件的演变与生命周期对应关系
1. 组件范式演变
1.1 两种组件写法
jsx
// Class 组件 (React 早期)
class Counter extends React.Component {
state = { count: 0 };
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<button onClick={this.increment}>
Count: {this.state.count}
</button>
);
}
}
// 函数组件 + Hooks (React 16.8+)
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}1.2 为什么推荐函数组件?
| 对比项 | Class 组件 | 函数组件 |
|---|---|---|
| 代码量 | 多(constructor, bind) | 少 |
| this 绑定 | 需要处理 | 无 this |
| 逻辑复用 | HOC/Render Props | 自定义 Hook |
| 学习曲线 | 陡(OOP 概念) | 平缓 |
| 优化 | shouldComponentUpdate | React.memo |
| 未来支持 | 维护模式 | 主推 |
2. Class 组件生命周期
2.1 生命周期图解
┌─────────────────────────────────────────────────────────────────┐
│ 挂载阶段 (Mounting) │
├─────────────────────────────────────────────────────────────────┤
│ constructor() → static getDerivedStateFromProps() → render() │
│ → componentDidMount() │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 更新阶段 (Updating) │
├─────────────────────────────────────────────────────────────────┤
│ static getDerivedStateFromProps() → shouldComponentUpdate() │
│ → render() → getSnapshotBeforeUpdate() │
│ → componentDidUpdate() │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 卸载阶段 (Unmounting) │
├─────────────────────────────────────────────────────────────────┤
│ componentWillUnmount() │
└─────────────────────────────────────────────────────────────────┘2.2 各生命周期方法
javascript
class MyComponent extends React.Component {
// ========== 挂载阶段 ==========
constructor(props) {
super(props);
this.state = { count: 0 };
// 初始化 state,绑定方法
}
static getDerivedStateFromProps(props, state) {
// 根据 props 更新 state(很少用)
// 返回新 state 或 null
if (props.value !== state.prevValue) {
return { derivedValue: props.value, prevValue: props.value };
}
return null;
}
componentDidMount() {
// DOM 已挂载,可以:
// - 发起网络请求
// - 添加订阅/监听器
// - 操作 DOM
}
// ========== 更新阶段 ==========
shouldComponentUpdate(nextProps, nextState) {
// 返回 false 阻止渲染(性能优化)
return nextProps.id !== this.props.id;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// DOM 更新前调用,返回值传给 componentDidUpdate
// 用于保存滚动位置等
return this.listRef.scrollHeight;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// DOM 已更新
// snapshot 是 getSnapshotBeforeUpdate 的返回值
if (prevProps.id !== this.props.id) {
this.fetchData(this.props.id);
}
}
// ========== 卸载阶段 ==========
componentWillUnmount() {
// 清理工作:
// - 取消订阅
// - 清除定时器
// - 取消网络请求
}
// ========== 错误处理 ==========
static getDerivedStateFromError(error) {
// 渲染降级 UI
return { hasError: true };
}
componentDidCatch(error, info) {
// 记录错误信息
logErrorToService(error, info.componentStack);
}
render() {
return <div>{this.state.count}</div>;
}
}2.3 已废弃的生命周期
javascript
// ❌ React 17+ 已废弃,会有警告
componentWillMount() // → constructor + componentDidMount
componentWillReceiveProps() // → getDerivedStateFromProps
componentWillUpdate() // → getSnapshotBeforeUpdate3. 函数组件与 Hooks 对应关系
3.1 生命周期映射
| Class 生命周期 | 函数组件 Hooks |
|---|---|
constructor | useState 初始化 |
componentDidMount | useEffect(() => {}, []) |
componentDidUpdate | useEffect(() => {}, [deps]) |
componentWillUnmount | useEffect 返回的清理函数 |
shouldComponentUpdate | React.memo + useMemo |
getDerivedStateFromProps | 渲染时直接计算 |
getSnapshotBeforeUpdate | useLayoutEffect |
3.2 对应示例
jsx
// Class 组件
class Example extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
document.title = `Count: ${this.state.count}`;
this.subscription = subscribe();
}
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `Count: ${this.state.count}`;
}
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
return <div>{this.state.count}</div>;
}
}
// 函数组件等价实现
function Example() {
const [count, setCount] = useState(0);
// componentDidMount + componentDidUpdate
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
// componentDidMount + componentWillUnmount
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe(); // 清理
}, []);
return <div>{count}</div>;
}3.3 useEffect 执行时机
渲染 → DOM 更新 → 浏览器绘制 → useEffect 执行
↑
异步,不阻塞渲染
渲染 → DOM 更新 → useLayoutEffect 执行 → 浏览器绘制
↑
同步,阻塞渲染4. StrictMode
4.1 作用
jsx
<React.StrictMode>
<App />
</React.StrictMode>StrictMode 在开发模式下会:
- 双重调用函数组件、生命周期方法(检测副作用)
- 警告使用废弃 API
- 检测意外的副作用
4.2 为什么双重调用?
jsx
// StrictMode 会调用两次,帮助发现问题
function Counter() {
const [count, setCount] = useState(0);
// ❌ 错误:副作用在渲染中
globalCounter++; // 会被调用两次!
// ✅ 正确:副作用在 useEffect 中
useEffect(() => {
globalCounter++;
}, []);
return <div>{count}</div>;
}4.3 React 18 的变化
jsx
// React 18 StrictMode 模拟组件卸载/重新挂载
// 用于测试 Effects 的清理逻辑
useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
// 开发模式下输出:
// mount
// unmount (模拟卸载)
// mount (模拟重新挂载)5. 组件通信
5.1 Props 向下传递
jsx
// 父组件
function Parent() {
const [value, setValue] = useState('hello');
return <Child value={value} onChange={setValue} />;
}
// 子组件
function Child({ value, onChange }) {
return <input value={value} onChange={e => onChange(e.target.value)} />;
}5.2 状态提升
jsx
// 多个子组件共享状态,提升到共同父组件
function Parent() {
const [temperature, setTemperature] = useState(0);
return (
<>
<CelsiusInput temp={temperature} onChange={setTemperature} />
<FahrenheitInput temp={temperature} onChange={setTemperature} />
</>
);
}5.3 Context 跨层级
jsx
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<DeepChild />
</ThemeContext.Provider>
);
}
function DeepChild() {
const theme = useContext(ThemeContext);
return <div className={theme}>...</div>;
}6. 面试高频问题
Q1: 函数组件和 Class 组件的区别?
| 区别 | Class 组件 | 函数组件 |
|---|---|---|
| 写法 | ES6 class | 普通函数 |
| 状态 | this.state | useState |
| 生命周期 | 生命周期方法 | useEffect |
| this | 需要绑定 | 无 this |
| 性能 | 实例化开销 | 更轻量 |
| 逻辑复用 | HOC/Render Props | 自定义 Hook |
Q2: useEffect 和 componentDidMount 的区别?
| 区别 | componentDidMount | useEffect |
|---|---|---|
| 执行时机 | DOM 挂载后同步执行 | DOM 挂载后异步执行 |
| 依赖 | 无依赖概念 | 可指定依赖 |
| 清理 | componentWillUnmount | 返回清理函数 |
| 数量 | 一个 | 可以多个 |
Q3: 为什么 useEffect 的依赖数组很重要?
jsx
// 依赖数组控制 effect 何时执行
useEffect(() => {}, []); // 只在挂载时执行
useEffect(() => {}, [dep]); // dep 变化时执行
useEffect(() => {}); // 每次渲染都执行(通常是 bug)Q4: StrictMode 为什么会导致双重渲染?
为了帮助发现:
- 非幂等的渲染逻辑
- 副作用问题
- 状态初始化问题
通过双重调用,可以发现那些依赖"只执行一次"假设的 bug。
Q5: getDerivedStateFromProps 什么时候用?
很少用。大多数场景可以用:
- 完全受控组件(状态由父组件管理)
- 完全非受控组件 + key(重置时用新 key)
- useEffect 中更新状态
jsx
// 避免使用 getDerivedStateFromProps
// ✅ 更好的方式:使用 key 重置组件
<EmailInput key={userId} defaultEmail={user.email} />