所有文章

react-redux 與 Immutable 優化

react-redux

兩個前提:

  1. shouldComponentUpdate 默認返回 true,即只要組件的狀態(props 或者 state)發生改變,組件就會執行 render 函數進行重渲染。除非重寫 shouldComponentUpdate 透過返回 false 阻止重渲染,或者讓組件直接繼承自 PureComponent
  2. PureComponent 的原理只不過代替你實現了 shouldComponentUpdate:在函數內對當前和過去的 props/state 進行淺對比(即僅比較對象的引用而非比較對象每個屬性值)。

其實在 react-redux 中實現了這套邏輯,對數據進行淺對比:

import { connect } from 'react-redux'

function mapStateToProps(state) {
  return {
    todos: state.todos,
    visibleTodos: getVisibleTodos(state),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

react-redux 會假設 App 是一個 PureComponent,即對於唯一的 props/state 有唯一的渲染結果。故 react-redux 首先會對根狀態(即上述代碼中 mapStateToProps 的第一個形參 state)創建索引,進行淺對比,若對比結果一致則不對組件進行重渲染,否則繼續調用 mapStateToProps 函數;同時繼續對 mapStateToProps 返回的 props 對象中每個屬性值(state.todosgetVisibleTodos(state) 值)創建索引。和 shouldComponentUpdate 類似,只有當淺對比失敗,即索引發生變動時才會對封裝組件進行重渲染。

只有 state.todosgetVisibleTodos(state) 值不變,那麼 App 組件就永不會重渲染。但注意下面的陷阱模式:

function mapStateToProps(state) {
  return {
    data: {
      todos: state.todos,
      visibleTodos: getVisibleTodos(state),
    }
  }
}

即使兩者不再變化,但由於每次 mapStateToProps 返回結果 { data: { ... } } 中的 data 都是新創建的字面量對象,導致淺對比失敗,App 依然會重渲染。

其次是 combineReducers。Redux Store 鼓勵我們把狀態對象劃分為不同碎片(slices)或領域(domains,業務),並分別編寫 reducer 函數以管理其狀態,最後使用 combineReducers 將這些領域及其 reducer 關聯起來,拼裝成一個整體的 state

combineReducers({ todos: myTodoReducer, counter: myCounterReducer })

combineReducers 會遍歷每一對領域(key 是領域名,value 是 reducer),對於每次遍歷:

  1. 創建一個對當前碎片數據的引用
  2. 調用 reducer 計算碎片數據的新狀態,並返回
  3. 為 reducer 返回的新碎片數據創建新的引用,將新引用與當前數據進行淺對比,若對比失敗(意味著兩次引用不同,即 reducer 返回的是一個新對象),則將標識位 hasChanged 設為 true

遍歷完後,combineReducers 就得到一個新狀態對象,透過 hasChanged 標識位就能判斷出整體狀態是否發生更改,若為 true 則新狀態會被返回給下游(下游指 react-redux 及其更下游的介面組件)。

綜上所述我們知道,當狀態需要發生更改時,務必讓相應的 reducer 函數始終返回新對象。修改原有對象的屬性值然後返回,並不會觸發組件的重渲染。

return Object.assign({}, state, { count: state.count++ })
// 而非僅修改原對象:
state.count++
return state

Immutable Data 和 ImmutableJS

結上可知,無論是從 reducer 的定義上,還是從 Redux 的工作機制上,我們都走上了同一條 Object.assign 的模式,即不修改原狀態,只返回新狀態。可見 state 天生就是不可變異的(immutable)。

使用 ImmutableJS 能實現幾類不可變異的數據結構,譬如 MapList

import { Map } from 'immutable'
const person = Map()    // 創建一個空對象
const personWithAge = person.set('age', 20) // 為 person 實例添加 age 屬性
// 調用 toJS() 打印出兩個實例
console.log(person.toJS()) // {},person 的屬性不變
console.log(personWithAge.toJS()) // { age: 20 }

在 Immutable 的數據結構中,當你想更改某個對象屬性時,你得到的永遠是一個新對象,而原對象不會變化。上述 reducer 中:

return state.set('count', state.get('count') + 1);

Immutable 的原理:

  1. 有這樣一個 JavaScript 結構對象,根據 key 作為索引,組織成字典查找樹: image
  2. 假設此時你想修改 tea 屬性的值為 14,首先找到訪問到 tea 節點的關鍵路徑: image
  3. 然後將路徑上的節點複製出來,構建一棵一模一樣結構的樹,唯新樹的其他節點均是對原樹的引用: image
  4. 最後將新建的樹返回。

完 :)

發佈於 2 Sep 2016

慎獨|純亦不已
Jason Lam on Twitter