React 渲染優化:diff 與 shouldComponentUpdate

原文鏈接:https://ssshooter.com/2019-03...

我曾經對 shouldComponentUpdate 的用途不解。react 的賣點之一,是通過 diff 虛擬節點樹,減少對真實節點的操作,所以我以前以爲既然 diff 了,那就自然知道節點有沒有更新了,diff 是根據 setState 的內容進行的,那 shouldComponentUpdate 有什麼用呢?

然而我以前的理解是完全錯誤的,造成這個疑問的原因便是對 React 渲染流程的不熟悉。從頭說起。

setState

你修改了數據,需要 React 重新渲染頁面,讓你的新數據展示在頁面上,需要藉助 setState 方法。

setState 調用後,組件的 render 方法也會自動調用,這就是爲什麼你能在頁面看到新數據。但是無論你 setState 修改的是什麼,哪怕是頁面裏沒有的一個數據,render 都會被觸發,並且父組件渲染中會嵌套渲染自組件。

class Nest extends React.Component {
  render() {
    console.log('inner')
    return <div>Nest</div>
  }
}

class App extends React.Component {
  render() {
    console.log('outer')
    return (
      <div>
        <button
          onClick={() => {
            this.setState({
              anything: 1,
            })
          }}
        >
          setState
        </button>
        <Nest />
      </div>
    )
  }
}

所以在這個例子中,點擊按鈕,即使修改的 anything 根本沒有出現,甚至沒有定義,render 函數還是如期運行。每次點擊按鈕,上面的代碼會先輸出 outer,然後輸出 inner。

render

render 生成的是什麼呢?一般來說大家都是寫 jsx,所以視覺上是一個“dom”,但是實際上,官網也在顯眼的位置告訴你,這其實是一個函數。

// jsx
const element = <h1 className="greeting">Hello, world!</h1>
// babel 轉換爲瀏覽器能運行的函數
const element = React.createElement(
  'h1',
  { className: 'greeting' },
  'Hello, world!'
)

而因爲 React 的組件層層嵌套,render 函數會生成一棵描述應用結構的節點樹,並保存在內存中。在下一次渲染時,新的樹會被生成,接着就是對比兩棵樹。

diff

官方一點的定義應該稱爲 reconciliation,也就是 React 用來比較兩棵節點樹的算法,它確定樹中的哪些部分需要被更新。

在確定兩棵樹的區別後,會根據不同的地方對實際節點進行操作,這樣你看到的界面終於在這一步得到了改變。當年 React 也就因爲這個高效的 dom 操作方法得到追捧。

shouldComponentUpdate

終於說到 shouldComponentUpdate,他是一個組件的方法,用於攔截組件渲染。讓我們用例子解釋所謂“攔截渲染”。

class Nest extends React.Component {
  shouldComponentUpdate = () => { // <---- 注意這裏
    return false
  }
  render() {
    console.log('inner')
    return <div>Nest</div>
  }
}

class App extends React.Component {
  render() {
    console.log('outer')
    return (
      <div>
        <button
          onClick={() => {
            this.setState({
              anything: 1,
            })
          }}
        >
          setState
        </button>
        <Nest />
      </div>
    )
  }
}

跟之前的例子差不多,不過當我們在子組件添加 shouldComponentUpdate 後,再點擊按鈕,結果是 ————

沒錯,子組件的渲染函數並沒有調用,藉助 shouldComponentUpdate 返回 false,成功攔截了子組件的渲染。

當然一般不會這麼做,因爲永遠返回 false 的話這個組件(當然因爲渲染函數沒有運行,所以包括其所有子組件都是不會更新的)就永遠不會更新了。

常用操作是,在 shouldComponentUpdate 判定該組件的 props 和 state 是否有變化,就像這樣:

class Nest extends React.Component {
  shouldComponentUpdate = (nextProps, nextState) => {
    return (
      !shallowEqual(this.props, nextProps) ||
      !shallowEqual(this.state, nextState)
    )
  }
  render() {
    console.log('inner')
    return <div>Nest</div>
  }
}

這樣可以淺比較 props 和 state 是否有變化,至於爲什麼不深比較?因爲那樣效率可能會比直接全都運行 render 還低...

因爲上面的操作太常見,React 直接爲我們提供了 PureComponent:

class Nest extends React.PureComponent {
  render() {
    console.log('inner')
    return <div>Nest</div>
  }
}

使用 PureComponent 的效果就與上面淺比較一樣,並且省掉了 shouldComponentUpdate。

什麼時候用?

PureComponent 能提高性能!所以直接用 PureComponent 代替所有 Component!

這當然是錯的。

對於明知道需要修改的組件,肯定直接返回 false。而可能你沒想到,對於明知道需要修改的組件,也請不要使用 PureComponent。

因爲正如上面所說,PureComponent 需要進行兩次淺比較,而淺比較也是要時間的,若是你明知道這個組件百分百要修改,何必浪費時間去對比呢?

所以 PureComponent 請用在較少進行修改的組件上。

總結

總結一下以上內容,整個流程基本如下:

clipboard.png

本文部分存在個人理解,如果文中有不嚴謹的地方,請在評論區指出,謝謝大家的閱讀。

參考文獻:

https://reactjs.org/docs/faq-...

https://reactjs.org/docs/opti...

https://github.com/xitu/gold-...

https://cdb.reacttraining.com...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章