深刻React的生命週期(上):出生階段(Mount)

前言

本文是對開源圖書React In-depth: An exploration of UI development的概括和加強。同時也融入了本身在開發中的一些心得。javascript

你或許會問,閱讀完這篇文章以後,對工做中開發React相關的項目有幫助嗎?實話實說幫助不會太大。這篇文章不會教你使用一項新技術,不會幫助你提升編程技巧,而是完善你的React知識體系,例如區分某些概念,明白一些最佳實踐是怎麼來的等等。若是硬是要從功利的角度來考慮這些知識帶來的價值,那麼會是對你的面試很是有幫助,這篇文章裏知識點在面試時經常會被問到,爲何我知道,由於我吃過它們的虧。html

React組件的生命週期劃分爲出生(mount),更新(update)和死亡(unmount),然而咱們怎麼知道組件進入到了哪一個階段?只能經過React組件暴露給咱們的鉤子(hook)函數來知曉。什麼是鉤子函數,就是在特定階段執行的函數,好比constructor只會在組件出生階段被調用一次,這就算是一個「鉤子」。反過來講,當某個鉤子函數被調用時,也就意味着它進入了某個生命階段,因此你能夠在鉤子函數裏添加一些代碼邏輯在用於在特定的階段執行。固然這不是絕對的,好比render函數既會在出生階段執行,也會在更新階段執行。順便多說一句,「鉤子」在編程中也算是一類設計模式,好比github的Webhooks。顧名思義它也是鉤子,你可以經過Webhook訂閱github上的事件,當事件發生時,github就會像你的服務發送POST請求。利用這個特性,你能夠監聽master分支有沒有新的合併事件發生,若是你的服務收到了該事件的消息,那麼你就能夠例子執行部署工做。java

咱們按照階段的時間順序對每個鉤子函數進行講解。react

出生

  • constructor
  • getDefaultProps() (React.createClass) orMyComponent.defaultProps (ES6 class)
  • getInitialState() (React.createClass) or this.state = ... (ES6 constructor)
  • componentWillMount()
  • render()
  • componentDidMount()

首先咱們要引入一個概念:組件(Component)。組件很是好理解,就是能夠複用的模板。例如經過按鈕組件(模板)咱們能夠實例化出多個類似的按鈕出來。這和代碼中類(Class)的概念是相同的。而且在ES6代碼中定義組件時也是經過類來實現的:git

import React from 'react';

class MyButton extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <button>My Button</button>
    )
  }
}複製代碼

也能夠經過ES2015的語法接口React.createClass來定義組件:github

const MyButton = React.createClass({
  render: function() {
    return (
      <button>My Button</button>      
    );
  }
});複製代碼

若是你的babel配置文件.babelrcpresets指定了es2015,那麼在編譯以後的文件中,你會發現class MyButton extends React.Component語句編譯以後的結果就是React.createClassweb

注意到當咱們在使用class定義組件時,繼承(extends)了React.Component類。但實際上這並非必須的。好比你徹底能夠寫成純函數的形式:面試

const MyButton = () => {
  return <h1>My Button</h1>
}複製代碼

這就是無狀態(stateless)組件,顧名思義它是沒有本身獨立狀態的,這個概念被用於React的設計模式:High Order Component和Container Component中。具體能夠參考個人另外一篇文章面試系列之三:你真的瞭解React嗎(中)組件間的通訊以及React優化編程

它的侷限也很明顯,由於沒有繼承React.Component的緣故,你沒法得到各類生命週期函數,也沒法訪問狀態(state),可是仍然可以訪問傳入的屬性(props),它們是做爲函數的參數傳入的。設計模式

定義組件時並不會觸發任何的生命週期函數,組件本身也並不會存在生命週期這一說,真正的生命週期開始於組件被渲染至頁面中。

讓咱們看一段最簡單的代碼:

import React from 'react';
import ReactDOM from 'react-dom';

