Immutable.js与React,Redux及reselect的实践

欢迎访问个人博客Immutable.js与React,Redux及reselect的实践javascript

本篇文章将聚焦Immutable与Redux,reselect的项目实践,将从多方面阐述Immutable及Redux:包括什么是Immutable,为何须要使用Immutable,Immutable.js与React,Redux及reselect的组合实践及优化,最后总结使用Immutable可能遇到的一些问题及解决方式。html

Immutable

Immutable来自于函数式编程的世界,咱们能够称它为不可变,试想以下代码:java

var object = { x:1, y: 2 };
var object2 = { x: 1, y: 2 };
object == object2// false
object === object2 // false复制代码

相等性检查将包括两个部分:react

  1. 值检查
  2. 引用检查

引用检查

JavaScript的对象是一个很是复杂的数据结构,它的键能够指向任意值,包括object。JavaScript建立的对象将存储在计算机内存中(对应一个物理地址),而后它返回一个引用,JavaScript引擎经过该引用能够访问该对象,该引用赋值给某个变量后,咱们即可以经过该变量以引用的方式操做该对象。引用检查即检查两个对象的引用地址是否相同。git

值检查

层层循环检查对象各属性值是否相同。github

React从新渲染

React经过对组件属性(props)和状态(state)进行变动检查以决定是否更新并从新渲染该组件,若组件状态太过庞大,组件性能就会降低,由于对象越复杂,其相等性检查就会越慢。编程

  1. 对于嵌套对象,必须迭代层层进行检查判断,耗费时间过长;
  2. 若仅修改对象的属性,其引用保持不变,相等性检查中的引用检查结果不变;

Immutable提供一直简单快捷的方式以判断对象是否变动,对于React组件更新和从新渲染性能能够有较大帮助。redux

Immutable数据

Never mutated, instead copy it and then make change.api

绝对不要忽然修改对象,首先复制而后修改复制对象,再返回这个新对象,保持原对象不变。数组

Immutable对象和原生JavaScript对象的主要差别能够归纳为如下两点:

  1. 持久化数据结构(Persistent data structures)
  2. 结构共享(Structures sharing Trie

持久化数据结构

持久数据结构主张全部操做都返回该数据结构的更新副本,并保持原有结构不变,而不是改变原来的结构。一般利用Trie构建它不可变的持久性数据结构,它的总体结构能够看做一棵树,一个树节点能够对应表明对象某一个属性,节点值即属性值。

结构共享

一旦建立一个Immutable Trie型对象,咱们能够把该Trie型对象想象成以下一棵树,在以后的对象变动尽量的重用树节点:

Structures sharing
Structures sharing

当咱们要更新一个Immutable对象的属性值时,就是对应着须要重构该Trie树中的某一个节点,对于Trie树,咱们修改某一节点只须要重构该节点及受其影响的节点,即其祖先节点,如上图中的四个绿色节点,而其余节点能够彻底重用。

参考

  1. Immutable Persistent Data Structures
  2. Trie

为何须要Immutable

上一节简单介绍了什么是Immutable,本节介绍为何须要使用Immutable。

不可变,反作用及突变

咱们不鼓励忽然变动对象,由于那一般会打断时间旅行及bug相关调试,而且在react-redux的connect方法中状态突变将致使组件性能低下:

  1. 时间旅行:Redux DevTools开发工具指望应用在从新发起某个历史action时将仅仅返回一个状态值,而不改变任何东西,即无反作用。突变和异步操做将致使时间旅行混乱,行为不可预测。
  2. react-redux:connect方法将检查mapStateToProps方法返回的props对象是否变动以决定是否须要更新组件。为了提升这个检查变动的性能,connect方法基于Immutabe状态对象进行改进,使用浅引用相等性检查来探测变动。这意味着对对象或数组的直接变动将没法被探测,致使组件没法更新。

在reducer函数中的诸如生成惟一ID或时间戳的其余反作用也会致使应用状态不可预测,难以调试和测试。

若Redux的某一reducer函数返回一个能够突变的状态对象,意味着咱们不能追踪,预测状态,这可能致使组件发生多余的更新,从新渲染或者在须要更新时没有响应,也会致使难以跟踪调试bug。Immutable.js能提供一种Immutable方案解决如上提到的问题,同时其丰富的API也足够支撑咱们复杂的开发。

参考

  1. Why and When to use Immutable
  2. Why do we need Immutable class

如何使用Immutable

Immutable能给咱们的应用提供较大的性能提高,可是咱们必须正确的使用它,不然得不偿失。目前关于Immutable已经有一些类库,对于React应用,首选的是Immutable.js。

Immutable.js和React

首先须要明白的是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>
        )
    }
}复制代码
  1. 使用Immutable.js的访问API访问state,如get(),getIn();

  2. 使用Immutable.js的集合操做生成组件子元素:

    使用高阶函数如map()reduce()等建立React元素的子元素:

    {data.get('todos').map(item =>
        <li>Saved:
        {item}</li>
    )}复制代码
  3. 使用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)
    }));复制代码

