如何通俗易懂地向别人解释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