如何通俗易懂地向别人解释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生命周期方法?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 最渣的 Spring Boot 文章
- 下一篇: 复习日记-validate表单校验插件/