NNNの博客

我所知道的es6种react生命周期

基本上所有的React组件的生命周期方法都可以被分割成四个阶段:初始化挂载阶段(mounting)更新阶段卸载阶段(unmounting)。让我们来看一下各个阶段。

初始化阶段

es6中我们用下面方法来分别定义this.props默认值和this.state的初始值阶段

1
2
3
4
5
6
7
8
9
10
constructor(props) {
super(props)
this.pageSize = 1
this.rowWidth = 300
this.container = null
}
static propTypes = {
list: PropTypes.array,
fetchCallHistory: PropTypes.func,
}

仅当存在constructor的时候必须调用super,如果没有,则不用
如果在你声明的组件中存在constructor,则必须要加super,举个栗子:

1
2
3
4
5
class MyClass extends React.component {
render(){
return <div>Hello { this.props.world }</div>;
}
}

这段代码是没有问题的,我们不需要去调用super,然而,如果在代码中存在consturctor,那必须调用:

1
2
3
4
5
6
class MyClass extends React.component {
constructor(){
console.log(this) //Error: 'this' is not allowed before super()
}
}

之所以会报错,是因为若不执行super,则this无法初始化。
也就是说,在ES6中的class语法中,有constructor就得有super(当然,子类也可以没有constructor)


仅当你想在constructor内使用props才将props传入super。React会自行props设置在组件的其他地方(以供访问)。
将props传入super的作用是可以使你在constructor内访问它:

1
2
3
4
5
6
7
class MyClass extends React.component{
constructor(props){
super();
console.log(this.props); // this.props is undefined
}
}

完善后:

1
2
3
4
5
6
7
class MyClass extends React.component{
constructor(props){
super(props);
console.log(this.props); // prints out whatever is inside props
}
}

如果你只是想在别处访问它,是不必传入props的,因为React会自动为你设置好:

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass extends React.component{
render(){
// There is no need to call `super(props)` or even having a constructor
// this.props is automatically set for you by React
// not just in render but another where else other than the constructor
console.log(this.props); // it works!
}
}

挂载阶段

componentWillMount

即将挂载。组件初始化阶段,dom还没有渲染到html文档里面。该方法在完成首次渲染之前被调用,这也是在render方法调用前可以修改组件state的最后一次机会。此阶段修改state不会引发渲染。在此期间一般不调用ajax,如果调用,相当于同步触发。

render

挂载阶段。组件实例化时在componentWillMount执行完成后就会被执行。react最重要的步骤,创建虚拟dom,进行diff算法,更新dom树都在此进行。此时就不能更改state了,如果用setState方法,会引起无限的报错。

componentDidMount

挂载完成。在子组件也都加载完毕后执行,在RN中就是指组件初始化加载完毕,在react中DOM渲染完成,此时就可以操作DOM了。

更新阶段

componentWillReceiveProps

当传递给组件的props发生改变时,组件的componentWillReceiveProps即会被触发调用,方法传递的参数的是发更更改的之后的props值(通常我们命名为nextProps)。在这个方法里,你可以通过this.props访问当前的属性值,可以通过nextProps访问即将更新的属性值,或者将它们进行对比,或者将它们进行计算,最终确定你需要更新的状态(state)并最终调用setState方法对状态进行更新。在这个钩子函数中调用setState方法并不会触发再一次渲染。但是每次变化,react不会去验证新旧props是否发生改变,所以如果需要在变化时做一些事情,务必要手动的进行比较。

shouldComponentUpdate(nextProps, nextState)

我们上面刚刚说过,React并不会对props进行深度比较,这对state也同样适用。所以即使props与state并未发生了更改,shouldComponentUpdate也会被再次调用,包括接下来的步骤componentWillUpdate、render、componentDidUpdate也都会再次运行一次。这很明显会给性能造成不小的伤害。所以这是react性能优化非常重要的一环。组件接受新的state或者props时调用,我们可以设置在此对比前后两个props和state是否相同,如果相同则返回false阻止更新,因为相同的属性状态一定会生成相同的dom树,这样就不需要创造新的dom树和旧的dom树进行diff算法对比,节省大量性能,尤其是在dom结构复杂的时候。此方法返回一个布尔值,且默认是true。但是我们也可以返回false,这样下面的(生命周期)方法将不会被调用:

  • componentWillUpdate()
  • render()
  • componentDidUpdate()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    shouldComponentUpdate(nextProps, nextState) {
    if (!_.isEqual(this.props, nextProps) || !_.isEqual(this.state,nextState))
    {
    return true
    } else {
    return false
    }
    }
    或者使用PureComponent。PureComponent使用的是浅拷贝对比,复杂的数据类型可能存在对不不出来的情况。

