(阅读本文约需 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 https://facebook.github.io/immutable-js , 就是为了避免写出繁琐或者不正确的 reducer. 类似的还有 https://github.com/mweststrate/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:
Immutable Data Structures and JavaScript https://jlongster.com/Using-Immutable-Data-Structures-in-JavaScript#Reference---Value-Eq
- Reducers - Redux https://redux.js.org/basics/reducers
- Map -- Immutable.js https://facebook.github.io/immutable-js/docs/#/Map/toObject
来源: https://juejin.im/post/5b010d6851882542c83304eb