React 组件生命周期
1. 组件生命周期
基本概念
每个 React 组件都像一个生命体,从诞生(被创建并添加到 DOM)到成长(通过更新 props 和 state 来改变),再到最终的消亡(从 DOM 中移除)。React 提供了一系列特殊的“生命周期方法”,允许我们在组件生命中的特定时刻执行代码。
React 的组件生命周期的三个核心阶段如下:
- 挂载 (Mounting): 组件实例被创建并插入到 DOM 中的过程。
- 更新 (Updating): 当组件的
props或state发生变化,导致需要重新渲染时的过程。 - 卸载 (Unmounting): 当组件实例从 DOM 中被移除时的过程。
除此之外,还有一个特殊的阶段:错误处理 (Error Handling),用于捕获并处理子组件发生的错误。
下面是一个组件生命周期图谱:
挂载阶段
当组件实例被创建并插入到 DOM 中时,以下方法会按顺序被调用。这个阶段只发生一次。
constructor(props):它完成了如下工作:- 通过给
this.state赋值来初始化组件的内部 state。 - 为事件处理函数绑定
this。
- 通过给
注意: 这是唯一可以直接对
this.state赋值的地方。在所有其他方法中,都必须使用this.setState()。
constructor(props) {
super(props); // 必须先调用 super(props)
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
static getDerivedStateFromProps(props, state):它在组件实例化后、渲染前调用,以及在后续更新中也会被调用。它允许组件根据props的变化来更新其内部state。它具有如下特点:- 它是一个静态方法,无法访问组件实例的
this。 - 它应该返回一个对象来更新
state,或者返回null表示无需更新。
- 它是一个静态方法,无法访问组件实例的
- 这是一个不常用的方法,适用于
state的值完全由props决定(受控组件)的罕见情况。
警告: 过度使用此方法通常会导致代码冗余和复杂化。在需要它之前,应该先考虑是否有更简单的解决方案,例如:将组件完全重构为受控组件,或使用
keyprop 来重置整个组件。
-
render(): 这是唯一必须的类组件方法。React 调用此方法来获取要渲染到屏幕上的 UI 描述(JSX)。render方法必须是纯函数。这意味着在不改变props和state的前提下,每次调用都应返回相同的结果,并且不应包含任何副作用(如 API 请求)。 -
componentDidMount():它在组件已经被渲染到 DOM 中之后立即调用。它是执行需要 DOM 节点或数据加载等副作用操作的完美时机。下面这些场景会使用它:- 网络请求: 从服务器获取数据。
- 设置订阅: 如
setInterval或addEventListener。 - DOM 操作: 与需要计算尺寸或位置的 DOM 元素进行交互。
componentDidMount() {
console.log('Component has mounted!');
const userID = this.props.userID;
fetch(`https://api.example.com/users/${userID}`)
.then(res => res.json())
.then(user => this.setState({ user }));
}
更新阶段
当组件的 props 或 state 发生变化时,会触发更新。更新的方法如下:
shouldComponentUpdate(nextProps, nextState):这是一个性能优化的关键方法。React 在重新渲染前会调用它,让我们有机会告诉 React 本次更新是否非必要。它的返回值如下:- 返回true(默认值): 继续执行更新流程(调用render等)。- 返回false: 跳过本次更新,render()不会被调用。注意: 可以在此方法中比较
this.props与nextProps,以及this.state与nextState,来决定是否需要重新渲染。
为了方便,可以让组件继承自
React.PureComponent而不是React.Component。PureComponent会自动为我们实现一个shouldComponentUpdate,它会对props和state进行浅层比较。
-
render():如果shouldComponentUpdate返回true,render()方法会再次被调用,以获取最新的 UI。 -
getSnapshotBeforeUpdate(prevProps, prevState):在render之后,但在最终渲染结果提交到 DOM 之前调用。它使得组件可以在 DOM 可能发生变化(例如,列表增加项目)之前从中捕获一些信息(如滚动位置)。此方法返回的任何值都将作为第三个参数传递给componentDidUpdate()。 -
componentDidUpdate(prevProps, prevState, snapshot):在组件更新被提交到 DOM 后立即调用。此方法不适用于首次渲染。它一般用于下面的场合:- 当
props变化时执行网络请求。 - 根据 DOM 的变化执行某些操作。
- 当
重要: 在
componentDidUpdate中调用setState必须包裹在一个条件语句中,否则会导致无限循环的重新渲染!
componentDidUpdate(prevProps, prevState) {
// 检查 props.userID 是否真的改变了
if (this.props.userID !== prevProps.userID) {
// 如果不加条件判断,每次 setState 都会触发 componentDidUpdate,造成死循环
this.fetchData(this.props.userID);
}
}
卸载阶段
componentWillUnmount():在组件即将被从 DOM 中移除并销毁之前调用。这是执行所有必要清理操作的理想场所。常见场景如下- 清除在
componentDidMount中创建的定时器 (clearInterval)。 - 移除事件监听器 (
removeEventListener)。 - 取消网络请求或任何挂起的订阅,以防止内存泄漏。
- 清除在
componentWillUnmount() {
console.log('Component will unmount. Cleaning up...');
clearInterval(this.timerID);
this.props.api.unsubscribe();
}
2. 在 useEffect 中的使用
函数组件中的 useEffect Hook 巧妙地将 componentDidMount、componentDidUpdate 和 componentWillUnmount 的功能整合在了一起。
-
模拟
componentDidMount:useEffect(() => { /* ... */ }, [])(依赖项数组为空) -
模拟
componentDidUpdate:useEffect(() => { /* ... */ }, [dep1, dep2])(依赖项数组有值) -
模拟
componentWillUnmount:useEffect返回的清理函数。
// 函数组件
useEffect(() => {
// 这部分代码在挂载时 (componentDidMount) 和 a, b 更新时 (componentDidUpdate) 运行
console.log("Effect running...");
// 返回的函数在卸载时 (componentWillUnmount) 和下一次 effect 运行前运行
return () => {
console.log("Cleanup...");
};
}, [a, b]); // 依赖项数组
3. 已废弃的生命周期方法
UNSAFE_componentWillMount(): 在render之前调用。现在应使用constructor或componentDidMount。UNSAFE_componentWillReceiveProps(): 在 props 改变时调用。现在应使用getDerivedStateFromProps。UNSAFE_componentWillUpdate(): 在render之前、shouldComponentUpdate之后调用。现在应使用getSnapshotBeforeUpdate。