参考:

  1. Immutable as React state

Immutable.js和Redux

React自己是专一于视图层的一个JavaScript类库,因此其单独使用时状态通常不会过于复杂,因此其和Immutable.js的协做比较简单,更重要也是咱们须要更多关注的地方是其与React应用状态管理容器的协做,下文就Immutable.js如何高效的与Redux协做进行阐述。

咱们在Redux中讲状态(state)主要是指应用状态,而不是组件状态。

redux-immutable

原始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

若是在项目中使用了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.js和Redux协做开发时,能够从以下几方面思考咱们的实践。

JavaScript对象转换为Immutable对象
  1. 不要在Immutable对象中混用原生JavaScript对象;

  2. 当在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 state tree
  1. 使用Immutable对象表示完整的Redux状态树;

    对于一个Redux应用,完整的状态树应该由一个Immutable对象表示,而没有原生JavaScript对象。

  2. 使用fromJS()方法建立状态树

    状态树对象能够是一个Immutable.Record或者任何其余的实现了get,set,withMutations方法的Immutable集合的实例。

  3. 使用redux-immutable库调整combineReducers方法使其能处理Immutable。

Immutable与Redux组件

当使用Redux做React应用状态管理容器时,咱们一般将组件分为容器组件和展现型组件,Immutable与Redux组件的实践也主要围绕这二者。

  1. 除了在展现型组件内,其余地方一概使用Immutable方式操做状态对象;

    为了保证应用性能,在容器组件,选择器(selectors),reducer函数,action建立函数,sagas和thunks函数内等全部地方均使用Immutable,可是不在展现型组件内使用。

  2. 在容器组件内使用Immutable

    容器组件可使用react-redux提供的connect方法访问redux的store,因此咱们须要保证选择器(selectors)老是返回Immutable对象,不然,将会致使没必要要的从新渲染。另外,咱们可使用诸如reselect的第三方库缓存选择器(selectors)以提升部分情景下的性能。

Immutable对象转换为JavaScript对象

toJS()方法功能就是把一个Immutable对象转换为一个JavaScript对象,而咱们一般尽量将Immutable对象转换为JavaScript对象这一操做放在容器组件中,这也与容器组件的宗旨吻合。另外toJS方法性能极低,应该尽可能限制该方法的使用,如在mapStateToProps方法和展现型组件内。

  1. 绝对不要在mapStateToProps方法内使用toJS()方法

    toJS()方法每次会调用时都是返回一个原生JavaScript对象,若是在mapStateToProps方法内使用toJS()方法,则每次状态树(Immutable对象)变动时,不管该toJS()方法返回的JavaScript对象是否实际发生改变,组件都会认为该对象发生变动,从而致使没必要要的从新渲染。

  2. 绝对不要在展现型组件内使用toJS()方法

    若是传递给某组件一个Immuatble对象类型的prop,则该组件的渲染取决于该Immutable对象,这将给组件的重用,测试和重构带来更多困难。

  3. 当容器组件将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渗透入展现型组件带来的可维护性,可重用性及可测试性是咱们更应该看重的。

参考
  1. Immutable.js Best practices

Immutable.js与reselect

reselect

使用Redux管理React应用状态时,mapStateToProps方法做为从Redux Store上获取数据过程当中的重要一环,它必定不能有性能缺陷,它自己是一个函数,经过计算返回一个对象,这个计算过程一般是基于Redux Store状态树进行的,而很明显的Redux状态树越复杂,这个计算过程可能就越耗时,咱们应该要可以尽量减小这个计算过程,好比重复在相同状态下渲染组件,屡次的计算过程显然是多余的,咱们是否能够缓存该结果呢?这个问题的解决者就是reselect,它能够提升应用获取数据的性能。

reselect的原理是,只要相关状态不变,即直接使用上一次的缓存结果。

选择器

