转自 Ant Design 实战教程:https://www.yuque.com/ant-design/course/lifemethods

你有没有遇到过这样的问题:

  • 组件的生命周期有哪些?为什么要有生命周期函数?
  • 我应该什么时候去获取后台数据? 为什么很多教程都推荐用 componentDidMount? 用 componentWillMount 会有什么问题?
  • 为什么 setState 写在这里造成了重复渲染多次?
  • setState 在这里写合适吗?

读完本文希望你能对 React 的组件生命周期有一定的了解,编写 React 代码的时候能够更加得心应手,注意本文的生命周期讲的主要是浏览器端渲染,这是后端和全栈的主要使用方式,服务端渲染有些不一样,请注意区分,我们会在文中进行简单说明。

Update: 更新为 React16 版本,React16 由于异步渲染等特性会让之前的一些方法如 componentWillMount 变得不够安全高效逐步废弃,详见Legacy Methods


生命周期

如果你做过安卓开发方面的编程,那么你应该了解 onCreate,onResume,onDestrory 等常见生命周期方法,生命周期函数说白了就是让我们在一个组件的各个阶段都提供一些钩子函数来让开发者在合适的时间点可以介入并进行一些操作,比如初始化(onCreate)的时候我们应该初始化组件相关的状态和变量,组件要销毁(onDestrory)时,我们应该把一些数据结构销毁掉以节约内存,防止后台任务一直运行。在 java 类中也存在一个最常见的钩子函数 contructor,你可以在这里调用 super 方法初始化父类,也可以在这里初始化各种变量。

我们先看下下面的图建立一个 React 组件生命周期的直观认识,图为 React 16 的生命周期,总的来说 React 组件的生命周期分为三个部分: 装载期间(Mounting)更新期间(Updating)卸载期间(Unmounting) ,React16 多出来一个componentDidCatch() 函数用于捕捉错误。知道什么时候去使用哪些生命周期函数对于掌握和理解 React 是非常重要的,你可以看到这些生命周期函数有一定的规律,比如在某件事情发生之前调用的会用 xxxWillxxx,而在这之后发生的会用 xxxDidxxx。

// 图来源于网络(侵删)

image.png | left | 747x414

接下来我们就这三个阶段分别介绍一下各个生命周期函数,详细的生命周期函数解释可以看官方文档 React.Component


装载期间

组件被实例化并挂载在到 DOM 树这一过程称为装载,在装载期调用的生命周期函数依次为


constructor(props)

构造函数,用于初始化这个组件的一些状态和操作,如果你是通过继承React.Component子类来创建 React 的组件的,那么你应当首先调用super(props) 初始化父类。

在 contructor 函数中,你可以初始化 state,比如this.state = {xxx};,不要在构造函数中使用 setState()函数,强行使用的话 React 会报错。其次你可以在构造函数中进行函数 bind,如:

1
this.handleClick = this.handleClick.bind(this);

一个示例 contructor 实现如下:

1
2
3
4
5
6
7
8
constructor(props) {
super(props);
this.state = {
color: '#fff'
};

this.handleClick = this.handleClick.bind(this);
}

如果你不需要初始化状态也不需要绑定 handle 函数的 this,那么你可以不实现 constructor 函数,由默认实现代替。


关于 bind 函数的解释说明

注意 js 的 this 指向比较特殊,比如以下的例子作为 onClick 回调函数由 button 组件去调用的时候不会把组件类的上下文带过去。

1
2
3
4
5
handleClick() {
console.log('handleClick', this); // undefined
}
...
<button onClick={this.handleClick}>click</button>

这种问题推荐三种可能的解决方式,其核心均为将函数的 this 强制绑定到组件类上:
1. 就是上面说的在 constructor 函数中显示调用 bind。
2. 在 onClick 的时候进行 bind: <button onClick = {this.handleClick.bind(this)} >,这种方式的劣势是每次调用的时候都需要进行 bind,优势是方便传参,处理函数需要传参可以参考 React 的文档 Passing Arguments to Event Handlers
3. 声明函数时使用箭头匿名函数,箭头函数会自动设置 this 为当前类。(简洁有效,墙裂推荐)

