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

react

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

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

React组件都有自己的阶段。

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

class HelloWorld extends React.Component {   render() {\treturn \u0026lt;h1\u0026gt; Hello World \u0026lt;/h1\u0026gt;    }}

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

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

对于人类而言,我们会经历小孩、成人、老人阶段。而对于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和绑定事件处理程序。

这是一个简单的例子:

const MyComponent extends React.Component {  constructor(props) {   super(props)     this.state = {       points: 0    }      this.handlePoints = this.handlePoints.bind(this)     }   }

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

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

2. static getDerivedStateFromProps()

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

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

const MyComponent extends React.Component {  ...   static getDerivedStateFromProps() {     //do stuff here  }  }

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

...   static getDerivedStateFromProps(props, state) {\t//do stuff here  }  ...

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

...   static getDerivedStateFromProps(props, state) {      return {     \tpoints: 200 // update state with this     }  }    ...

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

...   static getDerivedStateFromProps(props, state) {    return null  }  ...

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

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

下面是一个简单的例子:

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

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

class App extends Component {  state = {    points: 10  }  render() {    return (      \u0026lt;div className=\u0026quot;App\u0026quot;\u0026gt;        \u0026lt;header className=\u0026quot;App-header\u0026quot;\u0026gt;          \u0026lt;img src={logo} className=\u0026quot;App-logo\u0026quot; alt=\u0026quot;logo\u0026quot; /\u0026gt;          \u0026lt;p\u0026gt;            You've scored {this.state.points} points.          \u0026lt;/p\u0026gt;        \u0026lt;/header\u0026gt;      \u0026lt;/div\u0026gt;    );  }}

结果如下所示:

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

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

class App extends Component {  state = {    points: 10  }\t  // *******  //  NB: Not the recommended way to use this method. Just an example. Unconditionally overriding state here is generally considered a bad idea  // ********  static getDerivedStateFromProps(props, state) {    return {      points: 1000    }  }    render() {    return (      \u0026lt;div className=\u0026quot;App\u0026quot;\u0026gt;        \u0026lt;header className=\u0026quot;App-header\u0026quot;\u0026gt;          \u0026lt;img src={logo} className=\u0026quot;App-logo\u0026quot; alt=\u0026quot;logo\u0026quot; /\u0026gt;          \u0026lt;p\u0026gt;            You've scored {this.state.points} points.          \u0026lt;/p\u0026gt;        \u0026lt;/header\u0026gt;      \u0026lt;/div\u0026gt;    );  }}

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

我们将看到:

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

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

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

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

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

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

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

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

3. render()

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

class MyComponent extends React.Component {\t// render is the only required method for a class component    render() {\treturn \u0026lt;h1\u0026gt; Hurray! \u0026lt;/h1\u0026gt;   }}

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

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

class MyComponent extends React.Component {   render() {\treturn \u0026quot;Hurray\u0026quot;    }}

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

class MyComponent extends React.Component {   render() {    return [          \u0026lt;div key=\u0026quot;1\u0026quot;\u0026gt;Hello\u0026lt;/div\u0026gt;,           \u0026lt;div key=\u0026quot;2\u0026quot; \u0026gt;World\u0026lt;/div\u0026gt;      ];   }}class MyComponent extends React.Component {   render() {\treturn \u0026lt;React.Fragment\u0026gt;        \t\u0026lt;div\u0026gt;Hello\u0026lt;/div\u0026gt;        \t\u0026lt;div\u0026gt;World\u0026lt;/div\u0026gt;      \u0026lt;/React.Fragment\u0026gt;   }}

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

class MyComponent extends React.Component {    render() {\treturn null   }}class MyComponent extends React.Component {  // guess what's returned here?   render() {    return (2 + 2 === 5) \u0026amp;\u0026amp; \u0026lt;div\u0026gt;Hello World\u0026lt;/div\u0026gt;;  }}

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

class MyComponent extends React.Component {  render() {    return createPortal(this.props.children, document.querySelector(\u0026quot;body\u0026quot;));  }}

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

4. componentDidMount()

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

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

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

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

class ModalContent extends React.Component {  el = document.createElement(\u0026quot;section\u0026quot;);  componentDidMount() {    document.querySelector(\u0026quot;body).appendChild(this.el);  }    // using a portal, the content of the modal will be rendered in the DOM element attached to the DOM in the componentDidMount method. }

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

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

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

// e.g requestAnimationFrame componentDidMount() {    window.requestAnimationFrame(this._updateCountdown); }// e.g event listeners componentDidMount() {\tel.addEventListener()}

只需要确保在卸载组件时取消订阅,我们将在讨论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组件的无序列表:

\u0026lt;ul className=\u0026quot;chat-thread\u0026quot;\u0026gt;    \u0026lt;Chats chatList={this.state.chatList} /\u0026gt; \u0026lt;/ul\u0026gt;

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

Chats组件的实现如下:

class Chats extends Component {  render() {    return (      \u0026lt;React.Fragment\u0026gt;        {this.props.chatList.map((chat, i) =\u0026gt; (          \u0026lt;li key={i} className=\u0026quot;chat-bubble\u0026quot;\u0026gt;            {chat}          \u0026lt;/li\u0026gt;        ))}      \u0026lt;/React.Fragment\u0026gt;    );  }}

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

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

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

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

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

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

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

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

getSnapshotBeforeUpdate(prevProps, prevState) {   }

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

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

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

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

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

5. componentDidUpdate()

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

componentDidUpdate(prevProps, prevState) { }

但这并不是全部。

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

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

componentDidUpdate(prevProps, prevState, snapshot) { }

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

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

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

getSnapshotBeforeUpdate(prevProps, prevState) {    if (this.state.chatList \u0026gt; prevState.chatList) {      const chatThreadRef = this.chatThreadRef.current;      return chatThreadRef.scrollHeight - chatThreadRef.scrollTop;    }    return null;  }  componentDidUpdate(prevProps, prevState, snapshot) {    if (snapshot !== null) {      const chatThreadRef = this.chatThreadRef.current;      chatThreadRef.scrollTop = chatThreadRef.scrollHeight - snapshot;    }  }

这是聊天窗口:

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

我们在ul中添加了React Ref:

\u0026lt;ul className=\u0026quot;chat-thread\u0026quot; ref={this.chatThreadRef}\u0026gt;   ...\u0026lt;/ul\u0026gt;

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

getSnapshotBeforeUpdate(prevProps, prevState) {    if (this.state.chatList \u0026gt; prevState.chatList) {      // write logic here    }  }

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

getSnapshotBeforeUpdate(prevProps, prevState) {    if (this.state.chatList \u0026gt; prevState.chatList) {      // write logic here    }      return null }

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

getSnapshotBeforeUpdate(prevProps, prevState) {    if (this.state.chatList \u0026gt; prevState.chatList) {      const chatThreadRef = this.chatThreadRef.current;      return chatThreadRef.scrollHeight - chatThreadRef.scrollTop;    }    return null;  }

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

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

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

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

componentDidUpdate(prevProps, prevState, snapshot) {     }

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

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

componentDidUpdate(prevProps, prevState, snapshot) {    if (snapshot !== null) {      const chatThreadRef = this.chatThreadRef.current;      chatThreadRef.scrollTop = chatThreadRef.scrollHeight - snapshot;    }  }

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

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

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

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

卸载生命周期方法

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

componentWillUnmount()

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

// e.g add event listenercomponentDidMount() {\tel.addEventListener()}// e.g remove event listener componentWillUnmount() {    el.removeEventListener() }

错误处理生命周期方法

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

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

这是最基本的实现:

import React, { Component } from 'react';class ErrorBoundary extends Component {  state = {};  render() {    return null;  }}export default ErrorBoundary;

static getDerivedStateFromError()

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

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

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

import React, { Component } from \u0026quot;react\u0026quot;;class ErrorBoundary extends Component {  state = {};  static getDerivedStateFromError(error) {    console.log(`Error log from getDerivedStateFromError: ${error}`);    return { hasError: true };  }  render() {    return null;  }}export default ErrorBoundary;

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

componentDidCatch()

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

componentDidCatch(error, info) {}

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

componentDidCatch(error, info) {\tlogToExternalService(error, info) // this is allowed.         //Where logToExternalService may make an API call.}

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

import React, { Component } from \u0026quot;react\u0026quot;;class ErrorBoundary extends Component {  state = { hasError: false };  static getDerivedStateFromError(error) {    console.log(`Error log from getDerivedStateFromError: ${error}`);    return { hasError: true };  }  componentDidCatch(error, info) {    console.log(`Error log from componentDidCatch: ${error}`);    console.log(info);  }  render() {    return null  }}export default ErrorBoundary;

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

... render() {    if (this.state.hasError) {      return \u0026lt;h1\u0026gt;Something went wrong.\u0026lt;/h1\u0026gt;;    }    return this.props.children; }

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

以上是 如何通俗易懂地向别人解释React生命周期方法? 的全部内容, 来源链接: utcz.com/z/383114.html

回到顶部