immutable-js、immer 以及其他 N 个 immutable data 库相关

2,096 阅读5分钟

前言

redux / flux 要求采用返回新对象的形式,来触发数据更新、re-render,一般推荐的做法就是采用对象结构的方式:

return {
  ...state,
  enabled: true,
}

如果要更改 state.settings.profile.darkmode,大概就会变成这样:

return {
  ...state,
  settings: {
    ...state.settings,
    profile:{
      ...state.settings.profile,
      darkmode: true,
    }
  }
}

以上存在两个问题:

  1. 如果 state 对象巨大(注意:对象巨大),在结构、拷贝 state 的过程中,耗时会较长
  2. 如上更改 state.settings.profile.darkmode,要进行 “庞大”的工作

如何解决这两个在使用 redux 过程中可能存在的问题,便是此文讨论的点。

相应的此文,此文包含内容:

先说结论,80% - 90% 的场景,直接使用 immer 即可。

不可变(Immutable)数据

facebook/immutable-js

仓库地址:facebook/immutable-js

参考阅读:

简言之:

  1. immutable-js 构建了一些新的数据结构,以空间换时间的方式,来解决 上述的 第一个 大结构对象 拷贝慢问题
  2. 并通过 stateMap.setIn(['settings', 'profile', 'darkmode'], true) 的方式,解决第二个问题

但相应的

  1. 如果对象不大,其实用不着 immutable-js
  2. 要留意 immutable 数据 和 原生的 数据之间的差异和操作

swannodette/mori

仓库地址:swannodette/mori

应该是历史遗留产物了,不赘述。

rtfeldman/seamless-immutable

仓库地址:rtfeldman/seamless-immutable

var array = Immutable([1,2,3]);
array.map(value => [value+2, value+4]);
// returns Immutable([ [ 3, 5 ], [ 4, 6 ], [ 5, 7 ] ])

Immutable.flatMap(array, value => [value+2, value+4]);
// returns Immutable([ 3, 5, 4, 6, 5, 7 ])

参考阅读:

冻结的不可变数组/对象,向后兼容 JS

相较 immutable-js,其没有构建新的数据结构,而是在原有 JS array、object 上做了扩展,冻结了一些原生的 array、object 的方法,例如:poppush 等,在 dev 环境直接报错

planttheidea/crio

仓库地址:planttheidea/crio

具有 API 的不可变 JS 对象,与 seamless-immutable 其实大同小异,继承了原生的 Array、Object,但是也 覆盖 / 封装 了 pushpop 等方法,提供了,使得其最终也会返回一个新的 crio immutable array

但是这其实和原生的 [].push[1, 2].pop 就会有差异,需要注意,原生这两个方法返回的是 数组的长度,这引入的差异,感觉更难以控制,反而弊大于利。

// you can assign with crio() directly
const crioArray = crio(['foo']);
const updatedCrioArray = crioArray.push('bar');

const crioObject = crio({foo: 'bar'});
const updatedCrioObject = crioObject.set('bar', 'baz');

// or use the convenience methods
const otherCrioArray = crio.array(['bar']);
const updatedOtherCrioArray = otherCrioArray.push('bar');

const otherCrioObject = crio.object({bar: 'baz'});
const updatedOtherCrioObject = otherCrioObject.set('bar', 'baz');

参考阅读:

aearly/icepick

仓库地址:aearly/icepick

又是一个轮子。和 seamless-immutable 几乎类似。区别可能就在于 这个 是类似 lodash 一样的,工具函数。而 seamless-immutable 则是面向对象的方式。

需要注意的是,其内部与 seamless-immutable 类似,是通过 object shallow copy、slice array 来创建新对象,而 planttheidea/crio 则是通过继承、重新 new 的方式来做。

var coll = {a: 1, b: 2};
var newColl = icepick.assoc(coll, "b", 3); // {a: 1, b: 3}

var arr = ["a", "b", "c"];
var newArr = icepick.assoc(arr, 2, "d"); // ["a", "b", "d"]

参考阅读:

综合这 5 个库,facebook/immutable-js 解决了最上方提到的两个问题,但是相对来说比较重。

而 这三个,因为都是使用原生 JS 数据结构,相对的,其实解决的是上方的第二个问题,意义并不是很大。

  • rtfeldman/seamless-immutable
  • planttheidea/crio
  • aearly/icepick

