[关闭]
@qinyun 2019-01-31T08:24:22.000000Z 字数 11272 阅读 1361

如何通俗易懂地向别人解释React生命周期方法

未分类


什么是生命周期方法?新的React16+生命周期方法是怎样的?你该如何直观地理解它们,以及为什么它们很有用?

生命周期方法到底是什么?

React组件都有自己的阶段。

如果要你“构建一个Hello World组件”,我相信你会这么做:

  1. class HelloWorld extends React.Component {
  2.    render() {
  3. return <h1> Hello World </h1> 
  4.    }
  5. }

在客户端渲染这个组件时,你最终可能会看到如下的视图:

在呈现这个视图之前,这个组件经历了几个阶段。这些阶段通常称为组件生命周期。

对于人类而言,我们会经历小孩、成人、老人阶段。而对于React组件而言,我们有挂载、更新和卸载阶段。

巧合的是,挂载一个组件就像将一个新生婴儿带到这个世界。这是组件第一次拥有了生命。组件正是在这个阶段被创建,然后被插入到DOM中。

这是组件经历的第一个阶段——挂载阶段。

但它并不会就这样结束了。React组件会“成长”,或者说组件会经历更新阶段。

如果React组件不经历更新阶段,它们将保持被创建时的状态。

大部分组件会被更新——无论是通过修改state还是props,也就是经历更新阶段。

组件经历的最后一个阶段是卸载阶段。

在这个阶段,组件会“死亡”。用React术语来描述,就是指从DOM中移除组件。

这些就是你需要了解的有关组件生命周期的一切。

对了,React组件还需要经历另一个阶段。有时候代码会无法运行或者某处出现了错误,这个时候组件正在经历错误处理阶段,就像人类去看医生。

现在,你了解了React组件的四个基本阶段或者说生命周期。

1.挂载——组件在这个阶段被创建然后被插入到DOM中;

2.更新——React组件“成长”;

3.卸载——最后阶段;

4.错误处理——有时候代码无法运行或某处出现了错误。

注意:React组件可能不会经历所有阶段。一个组件有可能在挂载后立即就被卸载——没有更新或错误处理。

了解各个阶段及其相关的生命周期方法

了解组件经历的各个阶段只是整个等式的一部分,另一部分是了解每个阶段所对应的方法。

这些方法就是众所周知的组件生命周期方法。

让我们来看看这4个阶段所对应的方法。

我们先来看一下挂载阶段的方法。

挂载生命周期方法

挂载阶段是指从组件被创建到被插入DOM的阶段。

这个阶段会调用以下几个方法(按顺序描述)。

1. constructor()

这是给组件“带来生命”时调用的第一个方法。

在将组件挂载到DOM之前会调用constructor方法。

通常,你会在constructor方法中初始化state和绑定事件处理程序。

这是一个简单的例子:

  1. const MyComponent extends React.Component {
  2.   constructor(props) {
  3.    super(props) 
  4.     this.state = {
  5.        points: 0
  6.     }  
  7.     this.handlePoints = this.handlePoints.bind(this) 
  8.     }   
  9. }

我相信你已经很熟悉这个方法了,所以我不打算进一步再做解释。

需要注意的是,这是第一个被调用的方法——在组件被挂载到DOM之前。

2. static getDerivedStateFromProps()

在解释这个生命周期方法之前,我先说明如何使用这个方法。

这个方法的基本结构如下所示:

  1. const MyComponent extends React.Component {
  2.   ... 
  3.   static getDerivedStateFromProps() {
  4.      //do stuff here
  5.   }  
  6. }

这个方法以props和state作为参数:

  1. ... 
  2.   static getDerivedStateFromProps(props, state) {
  3. //do stuff here
  4.   }  
  5. ...

你可以返回一个用于更新组件状态的对象:

  1. ... 
  2.   static getDerivedStateFromProps(props, state) { 
  3.      return {
  4.       points: 200 // update state with this
  5.      }
  6.   }  
  7.   ...

或者返回null,不进行更新:

  1. ... 
  2.   static getDerivedStateFromProps(props, state) {
  3.     return null
  4.   }  
  5. ...

你可能会想,这个生命周期方法很重要吗?它是很少使用的生命周期方法之一,但它在某些情况下会派上用场。

请记住,这个方法在组件被初始挂载到DOM之前调用。

下面是一个简单的例子:

