玩轉 React(五)- 元件的內部狀態和生命週期

sarike發表於2017-10-30

文章標題總算是可以正常一點了……

通過之前的文章我們已經知道:在 React 體系中所謂的 "在 JavaScript 中編寫 HTML 程式碼" 指的是 React 擴充套件了 JavaScript 的語法,也就是 JSX。JSX 語法中可以以類似 HTML 語法的方式使用 React 元件,從而編寫 React 元件就有一種創造一個新的 HTML 標籤的體驗。

上一篇文章《玩轉 React(四)- 創造一個新的 HTML 標籤》介紹瞭如何來建立一個 React 元件,以及元件的屬性。瞭解到元件的檢視是屬性的對映,通過改變元件屬性可以觸發元件重新渲染,從而改變元件的檢視。其實元件的檢視並不僅僅是由屬性對映來的,本篇將介紹另一種可以觸發元件重新渲染的方式,即元件的內部狀態(state),嚴格來說元件的檢視是由屬性和內部狀態對映而來的,即:view = f(props, state),跟屬性類似,狀態的改變也會觸發元件重新渲染,只不過狀態是元件內部基於自身邏輯或者使用者事件自己維護的,而不是由外部輸入的。

另外本文中會介紹一個通過類繼承方式定義的元件的生命週期,以及在各個生命週期函式中能做什麼,不能或儘量不要做什麼。

內容摘要

  • ReactDOM.render 在一個單頁面 web 應用中通常只呼叫一次。
  • 元件可以通過 setState 改變內部狀態 state 來更新檢視。
  • setState 多數情況下是非同步的。
  • 不要直接使用當前 state 的值生成下一個 state
  • 不要直接通過 this.state 修改 state
  • 元件生命週期流程圖。
  • 各個生命週期函式介紹及使用經驗。

以上是本文的內容摘要,如果你已經知道我要說的是什麼,那麼就沒有必要繼續看下去了,節約時間。

元件的內部狀態

此前,我們已經瞭解到可以通過 ReactDOM.render(<HelloMessage name="Lucy" />, container) 的方式,將帶有特定屬性的元件渲染到頁面的某個 DOM 節點中(container),這樣頁面上會展示出 “Hello Lucy”,當我們希望頁面上展示 “Hello Tom” 的時候,我們可以將元件的 name 屬性改為 Tom 後再次呼叫 ReactDOM.render 方法,這樣元件就會以新的屬性重新渲染,從而更新元件的檢視。

下面是官方文件中一個展示時鐘的例子,我簡單改造了下:

codepen.io/Sarike/pen/…

例子中定義了一個 Clock 元件,元件接收一個 time 屬性,在元件外部通過 setInterval 週期性地呼叫 ReactDOM.render 不斷更新 Clock 的屬性並重新渲染。

然而在很多實際場景中,對於一個時鐘元件,我們希望它有更好的封裝性和複用性,也就是說我們希望只呼叫一次 ReactDOM.render(<Clock />, container) 然後它可以自己更新自己的檢視,這樣我們就更容易在頁面上放置多個時鐘了,即複用性更好了。

要達到這個目的,就需要元件的內部狀態來支援。元件有一個特殊的屬性 state 用來儲存元件的內部狀態。使用者可以通過 this.setState(statePatch) 來更新元件的狀態,元件的狀態更新後會重新執行 render 方法來更新檢視,上面的例子使用內部狀態改造後:

codepen.io/Sarike/pen/…

這樣 Clock 作為一個完整的時鐘元件就可以自己來更新自己了,上篇文中也有提到過,如果想要使用元件的內部狀態,那元件必須以類繼承的方式來定義,而不能使用函式式元件。所以說,函式式元件經常也被稱作是無狀態元件(stateless)。

上面例子中有用到 componentDidMountcomponentWillUnmount 兩個函式,它們是元件的生命週期函式,本文的後半部分將會介紹,這倆函式分別在元件掛載到頁面上和元件將要從頁面上移除時呼叫。

改造後的例子,我們只需要呼叫一次 ReactDOM.render 即可,在實際的專案中,一個完整的單頁面 web 應用,也只需要呼叫一次 ReactDOM.render 方法把根元件掛載到頁面中即可,剩下的工作就都放心地交給 React 就行了。

初始化元件內部狀態