reselect经过建立选择器(selectors),该函数接受一个state参数,而后返回咱们须要在mapStateToProps方法内返回对象的某一个数据项,一个选择器的处理能够分为两个步骤:

  1. 接受state参数,根据咱们提供的映射函数数组分别进行计算,若是返回结果和上次第一步的计算结果一致,说明命中缓存,则不进行第二步计算,直接返回上次第二步的计算结果,不然继续第二步计算。第一步的结果比较,一般仅仅是===相等性检查,性能是足够的。

  2. 根据第一步返回的结果,计算,返回最终结果。

    以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方法,接受两个参数:

    1. 第一个参数是一个映射函数数组,选择器处理流程的第一步所处理的数据即为该数组内各函数的返回值,这些返回值也依次做为参数传入第二步处理函数;
    2. 第二个参数则是,第二步的具体计算函数,也即缓存结果处理函数,其返回结果也即mapStateToProps方法所需的数据;

    而后在mapStateToProps内使用该选择器函数,接受state参数:

    const mapStateToProps = (state) => {
      return {
        todos: selectFilterTodos(state)
      }
    }复制代码

    上文中的映射函数,内容如:

    const getTodos = (state) => {state.todos}
    const getFilter = (state) => {state.filter}复制代码
Immutable概念数据

另外须要注意的是,传入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
defaultMemoize(func, equalityCheck = defaultEqualityCheck)复制代码

defaultMemoize将缓存传递的第一个函数参数func的返回结果,该函数是使用createSelector建立选择器时传入的缓存结果处理函数,其默认缓存度为1。

equalityCheck是建立的选择器使用的缓存命中检测函数,默认函数代码如:

function defaultEqualityCheck(currentVal, previousVal) {
  return currentVal === previousVal
}复制代码

只是简单的进行引用检查。

createSelectorCreator

createSelectorCreator方法支持咱们建立一个自定义的createSelector函数,而且支持咱们传入自定义的缓存计算函数,覆盖默认的defaultMemoize函数,定义格式以下:

createSelectorCreator(memoize, ...memoizeOptions)复制代码
  1. memoize参数是一个缓存函数,用以替代defaultMemoize,该函数接受的第一个参数就是建立选择器时传入的缓存结果处理函数;
  2. …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)复制代码

结合Immutable.js

如上文为例,reselect是内在须要使用Immutable概念数据的,当咱们把整个Redux状态树Immutable化之后,须要进行一些修改。

修改映射函数:

const getTodos = (state) => {state.get('todos')}
const getFilter = (state) => {state.get('filter')}复制代码

特别须要注意的是在选择器第二步处理函数内,若是涉及Immutable操做,也须要额外修改为Immutable对应方式。

Immutable实践中的问题

不管什么状况,都不存在绝对完美的事物或者技术,使用Immutable.js也必然会带来一些问题,咱们能作的则是尽可能避免或者尽最大可能的分化这些问题,而能够更多的去发扬该技术带来的优点,使用Immutable.js最多见的问题以下。

  1. 很难进行内部协做

    Immutable对象和JavaScript对象之间存在的巨大差别,使得二者之间的协做一般较麻烦,而这也正是许多问题的源头。

    1. 使用Immutable.js后咱们再也不能使用点号和中括号的方式访问对象属性,而只能使用其提供的get,getIn等API方式;
    2. 再也不能使用ES6提供的解构和展开操做符;
    3. 和第三方库协做困难,如lodash和JQuery等。
  2. 渗透整个代码库

    Immutable代码将渗透入整个项目,这种对于外部类库的强依赖会给项目的后期带来很大约束,以后若是想移除或者替换Immutable是很困难的。

  3. 不适合常常变动的简单状态对象

    Immutable和复杂的数据使用时有很大的性能提高,可是对于简单的常常变动的数据,它的表现并很差。

  4. 切断对象引用将致使性能低下

    Immutable最大的优点是它的浅比较能够极大提升性能,当咱们屡次使用toJS方法时,尽管对象实际没有变动,可是它们之间的等值检查不能经过,将致使从新渲染。更重要的是若是咱们在mapStateToProps方法内使用toJS将极大破坏组件性能,若是真的须要,咱们应该使用前面介绍的高阶组件方式转换。

  5. 难以调试

    当咱们审查一个Immutable对象时,浏览器会打印出Immutable.js的整个嵌套结构,而咱们实际须要的只是其中小一部分,这致使咱们调试较困难,可使用Immutable.js Object Formatter浏览器插件解决。

相关文章
相关标签/搜索