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

文章標題總算是能夠正常一點了……react

經過以前的文章咱們已經知道:在 React 體系中所謂的 "在 JavaScript 中編寫 HTML 代碼" 指的是 React 擴展了 JavaScript 的語法,也就是 JSX。JSX 語法中能夠以相似 HTML 語法的方式使用 React 組件,從而編寫 React 組件就有一種創造一個新的 HTML 標籤的體驗。web

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

另外本文中會介紹一個經過類繼承方式定義的組件的生命週期,以及在各個生命週期函數中能作什麼,不能或儘可能不要作什麼。segmentfault

內容摘要

  • ReactDOM.render 在一個單頁面 web 應用中一般只調用一次。微信

  • 組件能夠經過 setState 改變內部狀態 state 來更新視圖。網絡

  • setState 多數狀況下是異步的。less

  • 不要直接使用當前 state 的值生成下一個 state異步

  • 不要直接經過 this.state 修改 state函數

  • 組件生命週期流程圖。性能

  • 各個生命週期函數介紹及使用經驗。

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

組件的內部狀態

此前,咱們已經瞭解到能夠經過 ReactDOM.render(<HelloMessage name="Lucy" />, container) 的方式,將帶有特定屬性的組件渲染到頁面的某個 DOM 節點中(container),這樣頁面上會展現出 「Hello Lucy」,當咱們但願頁面上展現 「Hello Tom」 的時候,咱們能夠將組件的 name 屬性改成 Tom 後再次調用 ReactDOM.render 方法,這樣組件就會以新的屬性從新渲染,從而更新組件的視圖。

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

https://codepen.io/Sarike/pen...

例子中定義了一個 Clock 組件,組件接收一個 time 屬性,在組件外部經過 setInterval 週期性地調用 ReactDOM.render 不斷更新 Clock 的屬性並從新渲染。

然而在不少實際場景中,對於一個時鐘組件,咱們但願它有更好的封裝性和複用性,也就是說咱們但願只調用一次 ReactDOM.render(<Clock />, container) 而後它能夠本身更新本身的視圖,這樣咱們就更容易在頁面上放置多個時鐘了,即複用性更好了。

要達到這個目的,就須要組件的內部狀態來支持。組件有一個特殊的屬性 state 用來保存組件的內部狀態。用戶能夠經過 this.setState(statePatch) 來更新組件的狀態,組件的狀態更新後會從新執行 render 方法來更新視圖,上面的例子使用內部狀態改造後:

https://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 來更新同一個字段時,只有最後一次更新纔會生效。以下示例:

https://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 的回調函數也是如此。

參考:https://www.zhihu.com/questio...

不要直接經過 this.state 來更新組件狀態

這一點跟屬性相似,直接經過 this.state 修改組件狀態,組件狀態被修改了,但並不會觸發組件的從新渲染。這樣就會致使組件視圖與狀態不一致。

生命週期函數

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

生命週期

圖片來源:https://tylermcginnis.com/an-...,這張圖略微有點老,不過結合下文來看也沒什麼問題。

下面咱們來解釋一下上面這張圖。

組件初始化: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 新加入的一個生命週期函數。定義該生命週期函數的組件將會成爲一個錯誤邊界,錯誤邊界這個詞很是形象,它能夠有效地將錯誤限制在一個有限的範圍內,而不會致使整個應用崩潰,防止一顆耗子屎壞了一鍋湯。

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

下面是官方的一個示例,你們感覺下:

https://codepen.io/gaearon/pe...

最後(微信羣)

這篇文章來的有點慢,很是抱歉。

另外爲了方便你們閱讀,我將全部文章的連接更新到第一篇文章 《玩轉React(一)- 前言》 中。

文字的表現範圍畢竟有限,爲了方便你們交流,我建了一個微信羣,對 React 感興趣的同窗能夠進羣一塊兒交流、學習,因爲微信羣邀請的時間限制,你們能夠先掃描下面二維碼,加我好友,我拉你們進羣:

clipboard.png

個人微信:leobaba88

相關文章
相關標籤/搜索