朗朗上口的react 生命周期(上)

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

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

出生

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

首先我們要引入一個概念:組件(Component)。組件非常好理解,就是可以復用的模板。例如通過按鈕組件(模板)我們可以實例化出多個相似的按鈕出來。這和代碼中類(Class)的概念是相同的。并且在ES6代碼中定義組件時也是通過類來實現的:

import React from 'react';

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

也可以通過ES2015的語法接口React.createClass來定義組件:

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

如果你的babel配置文件.babelrcpresets指定了es2015,那么在編譯之后的文件中,你會發現class MyButton extends React.Component語句編譯之后的結果就是React.createClass

注意到當我們在使用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元素。

你可能會覺得所有這一切應當。

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

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

image

在出生階段時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方法

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容

  • 作為一個合格的開發者,不要只滿足于編寫了可以運行的代碼。而要了解代碼背后的工作原理;不要只滿足于自己的程序...
    六個周閱讀 8,481評論 1 33
  • 生命周期流程圖簡單如下: 組件讓你把用戶界面分成獨立的,可重復使用的部分,并且將每個部分分開考慮。React.Co...
    Simple_Learn閱讀 1,093評論 0 0
  • 40、React 什么是React?React 是一個用于構建用戶界面的框架(采用的是MVC模式):集中處理VIE...
    萌妹撒閱讀 1,038評論 0 1
  • 3. 組件生命周期 React嚴格定義了組件的生命周期,生命周期可能會經歷如下三個過程: 裝載過程(Mount):...
    懷念不能閱讀 641評論 1 3
  • 我老公生病的時候,除非特別嚴重的腸胃問題,他總是會說,人不舒服就應該吃點想吃的好東西,然后只要還可以活動自如...
    走路外八字閱讀 524評論 0 0