而且后面两个仓库 star 数量较少,当然具体代码未细看,需要说具体问题、具体场景去具体分析,才好做相应技术选型。

但是,如果第一个问题不突出,只是去解决 这第二个 “工作量大” 的问题,真的需要引入上述 immutable 相应的数据结构吗?去记相应的新的对象、数组的新方法吗?

不可变更新(Immutable Update)实用程序

此部分,就是单纯处理第二个问题,没有新的数据结构、数据对象。包含的四个工具库:

直接推荐 压轴的 mweststrate/immer

dot-prop-immutable

仓库地址:debitoor/dot-prop-immutable

只是一些 helper 方法。

var dotProp = require('dot-prop-immutable');
var state = { todos: [] }, index = 0;

// Add todo:
state = dotProp.set(state, 'todos', list => [...list, {text: 'cleanup', complete: false}])
// or with destructuring assignment
state = {...state, todos: [...state.todos, {text: 'cleanup', complete: false}]};
//=>  { todos: [{text: 'cleanup', complete: false}] }

// Complete todo:
state = dotProp.set(state, `todos.${index}.complete`, true)
// or with destructuring assignment
state = {...state, todos: [
	...state.todos.slice(0, index),
	{...state.todos[index], complete: true},
	...state.todos.slice(index + 1)
]};
//=>  { todos: [{text: 'cleanup', complete: true}] }

// Delete todo:
state = dotProp.delete(state, `todos.${index}`)
// or with destructuring assignment
state = {...state, todos: [
	...state.todos.slice(0, index),
	...state.todos.slice(index + 1)
]};
//=>  { todos: [] }

kolodny/immutability-helper

仓库地址:kolodny/immutability-helper

只是一些 helper 写法 (以 $method 方式)

import update from 'immutability-helper';

const newData = update(myData, {
  x: {y: {z: {$set: 7}}},
  a: {b: {$push: [9]}}
});

const initialArray = [1, 2, 3];
const newArray = update(initialArray, {$push: [4]}); // => [1, 2, 3, 4]

const collection = [1, 2, {a: [12, 17, 15]}];
const newCollection = update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}});
// => [1, 2, {a: [12, 13, 14, 15]}]


const obj = {a: 5, b: 3};
const newObj = update(obj, {b: {$apply: function(x) {return x * 2;}}});
// => {a: 5, b: 6}
// This is equivalent, but gets verbose for deeply nested collections:
const newObj2 = update(obj, {b: {$set: obj.b * 2}});

mariocasciaro/object-path-immutable

仓库地址:mariocasciaro/object-path-immutable

大同小异,helper 方法,返回新的数据对象

const newObj1 = immutable.set(obj, 'a.b', 'f')
const newObj2 = immutable.set(obj, ['a', 'b'], 'f')

// {
//   a: {
//     b: 'f',
//     c: ['d', 'f']
//   }
// }

// Note that if the path is specified as a string, numbers are automatically interpreted as array indexes.

const newObj = immutable.set(obj, 'a.c.1', 'fooo')
// {
//   a: {
//     b: 'f',
//     c: ['d', 'fooo']
//   }
// }

mweststrate/immer

仓库地址:mweststrate/immer

import produce from "immer"

const baseState = [
    {
        todo: "Learn typescript",
        done: true
    },
    {
        todo: "Try immer",
        done: false
    }
]

const nextState = produce(baseState, draftState => {
    draftState.push({todo: "Tweet about it"})
    draftState[1].done = true
})

和上面的思路不一样,原始对象先做了一层 Proxy 代理,得到 draftState 传递给 function。

function(带副作用) 直接更改 draftState,最后 produce 返回新的对象。

推荐直接使用 immer,毕竟人是 mobx 的作者,毕竟是获了奖的。而且其写法特别符合人的直觉,还省了 returnproduce 函数内部帮我们做掉了)

参考阅读:精读《Immer.js》源码

Immutable/Redux 互操作

gajus/redux-immutable

仓库地址:gajus/redux-immutable

将 immutable-js 与 redux 结合的工具

eadmundo/redux-seamless-immutable

仓库地址:eadmundo/redux-seamless-immutable

将 seamless-immutable 与 redux 结合的工具

既然,如无必要不会使用 immutable-js 去处理第一个问题,那么,第二个问题 使用 immer 可以做的很好,也就没必要使用这两个工具了。

其他参考

redux - 生态系统 - 不可变(Immutable)数据