前言
redux / flux 要求采用返回新对象的形式,来触发数据更新、re-render,一般推荐的做法就是采用对象结构的方式:
return {
...state,
enabled: true,
}
如果要更改 state.settings.profile.darkmode
,大概就会变成这样:
return {
...state,
settings: {
...state.settings,
profile:{
...state.settings.profile,
darkmode: true,
}
}
}
以上存在两个问题:
- 如果
state
对象巨大(注意:对象巨大),在结构、拷贝 state 的过程中,耗时会较长 - 如上更改
state.settings.profile.darkmode
,要进行 “庞大”的工作
如何解决这两个在使用 redux 过程中可能存在的问题,便是此文讨论的点。
相应的此文,此文包含内容:
-
不可变(Immutable)数据
-
不可变更新(Immutable Update)实用程序
-
Immutable/Redux 互操作
先说结论,80% - 90% 的场景,直接使用 immer 即可。
不可变(Immutable)数据
facebook/immutable-js
参考阅读:
简言之:
- immutable-js 构建了一些新的数据结构,以空间换时间的方式,来解决 上述的 第一个 大结构对象 拷贝慢问题
- 并通过
stateMap.setIn(['settings', 'profile', 'darkmode'], true)
的方式,解决第二个问题
但相应的
- 如果对象不大,其实用不着 immutable-js
- 要留意 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 的方法,例如:pop
、push
等,在 dev 环境直接报错
planttheidea/crio
仓库地址:planttheidea/crio
具有 API 的不可变 JS 对象,与 seamless-immutable 其实大同小异,继承了原生的 Array、Object,但是也 覆盖 / 封装 了 push
、pop
等方法,提供了,使得其最终也会返回一个新的 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"]
参考阅读:
- Why not just use Immutable.js or mori?
- How does this differ from React.addons.update or seamless-immutable.
综合这 5 个库,facebook/immutable-js 解决了最上方提到的两个问题,但是相对来说比较重。
而 这三个,因为都是使用原生 JS 数据结构,相对的,其实解决的是上方的第二个问题,意义并不是很大。
- rtfeldman/seamless-immutable
- planttheidea/crio
- aearly/icepick
而且后面两个仓库 star 数量较少,当然具体代码未细看,需要说具体问题、具体场景去具体分析,才好做相应技术选型。
但是,如果第一个问题不突出,只是去解决 这第二个 “工作量大” 的问题,真的需要引入上述 immutable 相应的数据结构吗?去记相应的新的对象、数组的新方法吗?
不可变更新(Immutable Update)实用程序
此部分,就是单纯处理第二个问题,没有新的数据结构、数据对象。包含的四个工具库:
- debitoor/dot-prop-immutable
- kolodny/immutability-helper
- mariocasciaro/object-path-immutable
- mweststrate/immer
直接推荐 压轴的 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 的作者,毕竟是获了奖的。而且其写法特别符合人的直觉,还省了 return
(produce
函数内部帮我们做掉了)
参考阅读:精读《Immer.js》源码
Immutable/Redux 互操作
gajus/redux-immutable
将 immutable-js 与 redux 结合的工具
eadmundo/redux-seamless-immutable
仓库地址:eadmundo/redux-seamless-immutable
将 seamless-immutable 与 redux 结合的工具
既然,如无必要不会使用 immutable-js 去处理第一个问题,那么,第二个问题 使用 immer 可以做的很好,也就没必要使用这两个工具了。