在建立一個擁有內部狀態的元件時,我們需要對內部狀態進行初始化,即設定元件最初的狀態是什麼。做法很簡單,就是在建構函式 constructor 中設定 state 屬性就可以了。如下所示:

class MyComponent extends React.Component {
    constructor(props) {
        super(props); // 這行程式碼不能少哦
        this.state = {
            name: "Lucy"
        }
    }
}複製程式碼

setState 大多數情況下是非同步的

setState 多數情況下是非同步的,非同步意味著通過 setState 更新元件狀態後,不能立刻通過 this.state 來獲取到更新之後的值,另外當連續多次呼叫 setState 來更新同一個欄位時,只有最後一次更新才會生效。如下示例:

codepen.io/Sarike/pen/…

如果希望上面示例程式碼正常工作,你需要通過回撥函式的方式來生成下一個 state,如下所示:

this.setState(preState => ({value: preState.value + 1}));
this.setState(preState => ({value: preState.value + 2}));
this.setState(preState => ({value: preState.value + 3}));複製程式碼

所以,直接基於當前 state 的值,生成一下個 state 是不靠譜的,但是很多不清楚這一點的同學基本上都是這麼做的,因為寫起來簡單嘛,而且貌似也沒有什麼問題。這是因為很多情況下,業務邏輯沒有那麼複雜,基本不會頻繁呼叫 setState 。但是這確實是一個隱患,如果在專案初期不注意規避,等專案複雜到一定程度以後,可能會出現難以排查的BUG。

那為什麼說多數情況下是非同步的呢?難道有些情況下不是非同步的嗎?是的,實際上只有在 React 能控制的事件處理過程中呼叫的 setState 才是非同步的,如:生命週期函式,React 內建的如 button,input 等元件的事件處理函式。在多數的情況下我們只需要在這些地方控制我們的元件就夠了,所以說大多數情況下 setState 是非同步的。

在某些特殊的元件中,可能需要通過 addEventListener 來設定某些 DOM 的事件處理函式,在這種通過原生的 JS API 來設定的事件處理過程呼叫 setState 就是同步的,會立即更新 this.state。另外還有 setIntervalsetTimeout 等原生 API 的回撥函式也是如此。

參考:www.zhihu.com/question/66…

不要直接通過 this.state 來更新元件狀態

這一點跟屬性類似,直接通過 this.state 修改元件狀態,元件狀態被修改了,但並不會觸發元件的重新渲染。這樣就會導致元件檢視與狀態不一致。

生命週期函式

一個元件被我們創造到這個世界上之後,在使用它時,它的每個例項都是有一定生命週期的,下面這張圖說明了一個元件例項的生命週期:

生命週期
生命週期

圖片來源:tylermcginnis.com/an-introduc…

這張圖略微有點老,不過結合下文來看也沒什麼問題,下面我們來解釋一下上面這張圖。

元件初始化:constructor

我們定義的每一個元件,都是一個類(class),這些類被例項化後才能作為 React DOM 中的一個節點渲染到頁面上。所以,當我們通過 ReactDOM.render 或者在某個元件中通過 JSX 表示式將一個元件第一次渲染到頁面上時,元件首先要做的就是對元件進行例項化。

例項化主要做的事情:

  • 建立一個元件的例項物件(也就是 Element,通常對應一個JSX表示式,如:<MyComponent />)。
  • 獲取元件的預設屬性。
  • 獲取元件的初始內部狀態(在 constructorthis.state = xxxx;)。

componentWillMount

在元件被渲染到頁面上之前執行,在元件的整個生命週期內只執行一次。在這裡可以呼叫 setState 更新內部狀態,但是更推薦將這裡的狀態更新操作放到 constructor 中。

該函式執行完後會立馬執行 render 方法並將元件渲染到頁面上。所以,在這裡執行 setState 不會觸發額外的渲染過程,因為這是沒有必要的。

componentDidMount

元件被渲染到頁面上後立馬執行,在元件的整個生命週期內只執行一次。這個時候是做如下操作的好時機:

  • 某些依賴元件 DOM 節點的操作。
  • 發起網路請求。
  • 設定 setIntervalsetTimeout 等計時器操作。

在這裡可以呼叫 setState 更新元件內部狀態,且會觸發一個重新渲染的過程,即會重新執行 render 方法並更新檢視。

componentWillReceiveProps

