欢迎访问个人博客Immutable.js与React,Redux及reselect的实践javascript
本篇文章将聚焦Immutable与Redux,reselect的项目实践,将从多方面阐述Immutable及Redux:包括什么是Immutable,为何须要使用Immutable,Immutable.js与React,Redux及reselect的组合实践及优化,最后总结使用Immutable可能遇到的一些问题及解决方式。html
Immutable来自于函数式编程的世界,咱们能够称它为不可变,试想以下代码:java
var object = { x:1, y: 2 };
var object2 = { x: 1, y: 2 };
object == object2// false
object === object2 // false复制代码
相等性检查将包括两个部分:react
JavaScript的对象是一个很是复杂的数据结构,它的键能够指向任意值,包括object。JavaScript建立的对象将存储在计算机内存中(对应一个物理地址),而后它返回一个引用,JavaScript引擎经过该引用能够访问该对象,该引用赋值给某个变量后,咱们即可以经过该变量以引用的方式操做该对象。引用检查即检查两个对象的引用地址是否相同。git
层层循环检查对象各属性值是否相同。github
React经过对组件属性(props)和状态(state)进行变动检查以决定是否更新并从新渲染该组件,若组件状态太过庞大,组件性能就会降低,由于对象越复杂,其相等性检查就会越慢。编程
Immutable提供一直简单快捷的方式以判断对象是否变动,对于React组件更新和从新渲染性能能够有较大帮助。redux
Never mutated, instead copy it and then make change.api
绝对不要忽然修改对象,首先复制而后修改复制对象,再返回这个新对象,保持原对象不变。数组
Immutable对象和原生JavaScript对象的主要差别能够归纳为如下两点:
持久数据结构主张全部操做都返回该数据结构的更新副本,并保持原有结构不变,而不是改变原来的结构。一般利用Trie构建它不可变的持久性数据结构,它的总体结构能够看做一棵树,一个树节点能够对应表明对象某一个属性,节点值即属性值。
一旦建立一个Immutable Trie型对象,咱们能够把该Trie型对象想象成以下一棵树,在以后的对象变动尽量的重用树节点:
当咱们要更新一个Immutable对象的属性值时,就是对应着须要重构该Trie树中的某一个节点,对于Trie树,咱们修改某一节点只须要重构该节点及受其影响的节点,即其祖先节点,如上图中的四个绿色节点,而其余节点能够彻底重用。
上一节简单介绍了什么是Immutable,本节介绍为何须要使用Immutable。
咱们不鼓励忽然变动对象,由于那一般会打断时间旅行及bug相关调试,而且在react-redux的connect
方法中状态突变将致使组件性能低下:
connect
方法将检查mapStateToProps
方法返回的props对象是否变动以决定是否须要更新组件。为了提升这个检查变动的性能,connect
方法基于Immutabe状态对象进行改进,使用浅引用相等性检查来探测变动。这意味着对对象或数组的直接变动将没法被探测,致使组件没法更新。在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>
)
}
}复制代码
使用Immutable.js的访问API访问state,如get()
,getIn()
;
使用Immutable.js的集合操做生成组件子元素:
使用高阶函数如map()
,reduce()
等建立React元素的子元素:
{data.get('todos').map(item =>
<li>Saved:
{item}</li>
)}复制代码
使用Immutable.js的更新操做API更新state;
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中讲状态(state)主要是指应用状态,而不是组件状态。
原始Redux的combineReducers
方法指望接受原生JavaScript对象而且它把state做为原生对象处理,因此当咱们使用createStore
方法而且接受一个Immutable对象做应用初始状态时,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对象,咱们须要对combineReducers
其进行加强,以使其能处理Immutable对象,redux-immutable 便是用来建立一个能够和Immutable.js协做的Redux 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;
};复制代码
当咱们使用syncHistoryWithStore
方法链接history对象和store时,须要将routing负载转换成一个JavaScript对象,以下传递一个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协做开发时,能够从以下几方面思考咱们的实践。
不要在Immutable对象中混用原生JavaScript对象;
当在Immutable对象内添加JavaScript对象时,首先使用fromJS()
方法将JavaScript对象转换为Immutable对象,而后使用update()
,merge()
,set()
等更新API对Immutable对象进行更新操做;
// 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对象表示完整的Redux状态树;
对于一个Redux应用,完整的状态树应该由一个Immutable对象表示,而没有原生JavaScript对象。
使用fromJS()
方法建立状态树
状态树对象能够是一个Immutable.Record或者任何其余的实现了get
,set
,withMutations
方法的Immutable集合的实例。
使用redux-immutable库调整combineReducers
方法使其能处理Immutable。
当使用Redux做React应用状态管理容器时,咱们一般将组件分为容器组件和展现型组件,Immutable与Redux组件的实践也主要围绕这二者。
除了在展现型组件内,其余地方一概使用Immutable方式操做状态对象;
为了保证应用性能,在容器组件,选择器(selectors),reducer函数,action建立函数,sagas和thunks函数内等全部地方均使用Immutable,可是不在展现型组件内使用。
在容器组件内使用Immutable
容器组件可使用react-redux提供的connect
方法访问redux的store,因此咱们须要保证选择器(selectors)老是返回Immutable对象,不然,将会致使没必要要的从新渲染。另外,咱们可使用诸如reselect的第三方库缓存选择器(selectors)以提升部分情景下的性能。
toJS()
方法功能就是把一个Immutable对象转换为一个JavaScript对象,而咱们一般尽量将Immutable对象转换为JavaScript对象这一操做放在容器组件中,这也与容器组件的宗旨吻合。另外toJS
方法性能极低,应该尽可能限制该方法的使用,如在mapStateToProps
方法和展现型组件内。
绝对不要在mapStateToProps
方法内使用toJS()
方法
toJS()
方法每次会调用时都是返回一个原生JavaScript对象,若是在mapStateToProps
方法内使用toJS()
方法,则每次状态树(Immutable对象)变动时,不管该toJS()
方法返回的JavaScript对象是否实际发生改变,组件都会认为该对象发生变动,从而致使没必要要的从新渲染。
绝对不要在展现型组件内使用toJS()
方法
若是传递给某组件一个Immuatble对象类型的prop,则该组件的渲染取决于该Immutable对象,这将给组件的重用,测试和重构带来更多困难。
当容器组件将Immutable类型的属性(props)传入展现型组件时,需使用高阶组件(HOC)将其转换为原生JavaScript对象。
该高阶组件定义以下:
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} />
}复制代码
该高阶组件内,首先使用Object.entries
方法遍历传入组件的props,而后使用toJS()
方法将该组件内Immutable类型的prop转换为JavaScript对象,该高阶组件一般能够在容器组件内使用,使用方式以下:
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))复制代码
这类高阶组件不会形成过多的性能降低,由于高阶组件只在被链接组件(一般即展现型组件)属性变动时才会被再次调用。你也许会问既然在高阶组件内使用toJS()
方法必然会形成必定的性能降低,为何不在展现型组件内也保持使用Immutable对象呢?事实上,相对于高阶组件内使用toJS()
方法的这一点性能损失而言,避免Immutable渗透入展现型组件带来的可维护性,可重用性及可测试性是咱们更应该看重的。
使用Redux管理React应用状态时,mapStateToProps
方法做为从Redux Store上获取数据过程当中的重要一环,它必定不能有性能缺陷,它自己是一个函数,经过计算返回一个对象,这个计算过程一般是基于Redux Store状态树进行的,而很明显的Redux状态树越复杂,这个计算过程可能就越耗时,咱们应该要可以尽量减小这个计算过程,好比重复在相同状态下渲染组件,屡次的计算过程显然是多余的,咱们是否能够缓存该结果呢?这个问题的解决者就是reselect,它能够提升应用获取数据的性能。
reselect的原理是,只要相关状态不变,即直接使用上一次的缓存结果。
reselect经过建立选择器(selectors),该函数接受一个state参数,而后返回咱们须要在mapStateToProps
方法内返回对象的某一个数据项,一个选择器的处理能够分为两个步骤:
接受state参数,根据咱们提供的映射函数数组分别进行计算,若是返回结果和上次第一步的计算结果一致,说明命中缓存,则不进行第二步计算,直接返回上次第二步的计算结果,不然继续第二步计算。第一步的结果比较,一般仅仅是===
相等性检查,性能是足够的。
根据第一步返回的结果,计算,返回最终结果。
以TODO为例,有以下选择器函数:
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
}
}
)复制代码
如上,createSelector方法,接受两个参数:
mapStateToProps
方法所需的数据;而后在mapStateToProps
内使用该选择器函数,接受state参数:
const mapStateToProps = (state) => {
return {
todos: selectFilterTodos(state)
}
}复制代码
上文中的映射函数,内容如:
const getTodos = (state) => {state.todos}
const getFilter = (state) => {state.filter}复制代码
另外须要注意的是,传入createSelector
的映射函数返回的状态应该是不可变的,由于默认缓存命中检测函数使用引用检查,若是使用JavaScript对象,仅改变该对象的某一属性,引用检测是没法检测到属性变动的,这将致使组件没法响应更新。在缓存结果处理函数内执行以下代码,是不行的:
todos.map(todo => {
todo.completed = !areAllMarked
return todo
})复制代码
这种忽然性的改变某一状态对象后,其差别检测没法经过,将命中缓存,没法更新,在未使用Immutable.js库时,应该采用以下这种方式:
todos.map(todo => Object.assign({}, todo, {
completed: !areAllMarked
}))复制代码
老是返回一个新对象,而不影响原对象。
前面使用createSelector
方法建立的选择器函数默认缓存间隔是1,只缓存上一次的计算结果,即选择器处理流程的第一步,仅会将当前计算结果与紧邻的上一次计算结果对比。
有时候也许咱们会想是否能够加大缓存程度呢?好比当前状态a,变化到状态b,此时缓存的仅仅是状态b下的选择器计算结果,若是状态再次变为a,比对结果天然是false,依然会执行复杂的计算过程,那咱们是否能缓存第一次状态a下的选择器计算结果呢?答案就在createSelectorCreator
。
defaultMemoize(func, equalityCheck = defaultEqualityCheck)复制代码
defaultMemoize将缓存传递的第一个函数参数func
的返回结果,该函数是使用createSelector
建立选择器时传入的缓存结果处理函数,其默认缓存度为1。
equalityCheck
是建立的选择器使用的缓存命中检测函数,默认函数代码如:
function defaultEqualityCheck(currentVal, previousVal) {
return currentVal === previousVal
}复制代码
只是简单的进行引用检查。
createSelectorCreator
方法支持咱们建立一个自定义的createSelector
函数,而且支持咱们传入自定义的缓存计算函数,覆盖默认的defaultMemoize
函数,定义格式以下:
createSelectorCreator(memoize, ...memoizeOptions)复制代码
memoize
参数是一个缓存函数,用以替代defaultMemoize
,该函数接受的第一个参数就是建立选择器时传入的缓存结果处理函数;…memoizeOptions
是0或多个配置对象,将传递给memoize
缓存函数做为后续参数,如能够传递一个自定义缓存检测函数覆盖defaultEqualityCheck
;// 使用lodash.isEqual覆盖默认的‘===’引用等值检测
import isEqual from 'lodash.isEqual'
import { createSelectorCreator, defaultMemoize } from 'reselect'
// 自定义选择器建立函数
const customSelectorCreator = createSelectorCreator(
customMemoize, // 自定义缓存函数,也能够直接使用defaultMemoize
isEqual, // 配置项
option2 // 配置项
)
// 自定义选择器
const customSelector = customSelectorCreator(
input1, // 映射函数
input2, // 映射函数
resultFunc // 缓存结果处理函数
)
// 调用选择器
const mapStateToProps = (state) => {
todos: customSelector(state)
}复制代码
在自定义选择器函数内部,会执行缓存函数:
customMemoize(resultFunc, isEqual, option2)复制代码
如上文为例,reselect是内在须要使用Immutable概念数据的,当咱们把整个Redux状态树Immutable化之后,须要进行一些修改。
修改映射函数:
const getTodos = (state) => {state.get('todos')}
const getFilter = (state) => {state.get('filter')}复制代码
特别须要注意的是在选择器第二步处理函数内,若是涉及Immutable操做,也须要额外修改为Immutable对应方式。
不管什么状况,都不存在绝对完美的事物或者技术,使用Immutable.js也必然会带来一些问题,咱们能作的则是尽可能避免或者尽最大可能的分化这些问题,而能够更多的去发扬该技术带来的优点,使用Immutable.js最多见的问题以下。
很难进行内部协做
Immutable对象和JavaScript对象之间存在的巨大差别,使得二者之间的协做一般较麻烦,而这也正是许多问题的源头。
get
,getIn
等API方式;渗透整个代码库
Immutable代码将渗透入整个项目,这种对于外部类库的强依赖会给项目的后期带来很大约束,以后若是想移除或者替换Immutable是很困难的。
不适合常常变动的简单状态对象
Immutable和复杂的数据使用时有很大的性能提高,可是对于简单的常常变动的数据,它的表现并很差。
切断对象引用将致使性能低下
Immutable最大的优点是它的浅比较能够极大提升性能,当咱们屡次使用toJS
方法时,尽管对象实际没有变动,可是它们之间的等值检查不能经过,将致使从新渲染。更重要的是若是咱们在mapStateToProps
方法内使用toJS
将极大破坏组件性能,若是真的须要,咱们应该使用前面介绍的高阶组件方式转换。
难以调试
当咱们审查一个Immutable对象时,浏览器会打印出Immutable.js的整个嵌套结构,而咱们实际须要的只是其中小一部分,这致使咱们调试较困难,可使用Immutable.js Object Formatter浏览器插件解决。