React 渲染優化:diff 與 shouldComponentUpdate

ssshooter發表於2019-03-22

原文連結:ssshooter.com/2019-03-15-…

我曾經對 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 請用在較少進行修改的元件上。

總結

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

流程.png

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

參考文獻:

reactjs.org/docs/faq-in…

reactjs.org/docs/optimi…

github.com/xitu/gold-m…

cdb.reacttraining.com/react-inlin…

相關文章