假设有一个简单的组件,用于呈现足球队的得分。

得分被保存在组件的state对象中:

  1. class App extends Component {
  2.   state = {
  3.     points: 10
  4.   }
  5.   render() {
  6.     return (
  7.       <div className="App">
  8.         <header className="App-header">
  9.           <img src={logo} className="App-logo" alt="logo" />
  10.           <p>
  11.             You've scored {this.state.points} points.
  12.           </p>
  13.         </header>
  14.       </div>
  15.     );
  16.   }
  17. }

结果如下所示:

源代码可以在GitHub上获得:https://github.com/ohansemmanuel/points

假设你像下面这样在static getDerivedStateFromProps方法中放入其他分数,那么呈现的分数是多少?

  1. class App extends Component {
  2. state = {
  3. points: 10
  4. }
  5. // *******
  6. // NB: Not the recommended way to use this method. Just an example. Unconditionally overriding state here is generally considered a bad idea
  7. // ********
  8. static getDerivedStateFromProps(props, state) {
  9. return {
  10. points: 1000
  11. }
  12. }
  13. render() {
  14. return (
  15. <div className="App">
  16. <header className="App-header">
  17. <img src={logo} className="App-logo" alt="logo" />
  18. <p>
  19. You've scored {this.state.points} points.
  20. </p>
  21. </header>
  22. </div>
  23. );
  24. }
  25. }

现在我们有了static getDerivedStateFromProps组件生命周期方法。在将组件挂载到DOM之前这个方法会被调用。通过返回一个对象,我们可以在组件被渲染之前更新它的状态。

我们将看到:

1000来自static getDerivedStateFromProps方法的状态更新。

当然,这个例子主要是出于演示的目的,static getDerivedStateFromProps方法不应该被这么用。我这么做只是为了让你先了解这些基础知识。

我们可以使用这个生命周期方法来更新状态,但并不意味着必须这样做。static getDerivedStateFromProps方法有它特定的应用场景。

那么什么时候应该使用static getDerivedStateFromProps方法呢?

方法名getDerivedStateFromProps包含五个不同的单词:“Get Fromived State From Props”。

顾名思义,这个方法允许组件基于props的变更来更新其内部状态。

此外,以这种方式获得的组件状态被称为派生状态。

根据经验,应该谨慎使用派生状态,因为如果你不确定自己在做什么,很可能会向应用程序引入潜在的错误。

3. render()

在调用static getDerivedStateFromProps方法之后,下一个生命周期方法是render:

  1. class MyComponent extends React.Component {
  2. // render is the only required method for a class component
  3. render() {
  4. return <h1> Hurray! </h1>
  5. }
  6. }

如果要渲染DOM中的元素,可以在render方法中编写代码,即返回一些JSX。

你还可以返回纯字符串和数字,如下所示:

  1. class MyComponent extends React.Component {
  2. render() {
  3. return "Hurray"
  4. }
  5. }

或者返回数组和片段,如下所示:

  1. class MyComponent extends React.Component {
  2. render() {
  3. return [
  4. <div key="1">Hello</div>,
  5. <div key="2" >World</div>
  6. ];
  7. }
  8. }
  9. class MyComponent extends React.Component {
  10. render() {
  11. return <React.Fragment>
  12. <div>Hello</div>
  13. <div>World</div>
  14. </React.Fragment>
  15. }
  16. }

如果你不想渲染任何内容,可以在render方法中返回一个布尔值或null:

  1. class MyComponent extends React.Component {
  2. render() {
  3. return null
  4. }
  5. }
  6. class MyComponent extends React.Component {
  7. // guess what's returned here?
  8. render() {
  9. return (2 + 2 === 5) && <div>Hello World</div>;
  10. }
  11. }

你还可以从render方法返回一个portal:

  1. class MyComponent extends React.Component {
  2. render() {
  3. return createPortal(this.props.children, document.querySelector("body"));
  4. }
  5. }

关于render方法的一个重要注意事项是,不要在函数中调用setState或者与外部API发生交互。

4. componentDidMount()

在调用render后,组件被挂载到DOM,并调用componentDidMount方法。

在将组件被挂载到DOM之后会立即调用这个函数。

有时候你需要在组件挂载后立即从组件树中获取DOM节点,这个时候就可以调用这个组件生命周期方法。