.isEqual和.isEmpty是 lodash 插件里面的函数

componentWillUpdate(nextProps, nextState)

componentWillUpdate在props或state改变或shouldComponentUpdate返回true后触发。不可在其中使用setState。

componentDidUpdate

在React更新DOM之后立刻被调用。可以在此方法里操作被更新过的DOM或者执行一些后置动作(action)。此方法有两个参数:

  1. prevProps:旧的属性
  2. prevState:旧的state
    除和Mount阶段类似,当组件进入componentDidUpdate阶段时意味着最新的原生DOM已经渲染完成并且可以通过refs进行访问。该函数会传入两个参数,分别是prevProps和prevState,顾名思义是之前的状态。你仍然可以通过this关键字访问当前的状态,因为可以访问原生DOM的关系,在这里也适用于做一些第三方需要操纵类库的操作。
    update阶段各个钩子函数的调用顺序也与mount阶段相似,尤其是componentDidUpdate,子组件的该钩子函数优先于父组件调用
    因为可以访问DOM的缘故,我们有可能需要在这个钩子函数里获取实际的元素样式,并且写入state中,比如你的代码可能会长这样:
    1
    2
    3
    4
    5
    componentDidUpdate(prevProps, prevState) {
    // BAD: DO NOT DO THIS!!!
    let height = ReactDOM.findDOMNode(this).offsetHeight;
    this.setState({ internalHeight: height });
    }

如果默认情况下你的shouldComponentUpdate()函数总是返回true的话,那么这样在componentDidUpdate里更新state的代码又会把我们带入无限render的循环中。如果你必须要这么做,那么至少应该把上一次的结果缓存起来,有条件的更新state:

1
2
3
4
5
6
7
componentDidUpdate(prevProps, prevState) {
// One possible fix...
let height = ReactDOM.findDOMNode(this).offsetHeight;
if (this.state.height !== height ) {
this.setState({ internalHeight: height });
}
}

注意:在shouldComponentUpdate和componentwillUpdate中切勿使用setState方法,会导致循环调用。这是因为如果在shouldComponentUpdate和componentWillUpdate中调用了setState,此时this._pendingStateQueue != null,则performUpdateIfNecessary方法就会调用updateComponent方法进行组件更新。但是updateComponent方法又会调用shouldComponentUpdate和componentWillUpdate,因此造成循环调用,使得浏览器内存占满后崩溃。

(this._pendingElement != null) {
1
2
3
4
5
6
7
ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
} else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
} else {
this._updateBatchNumber = null;
}
}

当从父组件接收到新的属性时:

当通过this.setState( )改变状态时:

卸载阶段

componentWillUnmount

当组件需要从DOM中移除时,即会触发这个钩子函数。这里没有太多需要注意的地方,在这个函数中通常会做一些“清洁”相关的工作

将已经发送的网络请求都取消掉
移除组件上DOM的Event Listener

React v16.3 版本新生命周期函数浅析

一个月前,React 官方正式发布了 v16.3 版本。新增了 Context API , getDerivedStateFromProps,getSnapshotBeforeUpdate的两个生命周期函数。提出在未来 v17.0 版本中即将移除三个生命周期函数 componentWillMount,componentWillReceiveProps,componentWillUpdate。

Context API

现在 react + redux 已经成为了开始一个 React 项目标配,其实 react 本身是可以使用 state 和 props 来管理数据的,如果对 redux 的不正确使用,可能会增加应用整体的复杂度及代码量。新旧Context解决的都是prop drilling问题,旧的context存在一些问题,如果某个组件shouldComponentUpdate返回的是false,那么就不会再继续执行。新的context api 采用声明式的写法,并且可以透过shouldComponentUpdate返回false的组件继续向下传播,以保证目标组件可以接收到顶层组件context值的更新。
新的context api 分为三个部分:

  • React.createContext 用于初始化一个Context
  • XXXContext.Provider作为顶层组件接收一个名为的value的prop,可以接收任意需要被放入Context 的字符串,数字,甚至是函数
  • XXXContext.Consumer作为目标组件可以出现在组件树的任意位置(在Provider之后),接收children prop,这里的children必须是一个函数(context=>())用来接收顶层传来的context

componentWillReceiveProps


更新由 props 决定的 state 及处理特定情况下的回调

