深刻React的生命週期(下):更新(Update)

前言

本文是對開源圖書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進行從新賦值會致使程序報錯。面試

  • 更改statestate的更改是經過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 />。問題來了,那麼當我每次點擊按鈕時,按鈕MyButtoncomponentWillReceiveProps都會被調用嗎?

會,即便每次更新的值都是同樣的。

之因此出現這樣的狀況緣由其實很是簡單,由於React並不知道傳入的屬性是否發生了更改。而爲何React不嘗試去作一個是否相等的判斷呢?

由於辦不到,新傳入的屬性和舊屬性可能引用的是同一塊內存區域(引用類型),因此單純的用===判斷是否相等並不許確。可行的解決辦法之一就是對數據進行深度拷貝而後進行比較,可是這對大型數據結構來講性能太差,還能會碰上循環引用的問題。

因此React將這個變化經過鉤子函數暴露出來,千萬不要覺得當componentWillReceiveProps被調用就意味着props發生了更改,若是須要在變化時作一些事情,務必要手動的進行比較。

shouldComponentUpdate()

shouldComponentUpdate很重要,它能夠決定是否繼續當前的生命週期。默認狀況該函數返回true即繼續當前的生命週期;也能夠返回false終止當前的生命週期,阻止進一步的render與接下來的步驟。

咱們上面剛剛說過,React並不會對props進行深度比較,這對state也一樣適用。因此即便propsstate並未發生了更改,shouldComponentUpdate也會被再次調用,包括接下來的步驟componentWillUpdaterendercomponentDidUpdate也都會再次運行一次。這很明顯會給性能形成不小的傷害。

傳遞給shouldComponentUpdate的參數包括即將改變的propsstate,形參的名稱是nextPropsnextState,在這個函數裏你同時又能經過this關鍵字訪問到當前的stateprops,因此你在這裏你是「全知」的,能夠徹底按照你本身的業務邏輯判斷是否stateprops是否發生了更改,而且決定是否要繼續接下來的步驟。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 like React.Component, but implements shouldComponentUpdate() 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方法很類似,都是在即將發生渲染前觸發,在這裏你可以拿到nextPropsnextState,同時也能訪問到當前即將過時的propsstate。若是有須要的話你能夠把它們暫存起來便於之後使用。

componentWillMount不一樣的是,在這個方法中你不可使用setState,不然會當即觸發另外一輪的渲染而且又再一次調用componentWillUpdate,陷入無限循環中。

componentDidUpdate()

和Mount階段相似,當組件進入componentDidUpdate階段時意味着最新的原生DOM已經渲染完成而且能夠經過refs進行訪問。該函數會傳入兩個參數,分別是prevPropsprevState,顧名思義是以前的狀態。你仍然能夠經過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中移除時,即會觸發這個鉤子函數。這裏沒有太多須要注意的地方,在這個函數中一般會作一些「清潔」相關的工做

  1. 將已經發送的網絡請求都取消掉
  2. 移除組件上DOM的Event Listener

總結

最後再次強調,本文是開源圖書React In-depth: An exploration of UI development的概括。基本上想了解生命週期看這一本書就夠了,看完也無敵了。但願這篇中文簡約版也會對你有幫助。

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

參考

相關文章
相關標籤/搜索