class MyComponent extends React.Component {
  render() {
    return <div>Hello World!</div>;
  }
};

ReactDOM.render(<MyComponent />, document.getElementById('mount-point'));複製代碼

在這段代碼中,MyComponnet組件經過ReactDOM.render函數被渲染至頁面中。若是你在MyComponent組件的各個生命週期函數中添加日誌的話,會看到日誌依次在控制檯輸出。

爲了說明一些問題,咱們嘗試對代碼作一些修改:

import MyButton from './Button';
class MyComponent extends React.Component {
  render() {
    const button = <MyButton /> return <div>Hello World!</div>; } };複製代碼

在組件的render函數中,咱們使用到了另外一個組件MyButton,可是它並無出如今最終返回的DOM結構中。問題來了,當MyComponnet組件渲染至頁面上時,Mybutton組件的生命週期函數會開始調用嗎?<MyButton />究竟表明了什麼?

咱們先回答第二個問題。<MyButton />看上去確實有些奇怪,可是別忘了它是JSX語法。若是你去看babel編譯以後的代碼就會發現,其實它把<MyButton />轉化爲函數調用:React.createElement(MyButton, null)。也就是說<XXX />語法,實際上返回的是一個XXX類型的React元素(Element)。React元素說白了就是一個純粹的object對象,基本由key(id), props(屬性), ref, type(元素類型)四個屬性組成(children屬性包含在props中)。爲何要用「純粹」這個形容詞,是由於雖然它和組件有關,可是它並不包含組件的方法,此時此刻,它僅僅是一個包含若干屬性的對象。若是你以爲這一切看上去都無比熟悉的話,那麼你猜對了,元素表明的實際上是虛擬DOM(Virtual DOM)上的節點,是對你在頁面上看到的每個DOM節點的描述。

那麼咱們能夠回答第一個問題了,僅僅是生成一個React元素是不會觸發生命週期函數調用的。

當咱們把React元素傳遞給ReactDOM.render方法,而且告訴它具體在頁面上渲染元素的位置以後,它會給咱們返回組件的實例(Instance)。在JS語法中,咱們經過new關鍵字初始化一個類的實例,而在React中,咱們經過ReactDOM.render方法來初始化一個組件的實例。但通常狀況下咱們不會用到這個實例,不過你也能夠保留它的引用賦值給一個變量,當測試組件的時候能夠派上用場

Default Porps & Default State

若是被問起constructor以後的下一個生命週期函數是什麼,絕大部分人會回答componentWillMount。準確來講應該是getDefaultPropsgetInitialState

而爲何大部分人對這兩個函數陌生,是由於這兩個函數只是在ES2015語法中建立組件時暴露出來,在ES6語法中咱們經過兩個賦值語句實現了一樣的效果。

好比添加默認屬性的getDefaultProps函數在ES6中是經過給組件類添加靜態字段defaultProps實現的:

class MyComponent extends React.Component() {
  //...
}
MyComponent.defaultProps = { age: 'unknown' }複製代碼

在實際計算屬性的過程當中,將傳入屬性與默認屬性進行合併成爲最終使用的屬性,用僞代碼寫的意思就是

this.props = Object.assign(defaultProps, passedProps);複製代碼

注意知識點要來了,看下面這個組件:

class App extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return <div>{this.props.name}</div>
  }
}
App.defaultProps = { name: 'default' };複製代碼

我給這個組件設置了一個默認屬性name,值爲default。那麼在

  1. <App name={null} />
  2. <App name={undefined} />
    這兩種狀況下,this.props.name值會是什麼?也就是最終輸出會是什麼?

正確答案是若是給name傳入的值是null,那麼最終頁面上的輸出是空,也就是null會生效;若是傳入的是undefined,那麼React認爲這個值是undefined貨真價實的未定義,則會使用默認值,最終頁面上的輸出是default

而獲取默認狀態的函數getInitialState在ES6中是經過給this.state賦值實現的

