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

2019 年 2 月 13 日

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

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


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


React 组件都有自己的阶段。


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


class HelloWorld extends React.Component {   render() {  return <h1> Hello World </h1>    }}
复制代码


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



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


对于人类而言,我们会经历小孩、成人、老人阶段。而对于 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) {  //do stuff here  }  ...
复制代码


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


...   static getDerivedStateFromProps(props, state) {      return {       points: 200 // update state with this     }  }    ...
复制代码


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


...   static getDerivedStateFromProps(props, state) {    return null  }  ...
复制代码


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


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


下面是一个简单的例子:


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


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


class App extends Component {  state = {    points: 10  }
render() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> You've scored {this.state.points} points. </p> </header> </div> ); }}
复制代码


结果如下所示:



源代码可以在 GitHub 上获得:


https://github.com/ohansemmanuel/points


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


class App extends Component {  state = {    points: 10  }    // *******  //  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 (      <div className="App">        <header className="App-header">          <img src={logo} className="App-logo" alt="logo" />          <p>            You've scored {this.state.points} points.          </p>        </header>      </div>    );  }}
复制代码


现在我们有了 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 {  // render is the only required method for a class component    render() {  return <h1> Hurray! </h1>   }}
复制代码


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


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


class MyComponent extends React.Component {   render() {  return "Hurray"    }}
复制代码


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


class MyComponent extends React.Component {   render() {    return [          <div key="1">Hello</div>,           <div key="2" >World</div>      ];   }}class MyComponent extends React.Component {   render() {  return <React.Fragment>          <div>Hello</div>          <div>World</div>      </React.Fragment>   }}
复制代码


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


class MyComponent extends React.Component {    render() {  return null   }}
class MyComponent extends React.Component { // guess what's returned here? render() { return (2 + 2 === 5) && <div>Hello World</div>; }}
复制代码


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


class MyComponent extends React.Component {  render() {    return createPortal(this.props.children, document.querySelector("body"));  }}
复制代码


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


4. componentDidMount()


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


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


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


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


class ModalContent extends React.Component {
el = document.createElement("section");
componentDidMount() { document.querySelector("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() { el.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 组件的无序列表:


<ul className="chat-thread">    <Chats chatList={this.state.chatList} /> </ul>
复制代码


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


Chats 组件的实现如下:


class Chats extends Component {  render() {    return (      <React.Fragment>        {this.props.chatList.map((chat, i) => (          <li key={i} className="chat-bubble">            {chat}          </li>        ))}      </React.Fragment>    );  }}
复制代码


它只是通过映射 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 > 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:


<ul className="chat-thread" ref={this.chatThreadRef}>   ...</ul>
复制代码


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


getSnapshotBeforeUpdate(prevProps, prevState) {    if (this.state.chatList > prevState.chatList) {      // write logic here    }  }
复制代码


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


getSnapshotBeforeUpdate(prevProps, prevState) {    if (this.state.chatList > prevState.chatList) {      // write logic here    }      return null }
复制代码


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


getSnapshotBeforeUpdate(prevProps, prevState) {    if (this.state.chatList > 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() {  el.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 "react";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) {  logToExternalService(error, info) // this is allowed.         //Where logToExternalService may make an API call.}
复制代码


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


import React, { Component } from "react";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 <h1>Something went wrong.</h1>;    }
return this.props.children; }
复制代码


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


2019 年 2 月 13 日 14:133422
用户头像

发布了 731 篇内容, 共 359.6 次阅读, 收获喜欢 1824 次。

关注

评论

发布
暂无评论
发现更多内容

第九周作业

fmouse

极客大学架构师训练营

性能优化三第九周作业「架构师训练营第 1 期」

天天向善

第九周总结

alpha

极客大学架构师训练营

架构师训练营第 1 期第九周总结

Leo乐

极客大学架构师训练营

首次排查 OOM 实录

AI乔治

Java 架构 OOM

架构师训练营第五周作业

丁乐洪

架构一期第九周作业

Airs

Week5 - 技术选型 - 缓存,队列,负载均衡

evildracula

学习 架构

这种算法都看不懂!9张图是如何展示出来的

周老师

Java 编程 程序员 架构 面试

架构师训练营第 1 期 - 第 9 周学习总结

Anyou Liu

极客大学架构师训练营

极客时间架构师训练营 - 第五期作业

文江

三步法解析Axios源码

执鸢者

前端 axios

极客时间架构师培训 1 期 - 第 9 周作业

Kaven

架构师训练营第 1 期第九周作业

Leo乐

极客大学架构师训练营

极客时间架构师训练营 1 期 - 第 9 周总结

Kaven

JVM垃圾回收及秒杀系统

天天向上

极客大学架构师训练营

微服务手册:分库分表从分析到实践,不再停留只会说分库分表

互联网应用架构

分库分表

native关键字作用到底是什么?

秦怀杂货店

Java 源码 源码刨析 native

作业-第5周

arcyao

第九周总结

fmouse

极客大学架构师训练营

第8周 作业2

Yangjing

极客大学架构师训练营

神经网络泛化

计算机与AI

神经网络 学习

架構師訓練營第 1 期 - 第 09 周作業

Panda

架構師訓練營第 1 期

关于开发排期

张明森

第九周作业

alpha

极客大学架构师训练营

Week 9 设计秒杀系统

黄立

斐波那契查找

ilovealt

算法和数据结构

架构师第2期第5周作业一

老坛酸菜

极客大学架构师训练营

架构师入门学习感悟五

莫问

Java核心基础——动态代理、静态代理

老农小江

java基础 代理模式

架构师训练营第 1 期 - 第 9 周课后练习

Anyou Liu

极客大学架构师训练营

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