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) orthis.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配置文件.babelrc
中presets
指定了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
。準確來說應該是getDefaultProps
和getInitialState
。
而為什么大部分人對這兩個函數陌生,是因為這兩個函數只是在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
。那么在
<App name={null} />
-
<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
之前被調用,并且只會被調用一次。當組件進入到這個生命周期中時,所有的state
和props
已經配置完畢,我們可以通過this.props
和this.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 tocomponentWillMount
.
另一個需要注意的地方是,你也不應該在render
中通過ReactDOM.findDOMNode
方法訪問原生的DOM元素(原生相對于虛擬DOM而言)。因為這么做存在兩個風險:
- 此時虛擬元素還沒有被渲染到頁面上,所以你訪問的元素并不存在
- 因為當前的
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元素。
你可能會覺得所有這一切應當。
在之前講解每個周期函數時,都只考慮單個組件的情況。但是當組件包含孩子組件時,孩子組件的鉤子函數的調用順序就需要留意了。
比如有下面這樣的樹狀結構的組件
在出生階段時componentWillMount
和render
的調用順序是
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
方法