1
2
3
handleClick = () => {
console.log('handleClick', this); // Component
};

getDerivedStateFromProps()

这个函数会在 render 函数被调用之前调用,包括第一次的初始化组件以及后续的更新过程中,每次接收新的 props 之后都会返回一个对象作为新的 state,返回 null 则说明不需要更新 state。

该方法主要用来替代componentWillReceiveProps 方法,willReceiveProps 经常被误用,导致了一些问题,因此在新版本中被标记为 unsafe。以掘金上的 🌰 为例,componentWillReceiveProps 的常见用法如下,根据传进来的属性值判断是否要 load 新的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ExampleComponent extends React.Component {
state = {
isScrollingDown: false,
};

componentWillReceiveProps(nextProps) {
if (this.props.currentRow !== nextProps.currentRow) {
// 检测到变化后更新状态、并请求数据
this.setState({
isScrollingDown: nextProps.currentRow > this.props.currentRow,
});
this.loadAsyncData();
}
}

loadAsyncData() {
/* ... */
}
}

但这个方法的一个问题是外部组件多次频繁更新传入多次不同的 props,而该组件将这些更新 batch 后仅仅触发单次自己的更新,这种写法会导致不必要的异步请求,相比下来getDerivedStateFromProps 配合 componentDidUpdate 的写法如下:

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
class ExampleComponent extends React.Component {
state = {
isScrollingDown: false,
lastRow: null,
};

static getDerivedStateFromProps(nextProps, prevState) {
// 不再提供 prevProps 的获取方式
if (nextProps.currentRow !== prevState.lastRow) {
return {
isScrollingDown: nextProps.currentRow > prevState.lastRow,
lastRow: nextProps.currentRow,
};
}

// 默认不改动 state
return null;
}

componentDidUpdate() {
// 仅在更新触发后请求数据
this.loadAsyncData();
}

loadAsyncData() {
/* ... */
}
}

这种方式只在更新触发后请求数据,相比下来更节省资源。

注意getDerivedStateFromProps 是一个 static 方法,意味着拿不到实例的 this


render()

该方法在一个 React 组件中是必须实现的,你可以看成是一个 java interface 的接口

这是 React 组件的核心方法,用于根据状态 state 和属性 props 渲染一个 React 组件。我们应该保持该方法的纯洁性,这会让我们的组件更易于理解,只要 state 和 props 不变,每次调用 render 返回的结果应当相同,所以请不要在 render 方法中改变组件状态,也不要在在这个方法中和浏览器直接交互

如果你 React 如何使用 render 方法原理不太了解,可以阅读文章《当 React 来敲门》。


componentDidMount()

componentDidMount 方法会在 render 方法之后立即被调用,该方法在整个 React 生命周期中只会被调用一次。React 的组件树是一个树形结构,此时你可以认为这个组件以及他下面的所有子组件都已经渲染完了,所以在这个方法中你可以调用和真实 DOM 相关的操作了。

有些组件的启动工作是依赖 DOM 的,例如动画的启动,而  componentWillMount  的时候组件还没挂载完成,所以没法进行这些启动工作,这时候就可以把这些操作放在  componentDidMount  当中。

我们推荐可以在这个函数中发送异步请求,在回调函数中调用 setState()设置 state,等数据到达后触发重新渲染。但注意尽量不要在这个函数中直接调用setState()设置状态,这会触发一次额外的重新渲染,可能造成性能问题。

下面的代码演示了如何在 componentDidMount 加载数据并设置状态:

1
2
3
4
5
6
7
8
9
10
11
12
componentDidMount() {
console.log('componentDidMount');
fetch("https://api.github.com/search/repositories?q=language:java&sort=stars")
.then(res => res.json())
.then((result) => {
this.setState({ // 触发render
items: result.items
});
})
.catch((error) => { console.log(error)});
// this.setState({color: xxx}) // 不要这样做
}