在老版本的 React 中,如果组件自身的某个 state 跟其 props 密切相关的话,一直都没有一种很优雅的处理方式去更新 state,而是需要在 componentWillReceiveProps 中判断前后两个 props 是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的状态,但很多情况下,业务需求会要求从外部跳转至列表时,根据传入的某个值,直接定位到某个 Tab。
在新版本中,React 官方提供了一个更为简洁的生命周期函数:

1
static getDerivedStateFromProps(nextProps, prevState)

一个简单的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// before
componentWillReceiveProps(nextProps) {
if (nextProps.translateX !== this.props.translateX) {
this.setState({
translateX: nextProps.translateX,
});
}
}
// after
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.translateX !== prevState.translateX) {
return {
translateX: nextProps.translateX,
};
}
return null;
}

乍看下来这二者好像并没有什么本质上的区别,但是可以看出React 团队试图通过框架级别的 API 来约束或者说帮助开发者写出可维护性更佳的JavaScript代码。

1
2
3
4
5
6
7
8
9
10
11
// before
componentWillReceiveProps(nextProps) {
if (nextProps.isLogin !== this.props.isLogin) {
this.setState({
isLogin: nextProps.isLogin,
});
}
if (nextProps.isLogin) {
this.handleClose();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// after
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.isLogin !== prevState.isLogin) {
return {
isLogin: nextProps.isLogin,
};
}
return null;
}
componentDidUpdate(prevProps, prevState) {
if (!prevState.isLogin && this.props.isLogin) {
this.handleClose();
}

通常来讲,在 componentWillReceiveProps 中,我们一般会做以下两件事,一是根据 props 来更新 state,二是触发一些回调,如动画或页面跳转等。在老版本的 React 中,这两件事我们都需要在 componentWillReceiveProps 中去做。而在新版本中,官方将更新 state 与触发回调重新分配到了 getDerivedStateFromProps 与 componentDidUpdate 中,使得组件整体的更新逻辑更为清晰。而且在 getDerivedStateFromProps 中还禁止了组件去访问 this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去做其他一些让组件自身状态变得更加不可预测的事情。

在组件更新前读取 DOM 元素状态

另一个常见的 componentWillUpdate 的用例是在组件更新前,读取当前某个 DOM 元素的状态,并在 componentDidUpdate 中进行相应的处理。但在 React 开启异步渲染模式后,render 阶段和 commit 阶段之间并不是无缝衔接的,也就是说在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在
componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了或者 DOM 可能因为用户行为发生了变化。
为了解决上面提到的这个问题,React 提供了一个新的生命周期函数:

1
getSnapshotBeforeUpdate(prevProps, prevState)

与 componentWillUpdate 不同,getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。虽然 getSnapshotBeforeUpdate 不是一个静态方法,但我们也应该尽量使用它去返回一个值。这个值会随后被传入到 componentDidUpdate 中,然后我们就可以在 componentDidUpdate 中去更新组件的状态,而不是在 getSnapshotBeforeUpdate 中直接更新组件状态。
官方提供的一个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ScrollingList extends React.Component {
listRef = null;
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevProps.list.length < this.props.list.length) {
return (
this.listRef.scrollHeight - this.listRef.scrollTop
);
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
this.listRef.scrollTop =
this.listRef.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.setListRef}>
{/* ...contents... */}
</div>
);
}
setListRef = ref => {
this.listRef = ref;
};
}

升级方案

将现有的 componentWillUpdate 中的回调函数迁移至 componentDidUpdate。如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统一触发回调或更新状态。

小结

让我们从整体的角度再来看一下 React 这次生命周期函数调整前后的异同:
before

after

在第一张图中被红框圈起来的三个生命周期函数就是在新版本中即将被移除的。通过上述的两张图,我们可以清楚地看到将要被移除的三个生命周期函数都是在 render 之前会被调用到的。而根据原来的设计,在这三个生命周期函数中都可以去做一些诸如发送请求,setState 等包含副作用的事情。在老版本的 React 中,这样做也许只会带来一些性能上的损耗,但在 React 开启异步渲染模式之后,就无法再接受这样的副作用产生了。举一个 Git 的例子就是在开发者 commit 了 10 个文件更新后,又对当前或其他的文件做了另外的更新,但在 push 时却仍然只 push 了刚才 commit 的 10 个文件更新。这样就会导致提交记录与实际更新不符,如果想要避免这个问题,就需要保证每一次的文件更新都要经过 commit 阶段,再被提交到远端,而这也就是 React 在开启异步渲染模式之后要做到的。