(閱讀本文約需 2 分鐘)
引言
眾所周知,在使用 Redux 時最麻煩的一個部分就是 reducer 的編寫,由於 Redux 要求狀態是 immutable 的,也就是說,發生變化的狀態樹一定是一個新的引用。 所以 reducer 經常會寫成這樣:
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
default:
return state
}
}
複製程式碼
很多人會稱之為深克隆,其實並不是,這個過程既不是深克隆也不是淺克隆。
reducer 的正確寫法
首先我們來談談深克隆是否可行,如果你的 reducer 在每次狀態發生變化時都進行深克隆處理,你的 app 毋庸置疑是可以 work 的,Time Travelling 當然也可以用,那麼問題會出在哪裡呢?
我們不妨通過圖示來看一下:
整個狀態樹被重建了,這就意味著 PureComponent
和 shouldComponentUpdate
沒有實現好的元件都會重新 render。
所以在實際專案中,我們引入了 Immutable.js,就是為了避免寫出繁瑣或者不正確的 reducer。類似的還有 immer 這樣的庫。
Immutable.js 內部會使用 Shared Structure 來避免深克隆,一方面提升了 Immutable.js 自身的效能,另一方面能幫助 React 更高效地渲染。就像這樣:
當一個物件中的一個鍵發生變化時,這個物件中其他鍵的值不會有任何變化,而引用該物件的物件會產生一份新的引用,以此類推。這樣,我們的狀態樹就可以像值型別一樣進行對比了:
節點 4 發生變化,節點 1、2 變化前後一定不相等,但是節點 3、5、6 沒有變化仍然是相等的。我們甚至不用 deepEquals
,對比引用就可以了,因為 Immutable.js 可以保證它們不發生變化。
因此,我們的 React 元件如果採用了 PureComponent,就能自動獲得最好的優化,與變化無關的元件也不會重新渲染。
Immutable.js 與 React 配合的正確用法
然而在實際使用中,我們又遇到了問題,即便使用了 Immutable.js,每次更新時還是有很多無關元件發生更新了。搜查了一遍程式碼,我發現我們現在有很多這樣的寫法:
const mapStateToProps = state => {
const user = selectCurrentUser(state)
const me = user.toJS()
const myTeam = selectMyTeam(state)
const team = myTeam && myTeam.toJS()
//...
return { user, me, myTeam, team /*, ...*/ }
}
複製程式碼
問題就出在 toJS
的呼叫上,根據文件:
Deeply converts this Keyed collection to equivalent native JavaScript Object.
toJS
會將原本 structure shared 的物件完全深克隆一遍,所有 PureComponent 又會重新渲染。可以看一下我們現在的情況:
可以看到,改變了一個與左側邊欄無關的按鈕狀態的時候,左側邊欄依舊重新渲染了。
下面是去掉了 toJS
呼叫後的情況:
是不是好多了。
總結
至此我們也能夠得出結論了,React 的渲染效能很大一部分取決於更新的粒度,當我們的 render 函式已經足夠龐大時,我們能夠做的只有分步更新(Fiber 和 Time Slicing 主要解決的問題)和精準更新了。
而要做到精準更新,就一定要處理好狀態的變化,其實最簡單的方法就是狀態扁平化,物件層級越小,我們的程式碼裡可能出現的問題就越少。另外,儘可能將 connect
放置在需要狀態的元件外,目前我們還是有很多元件過早 connect
,然後將狀態一層一層通過 props
傳下去,這也是狀態物件層級太深(有多深我就不截圖了...)導致的。Redux 狀態更新(dispatch)時,所有的 Connect(...)
元件都會根據自己的 mapped state 進行更新,越早 connect
的元件越有可能發生更新,而其子元件如果沒有處理好 shouldComponentUpdate
就會出現許多無用的更新,白白損失效能。
References: