為什麼React和Immutable是好朋友

請叫我王磊同學發表於2017-05-13

  目前工作中用到了React,搭配一起使用了Immutable.js。之前沒有靜下來思考一下為什麼React社群這麼推崇搭配一起使用Immutable。正好想寫篇文章分析一下這個問題。之前我翻譯了React官方文件中Advanced Guides中關於一致化處理(Reconciliation)效能優化(Optimizing Performance)就涉及到這方面的內容。

React效能優化

  一提到React,大家第一時間就想到的虛擬DOM(Virtual DOM)和伴隨其帶來的高效能。但是React提供的是宣告式的API(declarative API),好的一方面是讓我們編寫程式更加方便,但另一方面,卻使得我們不太瞭解內部細節。

一致化處理(Reconciliation)

  React採用的是虛擬DOM,每次屬性(props)和狀態(state)發生變化的時候,render函式返回不同的元素樹,React會檢測當前返回的元素樹和上次渲染的元素樹之前的差異,然後找出何如高效的更新UI。

為什麼React和Immutable是好朋友
react-tree

  上圖展示的就是一個元素樹,React比較兩次元素樹差異的時候,首先從根節點開始。如果元素型別不相同時,該節點以下(包括當前節點)的元素樹都會被銷燬,樹的根節點以下的任何元件都會被解除安裝,因此狀態(state)也會丟失。如果元素節點型別是相同的,那就需要區分是DOM元素還是元件。如果是DOM元素,會保持節點相同,僅更新改變的屬性。而如果是元件的話,會保持元件例項不變,僅更新元件例項的屬性,因此元件例項的狀態(state)就會被保留下來。比較完當前節點,然後會遞迴遍歷比較子元素。

  一致化處理(Reconciliation)包括的就是React元素的比較以及對應的React元素不同時對DOM的更新,即可理解為React 內部將虛擬 DOM 同步更新到真實 DOM 的過程,包括新舊虛擬 DOM 的比較及計算最小 DOM 操作。我們可以看到促使React效能提升的一個重要點就是避免一致化處理。

shouldComponentUpdate

  React使用shouldComponentUpdate來判別元件是否會因為當前屬性(props)和狀態(state)變化而導致元件輸出變化。預設的shouldComponentUpdate會在props和state發生變化時返回true,表示元件會重新渲染,從而呼叫render函式。當然了在首次渲染的時候和使用forceUpdate的時候,是不會經過shouldComponentUpdate判斷。shouldComponentUpdate作為效能優化的一個非常有用且簡單的方法,非常實用。   

舉例

  我們以React官網中的圖作為例項:

為什麼React和Immutable是好朋友
diff.png

  SCU代表shouldComponentUpdate,紅色SCU表示shouldComponentUpdate返回true,綠色的SCU表示shouldComponentUpdate返回false。vDOMEq代表渲染的React元素是否相等。紅色vDOMEq表示React元素不相等,綠色的vDOMEq表示React元素相等。元素節點為紅色表示需要對該節點進行一致化處理,節點顏色為綠色表示不需要對其進行一致化處理。

  表示對於兩棵元素樹,React會同步比較。在比較C1節點,因為SCU返回的false,需要對其進行diff,vDOMEq返回的是false,故需要一致化處理,存在DOM元素的更新。迭代遞迴到C2,因為SCU返回的是true,以C2為根節點的整個子樹,都不需要diff判斷。但是C3的SCU返回true,需要進行diff比較。C3的子節點C6因為SCU返回true需要進行diff比較,並且因為vDOMEq返回的false,因此C6不可避免進行DOM的更新。對於C8來講,通過比較渲染元素而不需要進行一致化處理,而C7因為shouldComponentUpdate返回false從而不需要進行diff。

  因此我們可以發現,如果能夠合理地編寫shouldComponentUpdate函式,從而能避免不必要的一致化處理,使得效能可以極大提高。一般shouldComponentUpdate會比較propsstate中的屬性是否發生改變(淺比較)來判定是否shouldComponentUpdate是否需要返回true從而觸發一致化處理。我們可以通過繼承React.PureComponent或者通過引入PureRenderMixin模組來達到目的。但是這也存在一個問題:

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // This section is bad style and causes a bug
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}複製程式碼

  元件展現一個以逗號分隔的單詞列表,在父元件WordAdder,當你點選一個按鈕時會給列表新增一個單詞,但實際上,上面的程式碼是存在問題的,ListOfWords繼承的React.PureComponent,當你每次點選按鈕會給WordAdder元件中的this.state.words新增新的單詞。因此ListOfWords中的shouldComponentUpdate在判斷this.props.wordsnextProps.words實際是相等的,因此返回了false。所以,ListOfWords是不會被重新渲染的,因為React.PureComponent中的shouldComponentUpdate進行的是淺比較(shallow comparison),但是如果真的進行深比較,那麼比較的效能損耗又太大,不禁讓我們得出一個結論:

共享的可變狀態是萬惡之源

這時候Immutable.js橫空出世

Immutable Data

  Immutable Data是指一旦建立,就不能被更改的資料。對Immutable物件的修改都會返回新的Immutable物件。並且目前的Immutable庫,都實現了結構共享,即如果物件樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共享,避免了deepCopy把所有節點都複製一遍帶來的效能損耗。比較兩個Immutable物件是否相同,只需要使用===就可以輕鬆判別。因此如果React傳入的資料是Immutable Data,那麼React就能高效地比較前後屬性的變化,從而決定shouldComponentUpdate的返回值。解決了上面存在的問題。
  但是引入Immutable Data也不是沒有代價的,畢竟Immutable Data需要引入新的API,並且需要引入新的庫,在原有的專案中引入Immutable Data也是有風險和代價的而且還需要開發者轉變原有的思維(畢竟天下沒有白吃的午餐)。

相關文章