over 3 years ago

如何避免過度 re-render 底層 component

建立一個簡單的 component 來實做 re-render 情況

class App extends React.Component {  
  constructor(props) {
    super(props)
  }

  render() {
    return(
      <div>
      {
        this.props.list.map((item, i) => {
          return <p key={item+i}>{item}</p>
        })
      }
      </div>
    )
  }
}

以上 App Component會呈現 props.list 所有內容

為了避免 props.list 內容沒做更動就不需要 re-render Component
可利用 shouldComponentUpdate 來做判斷 return 是否要更新 Component

修正如下

class App extends React.Component {  
  constructor(props) {
    super(props)
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log(nextProps.list !== this.props.list)
    return (nextProps.list !== this.props.list)
  }

  render() {
    return(
      console.log('component render');
      <div>
      {
        this.props.list.map((item) => {
          return <p key={item}>{item}</p>
        })
      }
      </div>
    )
  }
}

完成後需要一個外部的component來驗證 App Component 是否會因 props.list 差異而 re-render
外部的 component 建立兩個 button
一個更新 this.state.list 內容
另一個更新 this.state.otherList 內容
並將 this.state.list 傳遞給 App Component
內容如下

class AppWrapper extends React.Component {  
  constructor(props) {
    super(props)
    this.state = {
      list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
      otherList: [1, 2, 3, 4, 5]
    }
    this.updateList = this.updateList.bind(this)
    this.updateOtherList = this.updateOtherList.bind(this)
  }

  updateList() {
    this.setState({
      list: [...this.state.list.slice(0, 9), 11]
    })
  }

  updateOtherList() {
    this.setState({
      otherList: [...this.state.otherList.slice(0, 4), 6]
    })
  }

  render() {
    return(
      <div>
        <button onClick={ this.updateList }> 
          Update List  
        </button>  
        <button onClick={ this.updateOtherList }> 
          Update Other List  
        </button> 
        <App list={ this.state.list } />
      </div>
    )
  }
}

結果


實驗結果會發現
每次按下 Update List 按鈕時 App Component 會出現 re-render
而 updateList() 是重複執行相同 array 內容給 this.state.list
但還是會造成 App 的 shouldComponentUpdate return true 而 re-render 產生 log ‘component render’

Update Other List 按鈕則是因為沒對 this.state.list 變動, 所以 shouldComponentUpdate return false

因為 React.Component 每當 state 做變動時都會產生一個 new state
而 js array 物件沒辦法做深層的判斷
所以造成 (nextProps.list !== this.props.list) return false
但一個一個去比對 array 內部 Object 是否相同, 實在是太費工了...

Immutable Data

Immutable Data 就是一旦創建,就不能再被更改的資料
對 Immutable 物件的任何修改或添加刪除操作都會返回一個新的 Immutable 物件
Immutable 實現的原理是 Persistent Data Structure(持久化資料結構)
也就是使用舊資料創建新資料時,要保證舊資料同時可用且不變
同時為了避免 deepCopy 把所有節點都複製一遍帶來的性能損耗
Immutable 使用了 Structural Sharing(結構共用)
即如果物件樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共用

使用 Immutable 優化 Component
修改如下

class AppWrapper extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      list: Immutable.List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
      otherList: Immutable.List.of(1, 2, 3, 4, 5)
    }
    this.updateList = this.updateList.bind(this)
    this.updateOtherList = this.updateOtherList.bind(this)
  }

  updateList() {
    this.setState({
      list: this.state.list.set(9, 11)
    })
  }

  updateOtherList() {
    this.setState({
      otherList: this.state.otherList.set(4, 6)
    })
  }

  render() {
    return(
      <div>
        <button onClick={ this.updateList }>
          Update List
        </button>
        <button onClick={ this.updateOtherList }>
          Update Other List
        </button>
        <App list={ this.state.list } />
      </div>
    )
  }
}
ReactDOM.render(<AppWrapper  />, document.getElementById('img'))

結果


重新執行後

會發現 (nextProps.list !== this.props.list) 因 Immutable 而能使用 !== 判斷出差異
進而避免無謂的 re-render

而 Immutable 也提供 is API來做快速比對
Immutable.is(nextProps.list,this.props.list)

參考網站
https://github.com/camsong/blog/issues/3
http://aristid.es/optimizing-react-w-immutable/

← React 圖片遮罩 component React 遮罩視窗 Component →
 
comments powered by Disqus