接手一个老项目,yarn start 之后控制台刷一屏黄字,说 componentWillReceiveProps 已经改名成 UNSAFE_componentWillReceiveProps。当时最直接的反应是把名字改一下把警告压下去,改完发现还是有地方在报,而且顺着报错点进去看,那段逻辑是「拿到新 props 就发一次请求」。这个写法在 React 15 时代到处都是,凭什么现在就成了 unsafe 的?
这篇就是顺着这个问题往下挖的记录。先把 React 16.0 之前那套完整的生命周期讲一遍,再讲 Fiber 之后为什么必须动刀,最后把 getDerivedStateFromProps 和 getSnapshotBeforeUpdate 这两个新钩子的适用边界说清楚。读完你应该能回答一个面试里很常见的问题:为什么 shouldComponentUpdate 活下来了,它旁边那三个却没有。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- React 16.0 之前完整的四个阶段和每个钩子的职责
- 造成组件更新的两类三种情况,以及各自怎么优化
- Fiber 的可中断渲染为什么会让 render 之前的钩子变得危险
- React 16.3 和 16.4 的
getDerivedStateFromProps有什么不一样 getSnapshotBeforeUpdate解决的是哪一类问题- 派生 state 这件事本身的坑,以及官方推荐的替代写法
- 这套 API 到今天(React 19 时代)的现状
# 一、React v16.0 前的生命周期
先看这张全景图,后面所有讨论都会回到它上面。

