React 生命周期函数深入浅出
本篇博文基于 React 16.5.2
吐槽:
作为一个后端开发,15 年开始关注大前端发展趋势,于 17 年去线下听了场前端开发会议,那个时候 Vue2.0 刚出没多久,就被那快速构建页面给吸引了。最早重返前端还是大半年前,新项目用vue写了几个功能页面,发现现在写前端是真挺舒服,尤其是对于后端人员来说(排除掉 CSS ),快速入门并上手不是什么问题。
至于为什么最终选择了 react 而非 vue ?是因为当时对 react 和 vue 及 RN 和 weex 做了番调研对比,鉴于 weex 的不给力,及后期 react 和 vue 学习成本差不多,但 react 的社区更为活跃,外加发起者背景,就毅然选择了 react 。(我个人是通一精百的支持者,所以对于 react 的理念(learn once,write anywhere),是很赞成的。而 weex 的理念 (write once,run anywhere) 虽然很吸引人,但时下个人觉得技术并未达到此程度,配置的复杂度及大量的轮子需造,难以满足大型项目的要求。包括目前 JD 推出的 Taro,个人目前持观望态度,等到 react 这块应用到项目之后,再码一波 Taro 实际调研一番)。
接触 React 的时候已经是 React 16.3 ,不禁感慨前端发展至今,越有后端的趋势。前后花了3个多月的时间过了一遍 webpack 4, npm , react ,在公司内部做了几场培训,发现了其中的一些不协调,但随着版本的迭代,这些不协调也依次在被更正。(看 React 17 的更新内容,原本一些摸棱两可的方法、属性元素命名均会得到改善:)
正文
本篇博文会对 React 16.5.2 中的常用生命周期函数做一些翻译、讲解说明及发表一些个人的理解和看法,如有错误、歧义之处,还望大家不吝啬指出,互相交流:)
开篇借用官方提供的生命周期函数图:(虽然是 React 16.4 的,但同样适用于 16.5 版本)
出处:http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
react 组件生命周期函数说明官方地址:https://reactjs.org/docs/react-component.html
getDerivedStateFromProps()方法的 16.3 版本与之后的 16.4 和 16.5 有所调整:
在 16.3 版本上,该方法仅在 props 变动时才会被触发。
在 16.3 之后的版本上,该方法调整为 props 和 state 的变动都会触发该方法。
从上图中,我们可以看到,React 的生命周期函数主要集中在三个阶段:挂载阶段(Mounting),更新阶段(Updating)和卸载阶段(Unmounting)。
其中:
-
Mounting阶段主要有这几个方法需要注意:
- constructor(props)
- componentWillReceiveProps(nextProps)
- getDerivedStateFromProps(props, state)
- componentWillMount()
- render()
- componentDidMount()
-
Updating阶段主要有这几个方法需要注意:
- componentWillReceiveProps(nextProps)
- getDerivedStateFromProps(props, state)
- shouldComponentUpdate()
- componentWillUpdate()
- render()
- componentDidUpdate()
-
Unmounting阶段只有一个方法需要注意:
- componentWillUnmount()
-
额外的几个方法说明:
- componentDidCatch()
- this.setState()
- this.forceUpdate()
下面我们就对以上生命周期函数做进一步说明:
Mounting:
constructor(props)
-
在构造函数中,一般就做两件事情:
-
初始化 state
-
在构造函数中初始化 state 需直接赋值,不能调用 this.setState() 方法。
-
constructor() 是唯一能够让你直接给 this.state 赋值的地方,可能有同学会说可以在构造函数外(跟 constructor 同级)直接给 state 赋值,确实,因为 state 派生自 Component 或者 PureComponent 模块,在外层直接赋值,其实是在给基类中的 state 赋值,而在 constructor() 使用 this.state = {…} 进行初始化,实际指的是当前组件这个实例。以下两种赋值方法均可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class TestContainer extends PureComponent {
state = {
content:"1" //先执行
}
constructor(props){
super(props);
this.state={
content:"2" //后执行,最终 this.state.content 为 2
}
}
...
}
-
-
绑定方法:this.handleClick = this.handleClick.bind(this);
- 我所认为的是:在构造函数中进行方法的绑定后,在使用的时候可以减少匿名方法的产生,进而提高性能。
-
-
如果不需要初始化state,或者绑定方法,那么可以不用实现constructor(props)。
-
如果实现了这个方法,那么应当在方法体的最开始调用 super(props),否则会导致 this.props 为 undefined 的问题。
-
不应在该方法中进行 “事件的订阅/引入有 side-effects 的方法”。(这种应该放在 componentDidMount() 中)
这里的 side-effects 方法,翻译出来是 “副作用” 的意思,个人觉得不妥,有点生涩,可能翻译成 “附加作用” 更为妥当。
它指的是,render() 方法应该只完成它的主要功能(如初始化 state 和 绑定方法 ),不应顺带完成其他的附加功能,如 统计,动画 等。
componentWillReceiveProps(nextProps)
该方法的应用场景:根据特定的 props 变换,来触发 state 的更新。如我们可能会有一个 canvas 用来表示页面 loading 百分比,这个时候就可以根据传入的 nextProps 中的百分比属性跟当前 state 的百分比属性做对比,若不一致,则更新,如下:
1 | componentWillReceiveProps(nextProps) { |
-
该方法在 React 16 版本中已经被标记为 unsafe ,虽然目前依然可以使用(即使用 componentWillReceiveProps 或者 UNSAFE_componentWillReceiveProps 均可),但在 React 17 中将会移除,不再建议使用【该方法将会被 getDerivedStateFromProps() 取代。】。
-
在组件对新(下一个) props 执行任何操作之前,该方法会被触发,并将 下一个props 作为参数。
-
初始渲染时该方法不会被触发。
-
可以在该方法内调用 this.setState(),但并不建议调用,因为这会导致只要组件更新,这个方法就会被执行。
getDerivedStateFromProps(props, state)
该方法的应用场景:使组件能够根据父组件传入的 props 来更新其自身的 state
在 16.4 版本之前,只有 props 更新才会触发该方法;但 FB 在 16.4 版本做了完善,目前 props 和 state 的更新均会触发该方法。
-
该方法的两个入参,分别表示:
- props:父组件传入的值。可能重命名为 nextProps 更为直观。
- state:组件自身的state,相当于 this.state,可能重命名为 prevState 更好理解。
-
该方法在调用 render() 方法前被触发。
-
如果父组件的 state 进行了更新,那么其子组件也会触发 getDerivedStateFromProps() 方法。
-
使用该方法的时候需要初始化 state ,否则在控制台中会出现警告信息,如下图:
-
使用该方法,需要在该方法中返回一个:对象/null:
- 如果返回的是 对象 ,则会更新 state。
- 如果返回的是 null ,则表示不更新。
-
该方法用于取代:componentWillReceiveProps(nextProps) 生命周期函数。
- getDerivedStateFromProps(props, state) 和 componentWillReceiveProps(nextProps) 的差异:
- componentWillReceiveProps(props, state):仅在父组件更新时触发。
- getDerivedStateFromProps(props, state):除了父组件更新,其自身的 state 更新时也会触发。
- getDerivedStateFromProps(props, state) 和 componentWillReceiveProps(nextProps) 的差异:
-
该方法无法访问组件的实例,换句话说,不能在该方法内部,调用 this.state,this.myFunction() 等实例对象/方法。
componentWillMount()
使用场景:可以在根组件的 componentWillMount 中做一些App的配置。
-
该方法在 React 16 版本中已经被标记为 unsafe ,虽然目前依然可以使用(即使用 componentWillMount 或者 UNSAFE_componentWillMount 均可),但在 React 17 中将会移除,不再建议使用。
-
该方法将会被 getDerivedStateFromProps() 取代。
-
该方法是 SSR(server-side render) 渲染上唯一的生命周期函数。
-
不要在该方法中使用 RPC 请求加载组件的数据。
-
不能在该方法中调用 this.setState() 。
render()
-
组件中唯一必须的方法。
-
render() 方法的返回类型目前有 string,number,boolean,null,portal 以及 fragments (带有key属性的数组),eg:
1
2
3
4
5
6
7
8
9
10
11
12render() {
return "string"; //string
return 123; //number
return true; //如果返回的是 false,则什么都不渲染
return null; //如果返回的是 null,则什么都不渲染
return ReactDOM.createPortal(<MyComponent></MyComponent>, domNode);//portal
return [
<div key="unique_1">如果返回的是数组类型</div>,
<span key="unique_2">需要在每个html元素上加上 key 值</span>, //fragments
<p key="unique_3">否则控制台会报错</p>
]
} -
需要注意的是,组件在更新的时候,也会触发 render() 方法,但如果 shouldComponentUpdate() 返回的是false,则在 “更新阶段” ,render() 方法不会被触发。
-
该方法应为一个纯函数,除了做渲染的动作外,不应顺带完成其他动作(如 setState 等),即应避免 side-effects。
-
应尽量仅在 render() 这个生命周期函数中中从 this.props 和 this.state 中读取数据。
componentDidMount()
使用场景:整体来说就是做“获取一些数据”,或者做必须要有 DOM 才能做的设置。
-
只会触发一次:在组件被挂载到页面之后被触发,之后就不会再执行了(如页面的更新等都不会再触发,需注意)。
-
一般一些 rpc 请求会放到这个方法中进行调用。
-
一般事件的订阅会写在该方法中(但是不要忘记在组件unmount的时候进行取消订阅。)
-
一些需要初始化DOM节点的操作可以放在这个声明周期函数中进行。
-
可以在该方法中使用 this.setState()。但需要注意,该操作会在浏览器更新屏幕之前发生,对于用户而言是无感的(即在该方法中又额外调用了一次 this.setState ,导致 render() 触发了2次,但用户的感受依然是屏幕更新了一次)。不过要谨慎在该方法中调用 this.setState,会导致性能问题。
Mounting 阶段,生命周期函数执行顺序如下图(包含子组件)
排除了 UNSAFE 的方法。
Updating:
componentWillReceiveProps(nextProps)
-
不再复述,具体 点此跳转
getDerivedStateFromProps(props, state)
-
不再复述,具体 点此跳转
shouldComponentUpdate(nextProps, nextState)
使用场景:一般用于精确控制组件是否需要重新渲染(这个方法一般用于性能优化),在绝大多数情况下,每次 state 的更改都应该让组件重新渲染。
- 只要有一个字段进行了更新,那么其他所有字段都会进行更新,这个会减慢页面速度。(通过 shouldComponentUpdate ,允许我们只有当我们关心的 props 更新的时候,才进行组件的更新)。
- 但需要谨慎使用,因为一旦你自己忘记实现了这个方法,可能会导致你的 react 组件无法正常更新。
-
该方法返回的数据类型是一个 boolean ,用于决定组件是否应该更新。
- 默认返回 true。
- 当返回 false 的时候,阻止的是当前的组件进行更新,对于当前组件的 “子组件” ,并不受影响。
-
不应该在继承于 PureComponent 的组件中显示实现 shouldComponentUpdate(),控制台会有警告。
-
目前,当该方法返回 false 的时候,componentWillUpdate()、更新时的 render() 以及 componentDidUpdate() 都将不会被触发。(FB在官网上说后期有可能会调整该方法:即使返回 false,依然有可能会触发再次渲染,需留个心)
-
什么时候该方法不会被触发?
- 组件第一次渲染时。
- 在组件中调用 this.forceUpdate() 时。
-
个人建议:
- 该方法应尽量不去手动实现。
- 不应使用该方法来做一些防止组件渲染的操作,这可能会导致一些莫名的 bug,如组件明明应该更新,但是却没有更新。
- 优先可以考虑继承 PureComponent 而非 Component 来优化组件而不是手动实现 shouldComponentUpdate。
-
不能在这个方法中使用 this.setState()。
componentWillUpdate(nextProps, nextState)
使用场景:当你实现了 shouldComponentUpdate (返回 true 时) 并且在 props 更改时需要执行某些操作的时候,那么 componentWillUpdate 这个方法还是有点意义的,但个人认为,作用不是太大,反倒是增加了理解的复杂度,被删除也是情理之中。
-
该方法在 React 16 版本中已经被标记为 unsafe,虽然目前依然可以使用(即使用 componentWillUpdate 或者 UNSAFE_componentWillUpdate() 均可),但在 React 17 中将会移除,不再建议使用。
-
从功能上来说,componentWillUpdate(nextProps, nextState) 跟 componentWillReceiveProps(nextProps) 基本相同,只是前者不允许调用 this.setState()。但是,这2个方法
-
当 shouldComponentUpdate 返回 false 时, 该生命周期函数将不会被触发。
-
不能使用 setState。
render()
-
不再复述,具体 点此跳转
componentDidUpdate(prevProps, prevState, snapshot)
使用场景:
- 一般在这个方法中进行 RPC 请求。(可以比较下先前的 props 和当前的 props 是否一致,若一致,则可以不用请求网络数据。)
- 如果想要在DOM自身更新之后做一些动作(如重新排列网格等),那么可以在这个方法中进行。
-
该生命周期函数在组件更新完成后立即执行。组件第一次渲染的时候,该生命周期函数不会被触发。
-
可以做跟 componentDidMount 中所作的相同的事情:如重置布局,重绘画布等
-
可以使用 setState。
Updating 阶段,生命周期函数执行顺序如下图(包含子组件)
排除了 UNSAFE 的方法。
Unmounting:
componentWillUnmount()
-
该方法在组件将要被卸载的时候触发。
-
另外,在渲染期间,当前组件/其子组件的 componentDidMount() 函数发生错误时,该方法也将被触发。
-
eg:
- 我刻意在"嵌套在子组件中的组件"中的 componentDidMount() 方法中,抛出了一个异常,此时该组件往上的父级组件均触发了 componentWillUnmount()
-
- 我尝试在 construct(),componentDidUpdate() 等生命周期函数中抛出异常,均不会触发 componentWillUnmount()。
Unmounting 阶段,生命周期函数执行顺序如下图(包含子组件)
这里需要注意下,该函数是 will unmount,所以触发顺序上是从父组件到子组件,但释放顺序上,是最内层的子组件先释放,最终最外层的根组件才释放。
额外的几个方法说明:
componentDidCatch(error,info)
使用场景:UI中的一些JS错误不应该使整个App崩溃,为了解决这个问题,React 16 中引入了 Error Boundary(错误边界) 这个概念,旨在解决允许页面的部分组件异常但不影响 App 的渲染。可以认为是组件中的 try-catch。而为了实现这个功能,就需要借助 componentDidCatch() 这个生命周期函数。
-
Error Boundaris 是自定义的 react 的组件,这些组件可以捕获其子组件的异常,并显示子组件错误时的替代组件内容,而 App 不会崩溃。
-
怎么样的组件可以认为是 Error Boundary 组件?
-
只要该组件内部实现了 componentDidCatch 这个方法就可以认为是 ErrorBoundary 组件,如:
1
2
3
4
5
6
7
8...
class MyErrorBoundary extends Component{
...
componentDidCatch(error,info){
...
}
...
}
-
-
一个例子:当子组件发生错误时,显示 “sth wrong here.”
-
MyErrorBoundary:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import React, { Component } from 'react';
class MyErrorBoundary extends Component {
state = {
isError: false
}
render() {
if (this.state.isError) {
return (<div>sth wrong here.</div>);
}
return this.props.children;
}
componentDidCatch(error, info) {
console.log(error, info);
this.setState({
isError = true
});
//也可以做其他的一些事情,如 日志,异常数统计 等
}
}
export default MyErrorBoundary; -
App.js
1
2
3
4
5
6
7
8
9
10
11...
render(){
return (
...
<MyErrorBoundary>
<OtherComponent />
</MyErrorBoundary>
...
);
}
...
-
-
ErrorBoundary 组件只能捕获其子组件,无法捕获其自身的异常。
-
为什么用 ErrorBoundary 这样的组件?直接在代码中 try-catch 不是很直观?
- 这个问题,就仁智各见了, try-catch 面向的是 代码,而 ErrorBoundary 面向的是组件:这里我想用一个后端例子来说明,在写后端代码时(比如一个 Web Api) ,你可以在具体业务代码中使用 try-catch 来对可能出现异常的代码块做处理,但有的时候,你可能想针对所有 “方法” 做异常处理,这个时候使用 AOP 的方式来写一个异常处理的方法更好(或者可以认为是 Exception filters 这样的自定义类)
-
注意事项:若要查看 ErrorBoundary 的效果,无法在开发模式下进行(即 mode=development)【或者直接运行 npm start,是不会出效果的,会显示具体的错误信息。】,有两种方法可以解决:
-
- 使用 npm run build 命令将项目打包后发布到服务器上进行;
-
- 将项目的模式从 development 调整为 production
- 需注意,若是使用 create-react-app 来创建的项目,配置文件需要解包后才能进行编辑,运行命令: npm run eject,解包后调整"/scripts/build.js",将 process.env.NODE_ENV 赋值为 production,之后再运行 npm start 的时候就能看到上面示例代码的效果。
-
this.setState()
-
用来设置 state 的值。
-
在 16.3 开始,FB 建议使用 setState 的异步函数写法,如:原先我们在使用的时候是直接进行赋值:
1
2
3
4
5inputHandler = (e) => {
this.setState({
inputValue: e.target.value //不再这么做
});
)
而是改用:
1
2
3
4
5
6
inputHandler = (e) => {
let value = e.target.value;
this.setState(() => {
return {inputValue: value}; //使用方法的形式,最终再返回一个对象,这里需要注意下,这么写是异步的,但存在一个问题,即输入的内容,需要再方法外层先获取到:如这里的value。
})
}
-
异步写法中,该方法提供了一个回调函数,通过该回调函数,可以确保只有等到 setState 触发完成之后,才会执行回调的方法(另外一个可以确保在 setState 执行完成之后再执行的点是 componentDidUpdate 方法),如:
1
2
3this.setState((prevState, props)=>{
...
}, callback);-
该方法提供了2个入参,prevState 和 props,前者相当于是 this.state。
-
只有当 setState 方法的第一个参数执行完成之后,才会执行 callback方法。
-
需要注意的是,因为 setState 是一个异步方法,所以在赋值的时候需要注意下,如果需要从表单或者其他地方获取值赋值给 state 的某一个属性,需要先把这个值在 setState 方法之前赋给一个变量,再在 setState 方法中使用这个变量。如
1
2
3
4
5
6let name = e.target.value;
this.setState((prevState, props)=>{
return {
userName : name //不能够直接 userName:e.target.value,异步方法中获取不到当前上下文
}
}, callback);
-
-
补充:对于 setState ,其不一定在调用 setState 的时候就立即触发这个动作。为了性能,react 会自行判断,将组件的所有 setState 在同一个时间点一同执行(批量执行),而非调用一次就执行一次。
-
每一次 setState 都会导致组件的再次渲染,除非 shouldComponentUpdate 返回 false。
this.forceUpdate()
-
调用forceUpdate()的时候,将会跳过 shouldComponentUpdate() 而直接重新 render() 组件。
-
父组件中调用 forceUpdate 亦会导致子组件的生命周期函数被触发(包括 componentDidUpdate)。
-
正常情况下,这个方法很少使用。