class Person extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  //...
}複製代碼

componentWillMount()

componentWillMount函數在第一次render以前被調用,而且只會被調用一次。當組件進入到這個生命週期中時,全部的stateprops已經配置完畢,咱們能夠經過this.propsthis.state訪問它們,也能夠經過setState從新設置狀態。總之推薦在這個生命週期函數裏進行狀態初始化的處理,爲下一步render作準備

render()

當一切配置都就緒以後,就可以正式開始渲染組件了。render函數和其餘的鉤子函數不一樣,它會同時在出生和更新階段被調用。在出生階段被調用一次,可是在更新階段會被調用屢次。

不管是編寫哪一個階段的render函數,請牢記一點:保證它的「純粹」(pure)。怎樣纔算純粹?最基本的一點是不要嘗試在render裏改變組件的狀態。由於經過setState引起的狀態改變會致使再一次調用render函數進行渲染,而又繼續改變狀態又繼續渲染,致使無限循環下去。若是你這麼作了你會在開發模式下收到警告:

Warning: Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.

另外一個須要注意的地方是,你也不該該在render中經過ReactDOM.findDOMNode方法訪問原生的DOM元素(原生相對於虛擬DOM而言)。由於這麼作存在兩個風險:

  1. 此時虛擬元素尚未被渲染到頁面上,因此你訪問的元素並不存在
  2. 由於當前的render即將執行完畢返回新的DOM結構,你訪問到的多是舊的數據。

而且若是你真的這麼作了,那麼你會獲得警告:

Warning: App is accessing findDOMNode inside its render(). render() should be a pure function of props and state. It should never access something that requires stale data from the previous render, such as refs. Move this logic to componentDidMount and componentDidUpdate instead.

componentDidMount()

當這個函數被調用時,就意味着能夠訪問組件的原生DOM了。若是你有經驗的話,此時不只僅可以訪問當前組件的DOM,還可以訪問當前組件孩子組件的原生DOM元素。

你可能會以爲全部這一切應當。

在以前講解每一個周期函數時,都只考慮單個組件的狀況。可是當組件包含孩子組件時,孩子組件的鉤子函數的調用順序就須要留意了。

好比有下面這樣的樹狀結構的組件

react element tree
react element tree

在出生階段時componentWillMountrender的調用順序是

A -> A.0 -> A.0.0 -> A.0.1 -> A.1 -> A.2.複製代碼

這很容易理解,由於當你想渲染父組件時,務必也要當即開始渲染子組件。因此子組件的生命週期開始於父組件以後。

componentDidMount的調用順序是

A.2 -> A.1 -> A.0.1 -> A.0.0 -> A.0 -> A複製代碼

componentDidMount的調用順序正好是render的反向。這其實也很好理解。若是父組件想要渲染完畢,那麼首先它的子組件須要提早渲染完畢,也因此子組件的componentDidMount在父組件以前調用。

正由於咱們能在這個函數中訪問原生DOM,因此在這個函數中一般會作一些第三方類庫初始化的工具,包括異步加載數據。好比說對c3.js的初始化

import React from 'react';
import ReactDOM from 'react-dom';
import c3 from 'c3';

export default class Chart extends React.Component {

  componentDidMount() {
    this.chart = c3.generate({
      bindto: ReactDOM.findDOMNode(this.refs.chart),
      data: {
        columns: [
          ['data1', 30, 200, 100, 400, 150, 250],
          ['data2', 50, 20, 10, 40, 15, 25]
        ]
      }
    });
  }

  render() {
    return (
      <div ref="chart"></div>
    );
  }
}複製代碼

由於可以訪問原生DOM的緣故,你可能會在componentDidMount函數中從新對元素的樣式進行計算,調整而後生效。所以當即須要對DOM進行從新渲染,此時會使用到forceUpdate方法

本文同時也發佈在個人知乎專欄上,也歡迎你們關注

參考

相關文章
相關標籤/搜索