# 进化的生命周期方法:React 16 生命周期工作流详解
来源于:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram (opens new window)
在React 16.4之后,React 生命周期在之前版本的基础上又经历了一次微调,其实就调在了更新过程的
getDerivedStateFromProps
这个生命周期上
这里我先提供一个 Demo,它将辅助你理解新的生命周期。Demo 代码如下
import React from "react"; import ReactDOM from "react-dom"; // 定义子组件 class LifeCycle extends React.Component { constructor(props) { console.log("进入constructor"); super(props); // state 可以在 constructor 里初始化 this.state = { text: "子组件的文本" }; } // 初始化/更新时调用 static getDerivedStateFromProps(props, state) { console.log("getDerivedStateFromProps方法执行"); return { fatherText: props.text } } // 初始化渲染时调用 componentDidMount() { console.log("componentDidMount方法执行"); } // 组件更新时调用 shouldComponentUpdate(prevProps, nextState) { console.log("shouldComponentUpdate方法执行"); return true; } // 组件更新时调用 getSnapshotBeforeUpdate(prevProps, prevState) { console.log("getSnapshotBeforeUpdate方法执行"); return "haha"; } // 组件更新后调用 componentDidUpdate(preProps, preState, valueFromSnapshot) { console.log("componentDidUpdate方法执行"); console.log("从 getSnapshotBeforeUpdate 获取到的值是", valueFromSnapshot); } // 组件卸载时调用 componentWillUnmount() { console.log("子组件的componentWillUnmount方法执行"); } // 点击按钮,修改子组件文本内容的方法 changeText = () => { this.setState({ text: "修改后的子组件文本" }); }; render() { console.log("render方法执行"); return ( <div className="container"> <button onClick={this.changeText} className="changeText"> 修改子组件文本内容 </button> <p className="textContent">{this.state.text}</p> <p className="fatherContent">{this.props.text}</p> </div> ); } } // 定义 LifeCycle 组件的父组件 class LifeCycleContainer extends React.Component { // state 也可以像这样用属性声明的形式初始化 state = { text: "父组件的文本", hideChild: false }; // 点击按钮,修改父组件文本的方法 changeText = () => { this.setState({ text: "修改后的父组件文本" }); }; // 点击按钮,隐藏(卸载)LifeCycle 组件的方法 hideChild = () => { this.setState({ hideChild: true }); }; render() { return ( <div className="fatherContainer"> <button onClick={this.changeText} className="changeText"> 修改父组件文本内容 </button> <button onClick={this.hideChild} className="hideChild"> 隐藏子组件 </button> {this.state.hideChild ? null : <LifeCycle text={this.state.text} />} </div> ); } } ReactDOM.render(<LifeCycleContainer />, document.getElementById("root"));
@程序员poetry: 代码已经复制到剪贴板
React 16 以来的生命周期也可以按照“挂载”“更新”和“卸载”三个阶段来看,所以接下来我们要做的事情仍然是分阶段拆解工作流程。
# Mounting 阶段
:组件的初始化渲染(挂载)
为了凸显 16 和 15 两个版本生命周期之间的差异,我将两个流程绘制到了同一张大图里,请看下面这张图:
运行示例代码
消失的 componentWillMount,新增的 getDerivedStateFromProps
从上图中不难看出,React 15
生命周期和 React 16.3
生命周期在挂载阶段的主要差异在于,废弃了 componentWillMount,新增了 getDerivedStateFromProps
注:细心的你可能记得,
React 16 对 render 方法也进行了一些改进
。React 16 之前,render方法必须返回单个元素
,而 React 16允许我们返回元素数组和字符串
getDerivedStateFromProps 不是 componentWillMount 的替代品
事实上,
componentWillMount
的存在不仅“鸡肋”而且危险,因此它并不值得被“代替”,它就应该被废弃
- 而
getDerivedStateFromProps
这个 API,其设计的初衷不是试图替换掉componentWillMount
,而是试图替换componentWillReceiveProps
,因此它有且仅有一个用途:使用 props 来派生/更新 state
- React 团队为了确保
getDerivedStateFromProps
这个生命周期的纯洁性,直接从命名层面约束了它的用途(getDerivedStateFromProps
直译过来就是“从 Props 里派生 State
”)。所以,如果你不是出于这个目的来使用getDerivedStateFromProps
,原则上来说都是不符合规范的
值得一提的是,
getDerivedStateFromProps
在更新和挂载两个阶段
都会“出镜”(这点不同于仅在更新阶段出现的componentWillReceiveProps
)。这是因为“派生 state”这种诉求不仅在props
更新时存在,在props
初始化的时候也是存在的
认识 getDerivedStateFromProps
这个新生命周期方法的调用规则如下:static getDerivedStateFromProps(props, state)
在使用层面,你需要把握三个重点。
第一个重点是最特别的一点:getDerivedStateFromProps
是一个静态方法。静态方法不依赖组件实例而存在,因此你在这个方法内部是访问不到 this
的。若你偏要尝试这样做,必定报错,报错形式如下图所示:
第二个重点,该方法可以接收两个参数:props 和 state,它们分别代表当前组件接收到的来自父组件的 props
和当前组件自身的 state
。我们可以尝试在 Demo 中输出这两个参数看一看,输出效果如下图所示:
可以看出,挂载阶段输出的 props
正是初始化阶段父组件传进来的 this.props
对象;而 state
是 LifeCycle
组件自身的 state
对象
第三个重点,getDerivedStateFromProps 需要一个对象格式的返回值。如果你没有指定这个返回值,那么大概率会被 React 警告一番,警告内容如下图所示:
getDerivedStateFromProps
的返回值之所以不可或缺,是因为 React 需要用这个返回值来更新(派生)组件的state
。因此当你确实不存在“使用props 派生 state
”这个需求的时候,最好是直接省略掉这个生命周期方法的编写,否则一定记得给它return
一个null
注意,getDerivedStateFromProps
方法对 state
的更新动作并非“覆盖”式的更新,而是针对某个属性的定向更新。比如这里我们在 getDerivedStateFromProps
里返回的是这样一个对象,对象里面有一个 fatherText
属性用于表示“父组件赋予的文本”:
{ fatherText: props.text }
@程序员poetry: 代码已经复制到剪贴板
该对象并不会替换掉组件原始的这个 state
:
this.state = { text: "子组件的文本" };
@程序员poetry: 代码已经复制到剪贴板
而是仅仅针对
fatherText
这个属性作更新(这里原有的state
里没有fatherText
,因此直接新增)。更新后,原有属性与新属性是共存的,如下图所示
# Updating 阶段:组件的更新
React 15 与 React 16.3 的更新流程对比如下图所示:
React 16.4
对生命周期流程进行了“微调”,其实就调在了更新过程的getDerivedStateFromProps
这个生命周期上
React 16.4
的挂载和卸载流程都是与 React 16.3
保持一致的,差异在于更新流程上:
- 在
React 16.4
中,任何因素触发的组件更新流程(包括由this.setState
和forceUpdate
触发的更新流程)都会触发getDerivedStateFromProps
; - 而在
v 16.3
版本时,只有父组件的更新会触发该生命周期
。
到这里,你已经对 getDerivedStateFromProps
相关的改变有了充分的了解。接下来,我们就基于这层了解,问出生命周期改变背后的第一个“Why”。
1. 改变背后的第一个“Why”:为什么要用 getDerivedStateFromProps 代替 componentWillReceiveProps?
对于 getDerivedStateFromProps
这个 API,React 官方曾经给出过这样的描述
与
componentDidUpdate
一起,这个新的生命周期涵盖过时componentWillReceiveProps
的所有用例
在这里,请你细细品味这句话,这句话里蕴含了下面两个关键信息:
getDerivedStateFromProps
是作为一个试图代替componentWillReceiveProps
的 API 而出现的;getDerivedStateFromProps
不能完全和componentWillReceiveProps
画等号,其特性决定了我们曾经在componentWillReceiveProps
里面做的事情,不能够百分百迁移到getDerivedStateFromProps
里。
接下来我们就展开说说这两点。
- 关于
getDerivedStateFromProps
是如何代替componentWillReceiveProps
的,在“挂载”环节已经讨论过:getDerivedStateFromProps
可以代替componentWillReceiveProps
实现基于props 派生 state
。 - 至于它为何不能完全和
componentWillReceiveProps
画等号,则是因为它过于“专注”了。这一点,单单从getDerivedStateFromProps
这个 API 名字上也能够略窥一二。原则上来说,它能做且只能做这一件事
getDerivedStateFromProps
生命周期替代componentWillReceiveProps
的背后,是 React 16 在强制推行“只用getDerivedStateFromProps
来完成props 到 state
的映射”这一最佳实践。确保生命周期函数的行为更加可控可预测,从根源上帮开发者避免不合理的编程方式,避免生命周期的滥用;同时,也是在为新的 Fiber 架构铺路
2. 消失的 componentWillUpdate 与新增的 getSnapshotBeforeUpdate
咱们先来看看 getSnapshotBeforeUpdate
是什么:
getSnapshotBeforeUpdate(prevProps, prevState) { // ... }
@程序员poetry: 代码已经复制到剪贴板
这个方法和
getDerivedStateFromProps
颇有几分神似,它们都强调了“我需要一个返回值”这回事。区别在于 getSnapshotBeforeUpdate 的返回值会作为第三个参数给到 componentDidUpdate
。它的执行时机是在 render 方法之后,真实 DOM 更新之前
。在这个阶段里,我们可以同时获取到更新前的真实 DOM 和更新前后的state&props
信息
尽管在实际工作中,需要用到这么多信息的场景并不多,但在对于实