本文是對開源圖書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
有關出生階段請參考上一篇《深刻React的生命週期(上):出生階段(Mount)》git
componentWillReceiveProps()
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate()
更新階段會在三種狀況下觸發:github
更改props
:一個組件並不能主動更改它擁有的props
屬性,它的props
屬性是由它的父組件傳遞給它的。強制對props
進行從新賦值會致使程序報錯。面試
更改state
:state
的更改是經過setState
接口實現的。同時設計state
是須要技巧的,哪些狀態能夠放在裏面,哪些不能夠;什麼樣的組件能夠有state
,哪些不能夠有;這些都須要遵循必定原則的。這個話題有機會能夠單獨拎出來講編程
調用forceUpdate
方法:這個咱們在上一階段已經提到了,強制組件進行更新。設計模式
setState
是異步的組件的更新緣由很大一部分是由於調用setState
接口更新state
所致,咱們經常以同步的方式調用setState
,但實際上setState
方法是異步的。好比下面的這段代碼:緩存
onClick() {
this.setState({
count: 1,
});
console.log(this.state.count)
}複製代碼
在一個組件的點擊事件處理函數中,咱們更新了state
中的count
,而後當即嘗試去讀取最新的count
。事實是你讀取的結果不是1
,二應該是以前的值。
更致命的錯誤是相似這樣在同一個塊級中連續調用setState
的代碼
this.setState({ ...this.state, foo: 42 });
this.setState({ ...this.state, isBar: true });複製代碼
在這種狀況下,第一次設置的foo
值會被第二次的設置覆蓋而還原
componentWillReceiveProps(nextProps)
當傳遞給組件的props
發生改變時,組件的componentWillReceiveProps
即會被觸發調用,方法傳遞的參數的是發更更改的以後的props
值(一般咱們命名爲nextProps
)。在這個方法裏,你能夠經過this.props
訪問當前的屬性值,能夠經過nextProps
訪問即將更新的屬性值,或者將它們進行對比,或者將它們進行計算,最終肯定你須要更新的狀態(state
)並最終調用setState
方法對狀態進行更新。在這個鉤子函數中調用setState
方法並不會觸發再一次渲染。
很是有意思的是,雖然props
的更改會引發componentWillReceiveProps
的調用;但componentWillReceiveProps
的調用並不意味着props
真的發生了變化。這可不是我說的,Facebook官方花了一整篇文章說這件事:(A => B) !=> (B => A)。好比看下面這個組件:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 1,
}
this.onClick = this.onClick.bind(this);
}
onClick() {
this.setState({
number: 1,
})
}
render() {
return (
<MyButton onClick={this.onClick} data-number={this.state.number} /> ); } }複製代碼
每一次點擊事件都會從新使用setState
接口對state
進行更新,但每次更新的值都是相同的,即number:1
。而且把當前組件的狀態以屬性的形式傳遞給<MyButton />
。問題來了,那麼當我每次點擊按鈕時,按鈕MyButton
的componentWillReceiveProps
都會被調用嗎?
會,即便每次更新的值都是同樣的。
之因此出現這樣的狀況緣由其實很是簡單,由於React並不知道傳入的屬性是否發生了更改。而爲何React不嘗試去作一個是否相等的判斷呢?
由於辦不到,新傳入的屬性和舊屬性可能引用的是同一塊內存區域(引用類型),因此單純的用===
判斷是否相等並不許確。可行的解決辦法之一就是對數據進行深度拷貝而後進行比較,可是這對大型數據結構來講性能太差,還能會碰上循環引用的問題。
因此React將這個變化經過鉤子函數暴露出來,千萬不要覺得當componentWillReceiveProps
被調用就意味着props
發生了更改,若是須要在變化時作一些事情,務必要手動的進行比較。
shouldComponentUpdate()
shouldComponentUpdate
很重要,它能夠決定是否繼續當前的生命週期。默認狀況該函數返回true
即繼續當前的生命週期;也能夠返回false
終止當前的生命週期,阻止進一步的render
與接下來的步驟。
咱們上面剛剛說過,React並不會對props
進行深度比較,這對state
也一樣適用。因此即便props
與state
並未發生了更改,shouldComponentUpdate
也會被再次調用,包括接下來的步驟componentWillUpdate
、render
、componentDidUpdate
也都會再次運行一次。這很明顯會給性能形成不小的傷害。
傳遞給shouldComponentUpdate
的參數包括即將改變的props
和state
,形參的名稱是nextProps
和nextState
,在這個函數裏你同時又能經過this
關鍵字訪問到當前的state
和props
,因此你在這裏你是「全知」的,能夠徹底按照你本身的業務邏輯判斷是否state
與props
是否發生了更改,而且決定是否要繼續接下來的步驟。shouldComponentUpdate
也就一般咱們在優化React性能時的第一步。這一步的優化不只僅是優化組件自身的流程,同時也能節省去子組件的從新渲染的代價 。
固然若是你對判斷props
是否發生改變的檢測邏輯要求比較簡單的話,好比只是淺度(shallow)的判斷(即判斷對象的引用是否發生了更改)對象是否發生了更改,那麼能夠利用PureRenderMixin
:
import PureRenderMixin from 'react-addons-pure-render-mixin'; // ES6
const createReactClass = require('create-react-class');
createReactClass({
mixins: [PureRenderMixin],
render: function() {
return <div className={this.props.className}>foo</div>;
}
});複製代碼
minins
是React支持的一種容許多個組件共用代碼的一種機制。PureRenderMixin
插件的工做很是簡單,它爲你重寫了shouldComponentUpdate
函數,並對對象進行了淺度對比,具體代碼能夠從這裏和這裏找到。
在ES6中你也能夠經過直接繼承React.PureComponent
而不是React.Component
來實現這個功能。用React官方的原話說就是
React.PureComponent
is exactly likeReact.Component
, but implementsshouldComponentUpdate()
with a shallow prop and state comparison.
Pure
咱們再次強調,PureComponent
爲你實現的只是對引用是否發生了更改的判斷,甚至能夠說它只是簡單的用===
進行的判斷,因此這也是咱們稱之爲pure的緣由。爲了具體說明問題,咱們舉一個實際的例子
/* MyButton.js: */
import React from 'react';
class MyButton extends React.PureComponent {
constructor(props) {
super(props);
}
render() {
console.log('render');
return <button onClick={this.props.onClick}>My Button</button>
}
}
export default MyButton;
/* App.js: */
import React from 'react';
import MyButton from './Button.js';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
arr: [1],
}
this.onClick = this.onClick.bind(this);
}
onClick() {
this.setState({
arr: [...this.state.arr, 2],
});
}
render() {
return (
<MyButton onClick={this.onClick} data-arr={this.state.arr} /> ); } } export default App;複製代碼
在上面的這個例子中,每一次點擊都會修改state
中的arr
變量,arr
變量的引用和值都發生了更改。重點是MyButton
組件繼承的是React.PureComponent
。那麼每一次點擊時,MyButton
中的log信息都會被打印出來,即每次都會從新出發render
若是咱們把onClick
方法作一些修改:
onClick() {
const arr = this.state.arr;
arr.push(2);
this.setState({
arr: arr,
})
}複製代碼
這個方法一樣使得arr
變量發生了變化,可是僅僅是值而不是引用,此時當再一次點擊按鈕(MyButton
)時,MyButton
都不會再次進行渲染了。也就是說PureComponent
提早爲咱們進行了shallow comparison.
使用這種只修改引用,不修改數據內容的immutable data也經常做爲優化React的一個手段之一。immutable.js就能爲咱們實現這個需求,每一次修改數據時你獲得的實際上是新的數據引用,而不會修改到原有的數據。同時Redux中的reducer想達到的效果其實也類似,reducer
的重點是它的純潔性(pure),在執行時不會形成反作用,即避免對傳入數據引用的修改,同時也方便比較出組件狀態的更新。
componentWillUpdate()
componentWillUpdate
方法和componentWillMount
方法很類似,都是在即將發生渲染前觸發,在這裏你可以拿到nextProps
和nextState
,同時也能訪問到當前即將過時的props
和state
。若是有須要的話你能夠把它們暫存起來便於之後使用。
與componentWillMount
不一樣的是,在這個方法中你不可使用setState
,不然會當即觸發另外一輪的渲染而且又再一次調用componentWillUpdate
,陷入無限循環中。
componentDidUpdate()
和Mount階段相似,當組件進入componentDidUpdate
階段時意味着最新的原生DOM已經渲染完成而且能夠經過refs
進行訪問。該函數會傳入兩個參數,分別是prevProps
和prevState
,顧名思義是以前的狀態。你仍然能夠經過this
關鍵字訪問當前的狀態,由於能夠訪問原生DOM的關係,在這裏也適用於作一些第三方須要操縱類庫的操做。
update階段各個鉤子函數的調用順序也與mount階段類似,尤爲是componentDidUpdate
,子組件的該鉤子函數優先於父組件調用
由於能夠訪問DOM的緣故,咱們有可能須要在這個鉤子函數裏獲取實際的元素樣式,而且寫入state
中,好比你的代碼可能會長這樣:
componentDidUpdate(prevProps, prevState) {
// BAD: DO NOT DO THIS!!!
let height = ReactDOM.findDOMNode(this).offsetHeight;
this.setState({ internalHeight: height });
}複製代碼
若是默認狀況下你的shouldComponentUpdate()
函數老是返回true
的話,那麼這樣在componentDidUpdate
裏更新state
的代碼又會把咱們帶入無限render
的循環中。若是你必需要這麼作,那麼至少應該把上一次的結果緩存起來,有條件的更新state
:
componentDidUpdate(prevProps, prevState) {
// One possible fix...
let height = ReactDOM.findDOMNode(this).offsetHeight;
if (this.state.height !== height ) {
this.setState({ internalHeight: height });
}
}複製代碼
componentWillUnmount()
當組件須要從DOM中移除時,即會觸發這個鉤子函數。這裏沒有太多須要注意的地方,在這個函數中一般會作一些「清潔」相關的工做
最後再次強調,本文是開源圖書React In-depth: An exploration of UI development的概括。基本上想了解生命週期看這一本書就夠了,看完也無敵了。但願這篇中文簡約版也會對你有幫助。
本文同時也發佈在個人知乎專欄,歡迎你們關注