componentWillReceiveProps(nextProps)複製程式碼

該宣告周期函式可能在兩種情況下被呼叫:

  1. 元件接收到了新的屬性。新的屬性會通過 nextProps 獲取到。
  2. 元件沒有收到新的屬性,但是由於父元件重新渲染導致當前元件也被重新渲染。

你只要知道,當該函式被呼叫時,並不一定是因為屬性發生了變化

在這裡也可以呼叫 setState 更新元件的內部狀態,同樣也不會觸發額外的重新渲染操作,React 會聰明地用更新後的屬性和內部狀態進行一次重新渲染。

shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState)複製程式碼

這是一個詢問式的生命週期函式,所以該函式需要一個返回值 true/false,如果為 true,元件將觸發重新渲染過程,如果為 false 元件將不會觸發重新渲染。因此,合理地利用該函式可以一定程度節省開銷,提高系統的效能。

此處不能呼叫 setState 更新元件的狀態。

由於元件屬性或者內部狀態被改變時都觸發元件重新渲染,所以該函式接受兩個引數:新的屬性(nextProps)、新的狀態(nextState)。

在處理該宣告周期函式時,切記要兼顧屬性和狀態,不能只顧其一,不然很容易踩坑。例如:某位同學只依據屬性來判斷是否觸發重新渲染,而忽略了內部狀態,這樣就導致你無論如何 setState,元件檢視都不能正常更新。

在上篇文章中我們提到類繼承方式定義元件時說到,React 提供了兩個基類,一個是 Component,另一個是 PureComponent,兩者的差別就在於後者已經幫我們簡單實現了一下 shouldComponentUpdate 函式,當屬性和狀態都沒有發生變化時返回 false 以避免額外的開銷。

但是比對過程出於效能考慮,只是進行淺比對,也就是隻比對物件的第一級欄位,而且是否發生變化是通過 Object.is 方法類判斷的。所以會導致有時候發生變化了元件沒有更新,沒有變化卻觸發了重新渲染過程。這個在這裡不再贅述,想深入探討可以掃描問候的二維碼加我微信好友(我的微信:leobaba88)。

componentWillUpdate

當元件 shouldComponentUpdate 返回 true 或者呼叫 forceUpdate 時將觸發此函式。

該函式中不能呼叫 setState 更新元件狀態,當你想這麼做的時候,你可以考慮將它移到 componentWillReceiveProps 函式裡。

該函式在函式第一次渲染的時候不會執行。

componentDidUpdate

componentDidUpdate(prevProps, prevState)複製程式碼

在元件重新渲染過程中,重新執行 render 方法並更新元件檢視後立即執行該函式。類似元件第一次渲染過程中的 componentDidMount,該函式在第一次渲染時不會執行。

在此處是做這些事情的好時機:

  • 執行依賴新 DOM 節點的操作。
  • 依據新的屬性發起新的網路請求。(但是此處一定要格外謹慎,一定要在確認屬性變化後再發起網路請求,不然極有可能進入死迴圈:didUpdate -> ajax -> changeProps -> didUpdate -> ...)。

componentWillUnmount

當元件被從頁面中移除之前呼叫,此時是清理戰場的好時機,如清理定時器、中指網路請求等。

componentDidCatch

componentDidCatch(error, info)複製程式碼

這是 React 16 新加入的一個生命週期函式。定義該生命週期函式的元件將會成為一個錯誤邊界,錯誤邊界這個詞非常形象,它可以有效地將錯誤限制在一個有限的範圍內,而不會導致整個應用崩潰,防止一顆耗子屎壞了一鍋湯。

錯誤邊界元件,可以捕獲其整個子元件樹內發生的任何異常,但是卻不能捕獲自身的異常。

下面是官方的一個示例,大家感受下:

codepen.io/gaearon/pen…

最後(微信群)

這篇文章來的有點慢,非常抱歉。

另外為了方便大家閱讀,我將所有文章的連結更新到第一篇文章 《玩轉React(一)- 前言》 中。

文字的表現範圍畢竟有限,為了方便大家交流,我建了一個微信群,對 React 感興趣的同學可以進群一起交流、學習,由於微信群邀請的時間限制,大家可以先掃描下面二維碼,加我好友,我拉大家進群:

clipboard.png
clipboard.png

我的微信:leobaba88

相關文章