使用redux的操作数据的时候对某个数据进行增删改查操作,需要维持数据的immutable。当然类似的库也有很多,像lodash、ramda等等,这些库往往方法众多,如果是初次使用必定眼花缭乱,抛开一切,在使用这些已有的库之前,我们需要思考一下,我们真正需要怎么样的功能,能否自己实现一个简单的帮助函数库。
首先明确我们的需求,我们需要实现的方法必须:
- 不修改原有数据无副作用
- 简单简单简单简单!
一切从增删改查开始
一个列表数据,我们想在reducer中对它进行增删改查操作,应该怎么做呢?
第一步,列表处理
我相信大多数人会首先把这个列表显示出来再进行操作,后端传过来的数据往往是这样一个数组。1
2
3
4
5
6
7
8
9
10[{
id:1000,
text:'test1'
},{
id:1001,
text:'test2'
},{
id:1002,
text:'test3'
}]
当然,我们是可以直接把这个数据存在store中渲染到界面上的,但是如果要对这个数据进行其它操作呢?增还好,push进数组,改和删,首先我们需要遍历查找到id为指定值的数据,然后再进行下一步操作,步骤比较繁琐。所以我们可不可以直接在存储阶段就对它进行处理,便于后面的操作呢?当然可以啦~
所以第一个方法,把数组处理成对象,即我们的结果是:1
2
3
4
5
6
7
8
9
10
11
12
13
14{
1000: {
id: 1000,
text: 'test1'
},
1001: {
id: 1001,
text: 'test2'
},
1002: {
id: 1002,
text: 'test3'
}
}
输入:数组,作为key值的属性名
输出:一个新的对象
实现:1
2
3
4
5
6
7export const mapArrayToObject = (array, value = 'id') => {
let obj = {};
array.map(item => {
obj[item[value]] = item;
});
return obj;
}
第二步,增加和修改
对象一个重要的特性是,如果这个key值之前存在会覆盖原有属性,没有则会增加键值对。从最简单的场景开始,如果一个对象:1
2
3
4{
a: 1,
b: 2
}
我们增加或删除一个属性,返回新的对象
输入:需添加的key,需添加的value,对象
输出:一个新的对象
实现:1
2
3
4
5export const assoc = (prop, val, obj) => {
let result = Object.assign({}, obj);
result[prop] = val;
return result;
}
考虑到如果一次性要更新或增加多个属性,我们可以传入一个对象,把这个新对象的属性加在原对象上
输入:props:Object[要加到对象上的属性],obj:Object[原对象]
输出:一个新的对象
实现:1
2
3
4export const merge = (props, obj) => {
const result = Object.assign({}, obj);
return Object.assign(result,props);
}
当然实际情况大多不可能那么简单,我们很多情况下或许要修改1
2
3
4
5
6
7
8
9{
a: {
b: {
c: {
d: 2
}
}
}
}
中的d,如果不使用帮助函数,哪怕ES6+有对象解构,也八成要写成这个模样1
2
3
4
5
6
7
8
9
10
11
12return {
a: {
...a
b: {
...b
c: {
...c,
d: 3
}
}
}
}
这时候想到了ramda中的assocPath,先放上源码我们分析一下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var assocPath = _curry3(function assocPath(path, val, obj) {
if (path.length === 0) {
return val;
}
var idx = path[0];
if (path.length > 1) {
var nextObj = (!isNil(obj) && _has(idx, obj)) ? obj[idx] : _isInteger(path[1]) ? [] : {};
val = assocPath(Array.prototype.slice.call(path, 1), val, nextObj);
}
if (_isInteger(idx) && _isArray(obj)) {
var arr = [].concat(obj);
arr[idx] = val;
return arr;
} else {
return assoc(idx, val, obj);
}
});
export default assocPath;
看一下里面用到的方法:
- _curry3 柯里化我们暂时不考虑
- isNil 检测输入值是否为 null 或 undefined
- _has 判断对象自身是否含有该属性
- _isInteger 判断是否为数字
- _isArray 判断是否为数组
看一下整个递归调用的过程,假如我们传入的参数是([‘a’,’b’,’c’,’d’],5,obj),1
2
3
4
5
6
7
8
9
10
11
12
13
14var obj = {
a:{
b:{
c:{
d:2,
d2:[32321,1314322,13212]
},
c2:'c'
},
b2:'b'
},
a2:'a'
}
首先,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17path = ['a','b','c','d'];
val = 5;
obj = {
a:{
b:{
c:{
d:2,
d2:[32321,1314322,13212]
},
c2:'c'
},
b2:'b'
},
a2:'a'
};
idX = 'a';
path.length > 11
2
3
4
5
6
7
8
9
10
11nextObj = {
b:{
c:{
d:2,
d2:[32321,1314322,13212]
},
c2:'c'
},
b2:'b'
};
并且进入下一层递归,经过层层递归,最后
1 | path = ['d']; |
此时path.length 不大于1,故不进入递归直接进入下面的逻辑,由于是一个对象,调用assoc(),返回修改完毕的新对象,再然后一层层执行之前递归未执行完的逻辑,最后返回修改完毕的对象,由于每次assoc会创建一个新对象,故不会修改原对象,assoc的for..in遍历是浅复制,也保证了新对象保持对原先对象未修改部分的引用,仅修改要变化的量。
再扩展一下,如果我们需要修改某一路径上多个属性1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 修改对象某一路径上的多个属性
* @param {Array} path 属性路径 ['a','b','c']
* @param {Object} vals 属性列表
* @param {Object} obj
*/
export const assocPathMore = (path, vals = {}, obj) => {
let result = Object.assign({}, obj);
for(let i in vals){
result = assocPath([...path, i], vals[i], result);
}
return result;
}
第三步,删除
删除有,或者说过滤,有两种,第一是元素的某个属性等于某个值时不显示,第二是直接把key为多少的元素从列表中移除.
第一种,换一种说法是返回一个列表中满足一定值的元素的集合1
2
3
4
5
6
7
8
9const pickBy = (cb, obj) => {
var result = {};
for (var prop in obj) {
if (cb(obj[prop], prop, obj)) {
result[prop] = obj[prop];
}
}
return result;
}
对于第二种,可以用delete操作符:1
2
3
4
5const without = (obj, prop) => {
const result = Object.assign({}, obj);
delete result[prop];
return result;
}
当然我们有时也会删除某个查找路径上的属性,或者过滤某个查找路径上的属性,也可按(2)中的思路进行修改,或者更甚一步,可不可以将“查找”这一过程独立出来复用呢?这个暂时有待实现,不过可以参考一些现有的方法集: