Ignite

React生命周期管理

2018-10-31

你有没有遇到过这样的问题: setState应该写在哪里?我们应该什么时候去获取后台的数据?怎样减少页面不必要的渲染?带着问题我们一起来回顾一下React生命周期那些知识点

生命周期

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

code

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

挂载(4个)

当组件实例创建并插入 DOM 时,其生命周期调用顺序如下:

constructor(props)

  • 如果不初始化 state 或不进行方法绑定, 则不需要为 React 组件实现构造函数。在 React 组件挂载之前,会调用他的构造函数。再为 React.Component 子类 实现构造函数时,需要首先调用 super(props),否则,this.props 在构造函数中出现未定义的 bug。
  • 构造函数主要是初始化 state 和为 事件处理函数 绑定实例。
  • 在 constructor 内不要调用 setState(),也不要做任何订阅。

一个示例 constructor 实现如下:

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函数,由默认实现代替。

static getDerivedStateFromProps(props, state)

这个函数会在每次渲染前都调用,让组件在 props 变化时更新 state ,返回null则说明不需要更新state。

该方法主要用来替代 componentWillReceiveProps 方法,willReceiveProp s经常被误用,导致了一些问题,因此在新版本中被标记为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
29
30
31
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 组件中是必须实现的

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

componentDidMount()

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

这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

我们可以在 componentDidMount() 里直接调用 setState()。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。请谨慎使用该模式,因为它会导致性能问题。通常,你应该在 constructor() 中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modals 等情况下,你可以使用此方式处理。

下面的代码演示了如何在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}) // 不要这样做
}

更新(5个)

当组件的 props 和 state 发生变化的时候会触发更新

shouldComponentUpdate(nextProps, nextState)

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

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

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

getSnapshotBeforeUpdate(prevProps, prevState)

该方法的触发时间为 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
31
32
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}

getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动​​位置以便我们稍后调整滚动位置。
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 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}

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

在上述示例中,重点是从 getSnapshotBeforeUpdate 读取 scrollHeight 属性,
因为 “render” 阶段生命周期(如 render)和
“commit” 阶段生命周期(如 getSnapshotBeforeUpdate 和 componentDidUpdate)之间可能存在延迟。

componentDidUpdate(prevProps, prevState, snapshot)

该方法会在更新后会被立即调用。首次渲染不会执行此方法。

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。

如果组件实现了 getSnapshotBeforeUpdate() 生命周期(不常用),则它的返回值将作为 componentDidUpdate() 的第三个参数 “snapshot” 参数传递。否则此参数将为 undefined。

1
2
3
4
5
6
7
8
componentDidUpdate(prevProps) {
if(prevProps.myProps !== this.props.myProp) {
// 典型用法(不要忘记比较 props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
}

注意:如果 shouldComponentUpdate() 返回值为 false,则不会调用 componentDidUpdate()。

卸载(1个)

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

componentWillUnmount()

该方法会在组件被卸载之前被调用。你可以在这个函数中进行相关清理工作,比如删除定时器之类的,取消网络请求或清除在 componentDidMount() 中创建的订阅等

下面给个示例代码:

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
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显示降级 UI
return { hasError: true };
}

componentDidCatch(error, info) {
// "组件堆栈" 例子:
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
logComponentStackToMyService(info.componentStack);
}

render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的降级 UI
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}

React16中的生命周期函数变化

componentWillMount(直接在 componentDidMount 中即可),componentWillUpdate( componentDidUpdate + getSnapshotBeforeUpdate ),
componentWillReceiveProps( getDerivedStateFromProps + componentDidUpdate ) 等生命周期方法在下个主版本中会被废弃?
根据这份RFC,是的,这些生命周期方法被认为是不安全的,在React16中被重命名为 UNSAFE_componentWillMount,UNSAFE_componentWillUpdate,UNSAFE_componentWillReceiveProps,而在更下个大版本中他们会被废弃。详见React 16.3版本发布公告

为什么废弃?从上面的生命周期的图中可以看出,被废弃的三个函数都是在render之前,因为fiber的出现,很可能因为高优先级任务的出现而打断现有任务导致它们会被执行多次。

React v16.3 版本新生命周期函数浅析及升级方案

总结

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

  • constructor: 初始化状态,进行函数绑定
  • componentDidMount: 进行DOM操作,进行异步调用初始化页面
  • componentWillReceiveProps: 根据props更新状态
  • componentWillUnmount: 清理组件定时器,网络请求或者相关订阅等
    其他的逻辑一般和用户的操作有关(各种handleClickXXXX),当然需要用到其他生命周期函数可以按需正确使用。如果阅读文章过程中遇到问题欢迎评论进行修正。
Tags: React
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章