本篇文章将聚焦Immutable与Redux的项目实践,将从多方面阐述Immutable及Redux:包括什么是Immutable,为什么需要使用Immutable,Immutable.js与React,Redux及reselect的组合实践及优化,最后总结使用Immutable可能遇到的一些问题及解决方式。
索引
Immutable来自于函数式编程的世界,我们可以称它为不可变,试想如下代码:
- var object = { x:1, y: 2 };
- var object2 = { x: 1, y: 2 };
- object == object2// false
- object === object2 // false
相等性检查将包括两个部分:
JavaScript的对象是一个非常复杂的数据结构,它的键可以指向任意值,包括object。JavaScript创建的对象将存储在计算机内存中(对应一个物理地址),然后它返回一个引用,JavaScript引擎通过该引用可以访问该对象,该引用赋值给某个变量后,我们便可以通过该变量以引用的方式操作该对象。引用检查即检查两个对象的引用地址是否相同。
层层循环检查对象各属性值是否相同。
React通过对组件属性(props)和状态(state)进行变更检查以决定是否更新并重新渲染该组件,若组件状态太过庞大,组件性能就会下降,因为对象越复杂,其相等性检查就会越慢。
Immutable提供一直简单快捷的方式以判断对象是否变更,对于React组件更新和重新渲染性能可以有较大帮助。
Never mutated, instead copy it and then make change.
绝对不要突然修改对象,首先复制然后修改复制对象,再返回这个新对象,保持原对象不变。
Immutable对象和原生JavaScript对象的主要差异可以概括为以下两点:
持久数据结构主张所有操作都返回该数据结构的更新副本,并保持原有结构不变,而不是改变原来的结构。通常利用 Trie 构建它不可变的持久性数据结构,它的整体结构可以看作一棵树,一个树节点可以对应代表对象某一个属性,节点值即属性值。
一旦创建一个Immutable Trie型对象,我们可以把该Trie型对象想象成如下一棵树,在之后的对象变更尽可能的重用树节点:
当我们要更新一个Immutable对象的属性值时,就是对应着需要重构该Trie树中的某一个节点,对于Trie树,我们修改某一节点只需要重构该节点及受其影响的节点,即其祖先节点,如上图中的四个绿色节点,而其他节点可以完全重用。
上一节简单介绍了什么是Immutable,本节介绍为什么需要使用Immutable。
我们不鼓励突然变更对象,因为那通常会打断时间旅行及bug相关调试,并且在react-redux的
方法中状态突变将导致组件性能低下:
- connect
方法将检查
- connect
方法返回的props对象是否变更以决定是否需要更新组件。为了提高这个检查变更的性能,
- mapStateToProps
方法基于Immutabe状态对象进行改进,使用浅引用相等性检查来探测变更。这意味着对对象或数组的直接变更将无法被探测,导致组件无法更新。
- connect
在reducer函数中的诸如生成唯一ID或时间戳的其他副作用也会导致应用状态不可预测,难以调试和测试。
若Redux的某一reducer函数返回一个可以突变的状态对象,意味着我们不能追踪,预测状态,这可能导致组件发生多余的更新,重新渲染或者在需要更新时没有响应,也会导致难以跟踪调试bug。Immutable.js能提供一种Immutable方案解决如上提到的问题,同时其丰富的API也足够支撑我们复杂的开发。
Immutable能给我们的应用提供较大的性能提升,但是我们必须正确的使用它,否则得不偿失。目前关于Immutable已经有一些类库,对于React应用,首选的是Immutable.js。
首先需要明白的是React组件状态必须是一个原生JavaScript对象,而不能是一个Immutable对象,因为React的
方法期望接受一个对象然后使用
- setState
方法将其与之前的状态对象合并。
- Object.assign
- class Component extends React.Component {
- Constructor(props) {
- super(props)
- this.state = {
- data: Immutable.Map({
- count: 0,
- todos: List()
- })
- }
- this.handleAddItemClick = this.handleAddItemClick.bind(this)
- }
- handleAddItemClick() {
- this.setState(({
- data
- }) = >{
- data: data.update('todos', todos = >todos.push(data.get('count')))
- })
- }
- render() {
- const data = this.state.data;
- Return( < div > <button onclick = {
- this.handleAddItemClick
- } > </button>
- <ul>
- {data.get('todos').map(item =>
- <li>Saved:
- {item}</li > )
- } < /ul>
- </div > )
- }
- }
,
- get()
;
- getIn()
,
- map()
等创建React元素的子元素:
- reduce()
- {data.get('todos').map(item =>
- <li>Saved:
- {item}</li>
- )}
或者
- this.setState(({data}) => ({
- data: data.update('count', v => v + 1)
- }))
- this.setState(({data}) => ({
- data: data.set('count', data.get('count') + 1)
- }));
参考:
React本身是专注于视图层的一个JavaScript类库,所以其单独使用时状态一般不会过于复杂,所以其和Immutable.js的协作比较简单,更重要也是我们需要更多关注的地方是其与React应用状态管理容器的协作,下文就Immutable.js如何高效的与Redux协作进行阐述。
原始Redux的
方法期望接受原生JavaScript对象并且它把state作为原生对象处理,所以当我们使用
- combineReducers
方法并且接受一个Immutable对象作应用初始状态时,
- createStore
将会返回一个错误,源代码如下:
- reducer
- if (!isPlainObject(inputState)) {
- return (`The $ {
- argumentName
- }
- has unexpected type of "` + ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
- ".Expected argument to be an object with the following + `keys: "${reducerKeys.join('", "')}"`)
- }
如上表明,原始类型reducer接受的state参数应该是一个原生JavaScript对象,我们需要对
其进行增强,以使其能处理Immutable对象,redux-immutable 即是用来创建一个可以和 Immutable.js 协作的Redux combineReducers 。
- combineReducers
- const StateRecord = Immutable.Record({
- foo: 'bar'
- });
- const rootReducer = combineReducers({
- first: firstReducer
- }, StateRecord);
如果在项目中使用了react-router-redux类库,那么我们需要知道routeReducer不能处理Immutable,我们需要自定义一个新的reducer:
- import Immutable from 'immutable';
- import { LOCATION_CHANGE } from 'react-router-redux';
- const initialState = Immutable.fromJS({
- locationBeforeTransitions: null
- });
- export default (state = initialState, action) => {
- if (action.type === LOCATION_CHANGE) {
- return state.set('locationBeforeTransitions', action.payload);
- }
- return state;
- };
当我们使用
方法连接history对象和store时,需要将routing负载转换成一个JavaScript对象,如下传递一个
- syncHistoryWithStore
参数给
- selectLocationState
方法:
- syncHistoryWithStore
- import { browserHistory } from 'react-router';
- import { syncHistoryWithStore } from 'react-router-redux';
- const history = syncHistoryWithStore(browserHistory, store, {
- selectLocationState (state) {
- return state.get('routing').toJS();
- }
- });
当使用Immutable.js和Redux协作开发时,可以从如下几方面思考我们的实践。
方法将JavaScript对象转换为Immutable对象,然后使用
- fromJS()
,
- update()
,
- merge()
等更新API对Immutable对象进行更新操作;
- set()
- // avoid
- const newObj = { key: value }
- const newState = state.setIn(['prop1'], newObj)
- // newObj has been added as a plain JavaScript object, NOT as an Immutable.JS Map
- // recommended
- const newObj = { key: value }
- const newState = state.setIn(['prop1'], fromJS(newObj))
方法创建状态树 状态树对象可以是一个Immutable.Record或者任何其他的实现了
- fromJS()
,
- get
,
- set
方法的Immutable集合的实例。
- withMutations
方法使其能处理Immutable。
- combineReducers
当使用Redux作React应用状态管理容器时,我们通常将组件分为容器组件和展示型组件,Immutable与Redux组件的实践也主要围绕这两者。
方法访问redux的store,所以我们需要保证选择器(selectors)总是返回Immutable对象,否则,将会导致不必要的重新渲染。另外,我们可以使用诸如reselect的第三方库缓存选择器(selectors)以提高部分情景下的性能。
- connect
方法功能就是把一个Immutable对象转换为一个JavaScript对象,而我们通常尽可能将Immutable对象转换为JavaScript对象这一操作放在容器组件中,这也与容器组件的宗旨吻合。另外
- toJS()
方法性能极低,应该尽量限制该方法的使用,如在
- toJS
方法和展示型组件内。
- mapStateToProps
方法内使用
- mapStateToProps
方法
- toJS()
方法每次会调用时都是返回一个原生JavaScript对象,如果在
- toJS()
方法内使用
- mapStateToProps
方法,则每次状态树(Immutable对象)变更时,无论该
- toJS()
方法返回的JavaScript对象是否实际发生改变,组件都会认为该对象发生变更,从而导致不必要的重新渲染。
- toJS()
方法 如果传递给某组件一个Immuatble对象类型的prop,则该组件的渲染取决于该Immutable对象,这将给组件的重用,测试和重构带来更多困难。
- toJS()
该高阶组件内,首先使用
- import React from 'react'
- import { Iterable } from 'immutable'
- export const toJS = WrappedComponent => wrappedComponentProps => {
- const KEY = 0
- const VALUE = 1
- const propsJS = Object.entries(wrappedComponentProps)
- .reduce((newProps, wrappedComponentProp) => {
- newProps[wrappedComponentProp[KEY]] = Iterable.isIterable(wrappedComponentProp[VALUE]) ? wrappedComponentProp[VALUE].toJS() : wrappedComponentProp[VALUE]
- return newProps
- }, {})
- return <WrappedComponent {...propsJS} />
- }
方法遍历传入组件的props,然后使用
- Object.entries
方法将该组件内Immutable类型的prop转换为JavaScript对象,该高阶组件通常可以在容器组件内使用,使用方式如下:
- toJS()
这类高阶组件不会造成过多的性能下降,因为高阶组件只在被连接组件(通常即展示型组件)属性变更时才会被再次调用。你也许会问既然在高阶组件内使用
- import { connect } from 'react-redux'
- import { toJS } from './to-js'
- import DumbComponent from './dumb.component'
- const mapStateToProps = state => {
- return {
- // obj is an Immutable object in Smart Component, but it’s converted to a plain
- // JavaScript object by toJS, and so passed to DumbComponent as a pure JavaScript
- // object. Because it’s still an Immutable.JS object here in mapStateToProps, though,
- // there is no issue with errant re-renderings.
- obj:getImmutableObjectFromStateTree(state)
- }
- }
- export default connect(mapStateToProps)(toJS(DumbComponent))
方法必然会造成一定的性能下降,为什么不在展示型组件内也保持使用Immutable对象呢?事实上,相对于高阶组件内使用
- toJS()
方法的这一点性能损失而言,避免Immutable渗透入展示型组件带来的可维护性,可重用性及可测试性是我们更应该看重的。
- toJS()
使用Redux管理React应用状态时,
方法作为从Redux Store上获取数据过程中的重要一环,它一定不能有性能缺陷,它本身是一个函数,通过计算返回一个对象,这个计算过程通常是基于Redux Store状态树进行的,而很明显的Redux状态树越复杂,这个计算过程可能就越耗时,我们应该要能够尽可能减少这个计算过程,比如重复在相同状态下渲染组件,多次的计算过程显然是多余的,我们是否可以缓存该结果呢?这个问题的解决者就是reselect,它可以提高应用获取数据的性能。
- mapStateToProps
reselect的原理是,只要相关状态不变,即直接使用上一次的缓存结果。
reselect通过创建选择器(selectors),该函数接受一个state参数,然后返回我们需要在
方法内返回对象的某一个数据项,一个选择器的处理可以分为两个步骤:
- mapStateToProps
相等性检查,性能是足够的。
- ===
如上,createSelector方法,接受两个参数:
- import { createSelector } from 'reselect'
- import { FilterTypes } from '../constants'
- export const selectFilterTodos = createSelector(
- [getTodos, getFilters],
- (todos, filters) => {
- switch(filters) {
- case FilterTypes.ALL:
- return todos;
- case FilterTypes.COMPLETED:
- return todos.filter((todo) => todo.completed)
- default:
- return todos
- }
- }
- )
方法所需的数据;
- mapStateToProps
内使用该选择器函数,接受state参数:
- mapStateToProps
上文中的映射函数,内容如:
- const mapStateToProps = (state) = >{
- return {
- todos: selectFilterTodos(state)
- }
- }
- const getTodos = (state) => {state.todos}
- const getFilter = (state) => {state.filter}
如上文为例,reselect是完全可以和Immutable独立使用的,如果使用了Immutable,我们需要进行一些额外修改以使Immutable.js与reselect的结合使用。
首先,修改映射函数:
- const getTodos = (state) => {state.get('todos')}
- const getFilter = (state) => {state.get('filter')}
特别需要注意的是在选择器第二步处理函数内,可能会涉及Immutable操作,也需要额外修改成Immutable对应方式。
无论什么情况,都不存在绝对完美的事物或者技术,使用Immutable.js也必然会带来一些问题,我们能做的则是尽量避免或者尽最大可能的分化这些问题,而可以更多的去发扬该技术带来的优势,使用Immutable.js最常见的问题如下。
,
- get
等API方式;
- getIn
方法时,尽管对象实际没有变更,但是它们之间的等值检查不能通过,将导致重新渲染。更重要的是如果我们在
- toJS
方法内使用
- mapStateToProps
将极大破坏组件性能,如果真的需要,我们应该使用前面介绍的高阶组件方式转换。
- toJS
来源: http://www.tuicool.com/articles/QriYv2B