更新期间

当组件的状态或属性变化时会触发更新,更新过程中会依次调用以下方法:


shouldComponentUpdate(nextProps, nextState)

你可以用这个方法来告诉 React 是否要进行下一次 render(),默认这个函数放回 true,即每次更新状态和属性的时候都进行组件更新。注意这个函数如果返回 false 并不会导致子组件也不更新。

这个钩子函数**一般不需要实现, **如果你的组件性能比较差或者渲染比较耗时,你可以考虑使React.PureComponent 重新实现该组件,PureComponent 默认实现了一个版本的 shouldComponentUpdate 会进行 state 和 props 的比较。当然如果你有自信,可以自己实现比较 nextProps 和 nextState 是否发生了改变。

该函数通常是优化性能的紧急出口,是个大招,不要轻易用,如果要用可以参考Immutable 详解及 React 中实践 .


getSnapshotBeforeUpdate()

该方法的触发时间为update 发生的时候,在 render 之后 dom 渲染之前返回一个值,作为 componentDidUpdate 的第三个参数。该函数与 componentDidUpdate 一起使用可以取代 componentWillUpdate 的所有功能,比如以下是官方的例子:

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
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}

getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}

componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}

render() {
return <div ref={this.listRef}>{/* ...contents... */}</div>;
}
}

componentDidUpdate(prevProps, prevState, snapshot)

该方法会在更新完成后被立即调用,你可以在这个方法中进行DOM 操作,或者做一些异步调用。这个和首次装载过程后调用 componentDidMount 是类似的,不一样的是你可能需要判断下属性是否变化了再发起网络请求,如:

1
2
3
4
5
6
7
8
componentDidUpdate(prevProps) { // 来自网络
if(prevProps.myProps !== this.props.myProp) {
// this.props.myProp has a different value
// we can perform any operations that would
// need the new value and/or cause side-effects
// like AJAX calls with the new value - this.props.myProp
}
}

卸载期间

卸载期间是指组件被从 DOM 树中移除时,调用的相关方法为:

componentWillUnmount()

该方法会在组件被卸载之前被调用,你可以在这个函数中进行相关清理工作,比如删除定时器。

下面给个示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
componentWillUnmount() {
console.log('componentWillUnmount');

// 清除timer
clearInterval(this.timerID1);
clearTimeout(this.timerID2);

// 关闭socket
this.myWebsocket.close();

// 取消消息订阅...
}

错误捕获

React16 中新增了一个生命周期函数:

componentDidCatch(error, info)

在 react 组件中如果产生的错误没有被被捕获会被抛给上层组件,如果上层也不处理的话就会抛到顶层导致浏览器白屏错误,在 React16 中我们可以实现这个方法来捕获子组件产生的错误,然后在父组件中妥善处理,比如搞个弹层通知用户网页崩溃等。

在这个函数中请只进行错误恢复相关的处理,不要做其他流程控制方面的操作。比如:

1
2
3
4
5
6
componentDidCatch(error, info) { // from react.org
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}

React16 中的生命周期函数变化

componentWillMount,componentWillUpdate, componentWillReceiveProps 等生命周期方法在下个主版本中会被废弃?

根据这份RFC,是的,这些生命周期方法被认为是不安全的,在 React16 中被重命名为 UNSAFE_componentWillMount,UNSAFE_componentWillUpdate,UNSAFE_componentWillReceiveProps,而在更下个大版本中他们会被废弃。详见 React 16.3 版本发布公告


总结

总结一下,以上讲的这些生命周期都有自己存在的意义,但在 React 使用过程中我们最常用到的生命周期函数是如下几个:

  • constructor: 初始化状态,进行函数绑定
  • componentDidMount: 进行 DOM 操作,进行异步调用初始化页面
  • componentWillReceiveProps: 根据 props 更新状态
  • componentWillUnmount: 清理组件定时器,网络请求或者相关订阅等

参考文章