引导语
前面小节我们提到过,React 会把状态的变更更新到 UI,状态的变更过程必然会经历组件的生命周期。首先要知道所谓生命周期,就是组件从开始生成到最后消亡的过程, React 通常将组件生命周期分为三个阶段:装载、更新和卸载,我们怎么能确定组件进入到了哪个阶段呢?通过 React 组件暴露给我们的钩子函数就可以知晓。接下来我们将一起学习 React 组件的生命周期。
图解
生命周期,如同字面意思,即这个组件的整个“生命过程”,为了更形象的理解,我们将通过下面的图示来讲解。
React v16.3
大家看上面的图,先大致了解下在 React 中,各个阶段都做了什么事情。
React v16.4
通过两张图对比,我们可以看到 getDerivedStateFromProps
在 React v16.4 中有一定的改动,这个函数会在每次 render
之前被调用,也就意味着即使你的 props
没有任何变化,由父组件的 state
的改动导致的 render
,这个生命周期依然会被调用,使用的时候需要注意。
下面我们开始介绍组件这三个阶段中的生命周期方法以及其应用。
装载阶段
装载阶段组件被创建,然后组件实例插入到 DOM 中,完成组件的第一次渲染,该过程只会发生一次,在此阶段会依次调用以下这些方法:
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
constructor
组件的构造函数,第一个被执行,若没有显式定义它,会有一个默认的构造函数,但是若显式定义了构造函数,我们必须在构造函数中执行 super(props)
,否则无法在构造函数中拿到 this
,这是 ES6 类的继承的知识,具体可以参考阮一峰-ECMAScript 6 入门
在构造函数里一般会做两件事:
- 初始化组件的 state
- 给事件处理方法绑定 this
constructor(props) {
super(props);
// 不要在构造函数中调用 setState,可以直接给 state 设置初始值
this.state = { counter: 0 }
this.handleClick = this.handleClick.bind(this)
}
getDerivedStateFromProps
static getDerivedStateFromProps(props, state)
这是个静态方法,所以不能在这个函数里使用 this
,有两个参数 props
和 state
,分别指接收到的新参数和当前组件的 state
对象,这个函数会返回一个对象用来更新当前的 state
对象,如果不需要更新可以返回 null
。
该函数会在装载时,接收到新的 props
或者调用了 setState
和 forceUpdate
时被调用。如当我们接收到新的属性想修改我们的 state
,就可以使用。
// 当 props.counter 变化时,赋值给 state
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
static getDerivedStateFromProps(props, state) {
if (props.counter !== state.counter) {
return {
counter: props.counter
}
}
return null
}
handleClick = () => {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<h1 onClick={this.handleClick}>Hello, world!{this.state.counter}</h1>
</div>
)
}
}
现在我们可以显式传入 counter
,但是这里有个小问题,如果我们想要通过点击实现 state.counter
的增加,但这时会发现值不会发生任何变化,一直保持 props
传进来的值。这是由于在 React 16.4^ 的版本中 setState
和 forceUpdate
也会触发这个生命周期,所以当组件内部 state
变化后,就会重新走这个方法,同时会把 state
值赋值为 props
的值。因此我们需要多加一个字段来记录之前的 props
值,这样就会解决上述问题。具体如下:
// 这里只列出需要变化的地方
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
// 增加一个 preCounter 来记录之前的 props 传来的值
preCounter: 0,
counter: 0
}
}
static getDerivedStateFromProps(props, state) {
// 跟 state.preCounter 进行比较
if (props.counter !== state.preCounter) {
return {
counter: props.counter,
preCounter: props.counter
}
}
return null
}
// ... 跟上述列出的代码一样
}
render
React 中最核心的方法,一个组件中必须要有这个方法,它会根据状态 state
和属性 props
渲染组件。这个函数只做一件事,就是返回需要渲染的内容,所以不要在这个函数内做其他业务逻辑,通常调用该方法会返回以下类型中一个:
- React 元素:这里包括原生的 DOM 以及 React 组件;
- 数组和 Fragment(片段):可以返回多个元素;
- Portals(插槽):可以将子元素渲染到不同的 DOM 子树种;
- 字符串和数字:被渲染成 DOM 中的 text 节点;
- 布尔值或 null:不会渲染任何东西。
componentDidMount
组件装载之后调用,此时我们可以获取到 DOM 节点并操作,比如对 canvas,svg 的操作以及服务器请求等。在 componentDidMount
中调用 setState
会触发一次额外的渲染,多调用了一次 render
函数,由于它是在浏览器刷新屏幕前执行的,所以用户对此没有感知,但是我们应当在开发中避免这样使用,因为这样会带来一定的性能问题,我们尽量在 constructor
中初始化我们的 state
对象。
在组件装载之后,将计数数字变为1。
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
componentDidMount () {
this.setState({
counter: 1
})
}
render () {
return (
<div className="counter">
counter值: { this.state.counter }
</div>
)
}
}
可以看到 counter
的值变为了 1
。
更新阶段
当组件的 props
改变了,或组件内部调用了 setState/forceUpdate
,会触发更新重新渲染,这个过程可能会发生多次。这个阶段会依次调用下面这些方法:
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
getDerivedStateFromProps
这个方法在装载阶段已经讲过了,这里不再赘述,记住在更新阶段,无论接收到新的 props
,调用了 setState
或者 forceUpdate
,这个方法都会被触发。
shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState)
在讲这个生命周期函数之前,我们先来探讨两个问题:
-
setState 函数在任何情况下都会导致组件重新渲染吗?例如下面这种情况:
this.setState({number: this.state.number})
-
如果没有调用 setState,props 值也没有变化,是不是组件就不会重新渲染?
首先来解答下这两个问题,第一个问题答案是 会 ,第二个问题如果是父组件重新渲染时,不管传入的 props 有没有变化,都会引起子组件的重新渲染。那么有没有什么方法解决在这两个场景下不让组件重新渲染进而提升性能呢?这个时候
shouldComponentUpdate
登场了,这个生命周期函数是用来提升速度的,它是在重新渲染组件开始前触发的,默认返回true
,我们可以比较this.props
和nextProps
,this.state
和nextState
值是否变化,来确认返回 true 或者false
。当返回false
时,组件的更新过程停止,后续的render
、componentDidUpdate
也不会被调用。添加
shouldComponentUpdate
方法时,不建议使用深度相等检查(如使用JSON.stringify()
),因为深比较效率很低,可能会比重新渲染组件效率还低。而且该方法维护比较困难,建议使用该方法会产生明显的性能提升时使用。
render
同样更新阶段也会触发该方法,装载阶段已经介绍过,不再重复介绍。
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState)
这个方法在 render
之后,componentDidUpdate
之前调用,有两个参数 prevProps
和 prevState
,表示更新之前的 props
和 state
,这个函数必须要和 componentDidUpdate
一起使用,并且要有一个返回值,默认是 null
,这个返回值作为第三个参数传给 componentDidUpdate
。
这个生命周期的使用场景并不常见,下面介绍一个处理滚动条位置的例子:
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 增加了新的一条聊天消息
// 获取滚动条位置,以便我们之后调整
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// snapshot 是上个生命周期的返回值,当有新消息加入时,调整滚动条位置,使新消息及时显示出来
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot)
该方法在 getSnapshotBeforeUpdate
方法之后被调用,有三个参数,分别表示更新之前的 props
和 state
,以及上个方法的返回值。在上一小节中有介绍过,就不再重复赘述。在这个方法中我们可以操作 DOM,发起 http 请求,也可以 setState
更新状态,但注意这里使用 setState
要有条件,不然就会陷入死循环。
卸载阶段
卸载阶段只有一个生命周期函数,在这个阶段你可以执行一些清理工作,比如清除定时器,取消未完成的网络请求,或清理事件监听等。
componentWillUnmount
这个生命周期在一个组件被卸载和销毁之前被调用,因此你不应该再这个方法中使用 setState
,因为组件一旦被卸载,就不会再装载,也就不会重新渲染。
错误处理 componentDidCatch
React 16 提供了一个内置函数 componentDidCatch,如果 render() 函数抛出错误,则会触发该函数,打印错误日志,并且显示回退的用户界面。它的出现,解决了早期的 React 开发中,一个小的组件抛出错误将会破坏整个应用程序的情况。
小结
本小节主要讲了 React 组件在装载阶段、更新阶段以及卸载阶段中的生命周期函数,并且分别介绍了不同生命周期函数的用法和应用场景。需要注意的是,更新阶段的几个生命周期函数现阶段了解即可,在之后深入项目实战的时候再逐渐理解和掌握。还要提醒大家,只有类组件才有生命周期方法,函数组件没有生命周期方法。