如何通俗易懂地向別人解釋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 listener
componentDidMount() {
	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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章