整张图分成四段,初始化、挂载、更新、卸载。挂载和卸载各走一次,更新这一段会反复走。
# 1.1 第一个是组件初始化(initialization)阶段
也就是以下代码中类的构造方法(constructor())。Test 类继承了 react 的 Component 这个基类,正因为继承了这个基类,才能有 render()、生命周期等方法可以使用,这也说明了为什么函数组件不能使用这些方法。
super(props) 用来调用基类的构造方法,也将父组件的 props 注入给子组件供子组件读取(组件中 props 只读不可变,state 可变),而 constructor() 用来做一些组件的初始化工作,比如定义 this.state 的初始内容。
import React, { Component } from 'react';
class Test extends Component {
constructor(props) {
super(props);
}
}
这段代码本身没干什么活,但它是理解后面一切的起点。构造函数是整个组件生命里唯一一次「什么都还没有」的时刻,DOM 没有,props 的后续变化也还没发生。所以凡是「只需要算一次的初始值」,放这里最省事。
顺带说一句,如果构造函数里只有 super(props) 这一行,那这个 constructor 可以整个删掉,ES6 class 会自动生成。ESLint 的 no-useless-constructor 规则就管这个。
# 1.2 第二个是组件的挂载(Mounting)阶段
这个阶段依次经过 componentWillMount、render、componentDidMount 三步。
# 1.2.1 componentWillMount
在组件挂载到 DOM 前调用,且只会被调用一次。在这里调用 this.setState 不会引起组件重新渲染,也可以把写在这里的内容提前到 constructor() 中,所以项目中很少用。
这条「可以提前到 constructor」的结论很关键,它其实已经暗示了这个钩子存在的必要性不强,后面 React 团队砍掉它的底气也在这里。
# 1.2.2 render
根据组件的 props 和 state(无两者的重传递和重赋值,无论值是否有变化,都可以引起组件重新 render),return 一个 React 元素(描述组件,即 UI),不负责组件实际渲染工作,之后由 React 自身根据此元素去渲染出页面 DOM。
render 是纯函数(Pure function,函数的返回结果只依赖于它的参数,函数执行过程里面没有副作用),不能在里面执行 this.setState,会有改变组件状态的副作用。
这里的「纯」不是一句道德倡议,是硬约束。你在 render 里写一次埋点上报,Fiber 时代这段代码可能被跑两遍,埋点数就翻倍了。
# 1.2.3 componentDidMount
组件挂载到 DOM 后调用,且只会被调用一次。
这是发请求、加事件监听、初始化第三方图表实例的标准位置。原因很简单,走到这一步真实 DOM 已经在页面上了,this.refs 和 ref 都能拿到节点。
# 1.3 第三个是组件的更新(update)阶段
setState 引起的 state 更新,或父组件重新 render 引起的 props 更新,更新后的 state 和 props 相对之前无论是否有变化,都将引起子组件的重新 render。
那句「无论是否有变化」是很多性能问题的源头,下面拆开讲。
# 1.3.1 造成组件更新有两类(三种)情况
1. 父组件重新 render
父组件重新 render 引起子组件重新 render 的情况有两种。
a. 直接使用,每当父组件重新 render 导致的重传 props,子组件将直接跟着重新渲染,无论 props 是否有变化。可通过 shouldComponentUpdate 方法优化。
class Child extends Component {
shouldComponentUpdate(nextProps){ // 应该使用这个方法,否则无论props是否有变化都将会导致组件跟着重新渲染
if(nextProps.someThings === this.props.someThings){
return false
}
}
render() {
return <div>{this.props.someThings}</div>
}
}
这段代码在解决的是「父组件动一下,子树全体陪跑」的问题。要注意这个示例只在相等时 return false,不相等时函数走到底返回 undefined,React 会当成真值继续渲染,行为上是对的,但更严谨的写法是显式 return true。生产代码里这类隐式返回很容易在后面加分支时出错。
b. 在 componentWillReceiveProps 方法中,将 props 转换成自己的 state。
class Child extends Component {
constructor(props) {
super(props);
this.state = {
someThings: props.someThings
};
}
componentWillReceiveProps(nextProps) { // 父组件重传props时就会调用这个方法
this.setState({someThings: nextProps.someThings});
}
render() {
return <div>{this.state.someThings}</div>
}
}
在该函数(componentWillReceiveProps)中调用 this.setState() 将不会引起第二次渲染。
原因是 componentWillReceiveProps 中判断 props 是否变化了,若变化了,this.setState 将引起 state 变化,从而引起 render,此时就没必要再做第二次因重传 props 引起的 render 了,不然重复做一样的渲染了。
这个写法有个正式名字叫「派生 state」,它是整篇文章里最值得警惕的一处,第四节会专门算这笔账。
2. 组件本身调用 setState,无论 state 有没有变化。可通过 shouldComponentUpdate 方法优化
class Child extends Component {
constructor(props) {
super(props);
this.state = {
someThings:1
}
}
shouldComponentUpdate(nextProps, nextState){ // 应该使用这个方法,否则无论state是否有变化都将会导致组件重新渲染
if(nextState.someThings === this.state.someThings){
return false
}
return true
}
handleClick = () => { // 虽然调用了setState ,但state并无变化
const preSomeThings = this.state.someThings
this.setState({
someThings: preSomeThings
})
}
render() {
return <div onClick = {this.handleClick}>{this.state.someThings}</div>
}
}
这里我改了原文一处签名。shouldComponentUpdate 的形参顺序固定是 (nextProps, nextState),原文写成了 shouldComponentUpdate(nextStates),那样第一个参数拿到的其实是 props,比较永远为 false,return false 分支根本进不去。这是个很典型的坑,把它当反面教材记住就行。
关于 setState 本身的批量合并、异步表现和更新队列,我在 React setState 原理剖析 里单独拆过,这里不重复。
# 1.3.2 componentWillReceiveProps(nextProps)
此方法只调用于 props 引起的组件更新过程中,参数 nextProps 是父组件传给当前组件的新 props。但父组件 render 方法的调用不能保证重传给当前组件的 props 是有变化的,所以在此方法中要根据 nextProps 和 this.props 来查明重传的 props 是否改变,以及如果改变了要执行什么,比如根据新的 props 调用 this.setState 触发当前组件的重新 render。
# 1.3.3 shouldComponentUpdate(nextProps, nextState)
此方法通过比较 nextProps、nextState 及当前组件的 this.props、this.state,返回 true 时当前组件将继续执行更新过程,返回 false 则当前组件更新停止,以此可用来减少组件的不必要渲染,优化组件性能。
这边也可以看出,就算 componentWillReceiveProps() 中执行了 this.setState 更新了 state,但在 render 前(如 shouldComponentUpdate、componentWillUpdate),this.state 依然指向更新前的 state,不然 nextState 及当前组件的 this.state 的对比就一直是 true 了。
# 1.3.4 componentWillUpdate(nextProps, nextState)
此方法在调用 render 方法前执行,在这里可执行一些组件更新发生前的工作,一般较少用。