例如,你可能有一个模态窗口,并希望在特定DOM元素中渲染模态窗口的内容,你可以这么做:

  1. class ModalContent extends React.Component {
  2. el = document.createElement("section");
  3. componentDidMount() {
  4. document.querySelector("body).appendChild(this.el);
  5. }
  6. // using a portal, the content of the modal will be rendered in the DOM element attached to the DOM in the componentDidMount method.
  7. }

如果你希望在组件被挂载到DOM后立即发出网络请求,可以在这个方法里进行:

  1. componentDidMount() {
  2. this.fetchListOfTweets() // where fetchListOfTweets initiates a netowrk request to fetch a certain list of tweets.
  3. }

你还可以设置订阅,例如计时器:

  1. // e.g requestAnimationFrame
  2. componentDidMount() {
  3. window.requestAnimationFrame(this._updateCountdown);
  4. }
  5. // e.g event listeners
  6. componentDidMount() {
  7. el.addEventListener()
  8. }

只需要确保在卸载组件时取消订阅,我们将在讨论componentWillUnmount生命周期方法时介绍更详细的内容。

挂载阶段基本上就是这样了,现在让我们来看看组件经历的下一个阶段——更新阶段。

更新生命周期方法

每当更改React组件的state或props时,组件都会被重新渲染。简单地说,就是组件被更新。这就是组件生命周期的更新阶段。

那么在更新组件时会调用哪些生命周期方法?

1. static getDerivedStateFromProps()

首先,还会调用static getDerivedStateFromProps方法。这是第一个被调用的方法。因为之前已经介绍过这个方法,所以这里不再解释。

需要注意的是,在挂载和更新阶段都会调用这个方法。

2. shouldComponentUpdate()

在调用static getDerivedStateFromProps方法之后,接下来会调用nextComponentUpdate方法。

默认情况下,或者在大多数情况下,在state或props发生变更时会重新渲染组件。不过,你也可以控制这种行为。

你可以在这个方法中返回一个布尔值——true或false,用于控制是否重新渲染组件。

这个生命周期方法主要用于优化性能。不过,如果state和props没有发生变更,不希望组件重新渲染,你也可以使用内置的PureComponent。

3. render()

在调用shouldComponentUpdate方法后,会立即调用render——具体取决于shouldComponentUpdate返回的值,默认为true。

4. getSnapshotBeforeUpdate()

在调用render方法之后,接下来会调用getSnapshotBeforeUpdatelifcycle方法。

你不一定会用到这个生命周期方法,但在某些特殊情况下它可能会派上用场,特别是当你需要在DOM更新后从中获取一些信息。

这里需要注意的是,getSnapshotBeforeUpdate方法从DOM获得的值将引用DOM更新之前的值,即使之前调用了render方法。

我们以使用git作为类比。

在编写代码时,你会在将代码推送到代码库之前暂存它们。

假设在将变更推送到DOM之前调用了render函数来暂存变更。因此,在实际更新DOM之前,getSnapshotBeforeUpdate获得的信息指向了DOM更新之前的信息。

对DOM的更新可能是异步的,但getSnapshotBeforeUpdate生命周期方法在更新DOM之前立即被调用。

如果你还是不太明白,我再举一个例子。

聊天应用程序是这个生命周期方法的一个典型应用场景。

我已经为之前的示例应用程序添加了聊天窗格。

可以看到右侧的窗格吗?

聊天窗格的实现非常简单,你可能已经想到了。在App组件中有一个带有Chats组件的无序列表:

  1. <ul className="chat-thread">
  2. <Chats chatList={this.state.chatList} />
  3. </ul>

Chats组件用于渲染聊天列表,为此,它需要一个chatList prop。基本上它就是一个数组,一个包含3个字符串的数组:[“Hey”, “Hello”, “Hi”]。

Chats组件的实现如下:

  1. class Chats extends Component {
  2. render() {
  3. return (
  4. <React.Fragment>
  5. {this.props.chatList.map((chat, i) => (
  6. <li key={i} className="chat-bubble">
  7. {chat}
  8. </li>
  9. ))}
  10. </React.Fragment>
  11. );
  12. }
  13. }

它只是通过映射chatList prop并渲染出一个列表项,而该列表项的样式看起来像气泡。

还有一个东西,在聊天窗格顶部有一个“Add Chat”按钮。

看到聊天窗格顶部的按钮了吗?

单击这个按钮将会添加新的聊天文本“Hello”,如下所示:

与大多数聊天应用程序一样,这里有一个问题:每当消息数量超过聊天窗口的高度时,预期的行为应该是自动向下滚动聊天窗格,以便看到最新的聊天消息。大现在的情况并非如此。

让我们看看如何使用getSnapshotBeforeUpdate生命周期方法来解决这个问题。

在调用getSnapshotBeforeUpdate方法时,需要将之前的props和state作为参数传给它。

我们可以使用prevProps和prevState参数,如下所示:

  1. getSnapshotBeforeUpdate(prevProps, prevState) {
  2. }

你可以让这个方法返回一个值或null:

  1. getSnapshotBeforeUpdate(prevProps, prevState) {
  2. return value || null // where 'value' is a valid JavaScript value
  3. }

无论这个方法返回什么值,都会被传给另一个生命周期方法。

getSnapshotBeforeUpdate生命周期方法本身不会起什么作用,它需要与componentDidUpdate生命周期方法结合在一起使用。

你先记住这个,让我们来看一下componentDidUpdate生命周期方法。

5. componentDidUpdate()

在调用getSnapshotBeforeUpdate之后会调用这个生命周期方法。与getSnapshotBeforeUpdate方法一样,它接收之前的props和state作为参数:

  1. componentDidUpdate(prevProps, prevState) {
  2. }

但这并不是全部。

无论从getSnapshotBeforeUpdate生命周期方法返回什么值,返回值都将被作为第三个参数传给componentDidUpdate方法。

我们姑且把返回值叫作snapshot,所以:

  1. componentDidUpdate(prevProps, prevState, snapshot) {
  2. }

有了这些,接下来让我们来解决聊天自动滚动位置的问题。

要解决这个问题,我需要提醒(或教导)你一些DOM几何学知识。

下面是保持聊天窗格滚动位置所需的代码:

  1. getSnapshotBeforeUpdate(prevProps, prevState) {
  2. if (this.state.chatList > prevState.chatList) {
  3. const chatThreadRef = this.chatThreadRef.current;
  4. return chatThreadRef.scrollHeight - chatThreadRef.scrollTop;
  5. }
  6. return null;
  7. }
  8. componentDidUpdate(prevProps, prevState, snapshot) {
  9. if (snapshot !== null) {
  10. const chatThreadRef = this.chatThreadRef.current;
  11. chatThreadRef.scrollTop = chatThreadRef.scrollHeight - snapshot;
  12. }
  13. }

这是聊天窗口:

下图突出显示了保存聊天消息的实际区域(无序列表ul)。

我们在ul中添加了React Ref:

  1. <ul className="chat-thread" ref={this.chatThreadRef}>
  2. ...
  3. </ul>

首先,因为getSnapshotBeforeUpdate可以通过任意数量的props或state更新来触发更新,我们将通过一个条件来判断是否有新的聊天消息:

  1. getSnapshotBeforeUpdate(prevProps, prevState) {
  2. if (this.state.chatList > prevState.chatList) {
  3. // write logic here
  4. }
  5. }

getSnapshotBeforeUpdate必须返回一个值。如果没有添加新聊天消息,就返回null:

  1. getSnapshotBeforeUpdate(prevProps, prevState) {
  2. if (this.state.chatList > prevState.chatList) {
  3. // write logic here
  4. }
  5. return null
  6. }

现在看一下getSnapshotBeforeUpdate方法的完整代码:

  1. getSnapshotBeforeUpdate(prevProps, prevState) {
  2. if (this.state.chatList > prevState.chatList) {
  3. const chatThreadRef = this.chatThreadRef.current;
  4. return chatThreadRef.scrollHeight - chatThreadRef.scrollTop;
  5. }
  6. return null;
  7. }

我们先考虑一种情况,即所有聊天消息的高度不超过聊天窗格的高度。

表达式chatThreadRef.scrollHeight - chatThreadRef.scrollTop等同于chatThreadRef.scrollHeight - 0。

这个表达式的值将等于聊天窗格的scrollHeight——在将新消息插入DOM之前的高度。

之前我们已经解释过,从getSnapshotBeforeUpdate方法返回的值将作为第三个参数传给componentDidUpdate方法,也就是snapshot:

  1. componentDidUpdate(prevProps, prevState, snapshot) {
  2. }

这个值是更新DOM之前的scrollHeight。

componentDidUpdate方法有以下这些代码,但它们有什么作用呢?

  1. componentDidUpdate(prevProps, prevState, snapshot) {
  2. if (snapshot !== null) {
  3. const chatThreadRef = this.chatThreadRef.current;
  4. chatThreadRef.scrollTop = chatThreadRef.scrollHeight - snapshot;
  5. }
  6. }

实际上,我们以编程方式从上到下垂直滚动窗格,距离等于chatThreadRef.scrollHeight - snapshot;。

由于snapshot是指更新前的scrollHeight,上述的表达式将返回新聊天消息的高度,以及由于更新而导致的任何其他相关高度。请看下图:

当整个聊天窗格高度被消息占满(并且已经向上滚动一点)时,getSnapshotBeforeUpdate方法返回的snapshot值将等于聊天窗格的实际高度。

componentDidUpdate将scrollTop值设置为额外消息高度的总和,这正是我们想要的。

卸载生命周期方法

在组件卸载阶段会调用下面这个方法。

componentWillUnmount()

在卸载和销毁组件之前会调用componentWillUnmount生命周期方法。这是进行资源清理最理想的地方,例如清除计时器、取消网络请求或清理在componentDidMount()中创建的任何订阅,如下所示:

  1. // e.g add event listener
  2. componentDidMount() {
  3. el.addEventListener()
  4. }
  5. // e.g remove event listener
  6. componentWillUnmount() {
  7. el.removeEventListener()
  8. }

错误处理生命周期方法

有时候组件会出现问题,会抛出错误。当后代组件(即组件下面的组件)抛出错误时,将调用下面的方法。

让我们实现一个简单的组件来捕获演示应用程序中的错误。为此,我们将创建一个叫作ErrorBoundary的新组件。

这是最基本的实现:

  1. import React, { Component } from 'react';
  2. class ErrorBoundary extends Component {
  3.   state = {};
  4.   render() {
  5.     return null;
  6.   }
  7. }
  8. export default ErrorBoundary;

static getDerivedStateFromError()

当后代组件抛出错误时,首先会调用这个方法,并将抛出的错误作为参数。

无论这个方法返回什么值,都将用于更新组件的状态。

让ErrorBoundary组件使用这个生命周期方法:

  1. import React, { Component } from "react";
  2. class ErrorBoundary extends Component {
  3.   state = {};
  4.   static getDerivedStateFromError(error) {
  5.     console.log(`Error log from getDerivedStateFromError: ${error}`);
  6.     return { hasError: true };
  7.   }
  8.   render() {
  9.     return null;
  10.   }
  11. }
  12. export default ErrorBoundary;

现在,只要后代组件抛出错误,错误就会被记录到控制台,并且getDerivedStateFromError方法会返回一个对象,这个对象将用于更新ErrorBoundary组件的状态。

componentDidCatch()

在后代组件抛出错误之后,也会调用componentDidCatch方法。除了抛出的错误之外,还会有另一个参数,这个参数包含了有关错误的更多信息:

  1. componentDidCatch(error, info) {
  2. }

在这个方法中,你可以将收到的error或info发送到外部日志记录服务。与getDerivedStateFromError不同,componentDidCatch允许包含会产生副作用的代码:

  1. componentDidCatch(error, info) {
  2. logToExternalService(error, info) // this is allowed. 
  3.         //Where logToExternalService may make an API call.
  4. }

让ErrorBoundary组件使用这个生命周期方法:

  1. import React, { Component } from "react";
  2. class ErrorBoundary extends Component {
  3.   state = { hasError: false };
  4.   static getDerivedStateFromError(error) {
  5.     console.log(`Error log from getDerivedStateFromError: ${error}`);
  6.     return { hasError: true };
  7.   }
  8.   componentDidCatch(error, info) {
  9.     console.log(`Error log from componentDidCatch: ${error}`);
  10.     console.log(info);
  11.   }
  12.   render() {
  13.     return null
  14.   }
  15. }
  16. export default ErrorBoundary;

此外,由于ErrorBoundary只能捕捉后代组件抛出的错误,因此我们将让组件渲染传进来的Children,或者在出现错误时呈现默认的错误UI:

  1. ... 
  2. render() {
  3.     if (this.state.hasError) {
  4.       return <h1>Something went wrong.</h1>;
  5.     }
  6.     return this.props.children;
  7.  }

英文原文:https://blog.logrocket.com/the-new-react-lifecycle-methods-in-plain-approachable-language-61a2105859f3

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注