在 React 项目里用 Immutable.js,最容易卡住的不是概念,是 API 太多记不住。官方文档一屏几十个方法,get / getIn、set / setIn、merge / mergeDeep / mergeIn / mergeDeepIn 排在一起,看完只记得住一个 fromJS。这篇把日常真正会用到的方法按「数据怎么进出、怎么读、怎么改、怎么找、怎么切、怎么算」六类归拢了一遍,每个方法都配一个能直接跑的例子,容易踩的边界也标出来。当速查表用就行,不用背。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
fromJS和toJS这两个出入口,以及自定义转换的 reviver 怎么写- 为什么比较两个 Immutable 对象要用
is而不是=== List和Map的创建、类型判断、取长度- 读数据的
get/getIn/has/includes/first/last - 改数据的
set/setIn/update/deleteIn/clear,以及List的增删方法 merge家族六个方法,浅合并和深合并到底差在哪- 序列算法:
map/filter/sort/groupBy这些和原生数组的差别 - 查找、切子集、聚合计算这三类方法的完整清单
- 2018 年之后这个库的现状,以及现在更常见的替代方案
# 一、先说清楚这些 API 的共同点
在翻方法清单之前,有一条规则必须先立住:Immutable 的所有「修改」方法都不改原对象,它们返回一个新对象。
const map1 = Immutable.Map({ a: 1 });
const map2 = map1.set('a', 2);
console.log(map1.get('a')); // 1,原对象没动
console.log(map2.get('a')); // 2
这条规则贯穿下面每一个方法。push、pop、sort、reverse 这些在原生数组里会改原数组的方法,在 Immutable 里全都是返回新对象。所以写的时候千万别丢掉返回值,list.push(5) 这一行单独写等于什么都没干。这个坑我踩过,排查了半天发现是漏了赋值。
另一件事是性能。你可能担心每次都造新对象会不会很贵,其实不会,Immutable 内部用的是结构共享,没变的那部分子树是直接复用引用的。这块的原理我在 Immutable之回顾 那篇里展开讲了,这篇专注在 API 上。
# 二、fromJS
作用:是最最常用的将原生 JS 数据转换为 ImmutableJS 数据的转换方法,默认将原生 JS 的 Array 转为 List,Object 转为 Map。
它是深转换,嵌套多少层就转多少层:
Immutable.fromJS({
a: {
b: [1, 2, 3],
c: 40
}
});
// 得到
Map {
"a": Map {
"b": List [1, 2, 3],
"c": 40
}
}
// 常见
const t1 = Immutable.fromJS({a: {b: [10, 20, 30]}, c: 40});
console.log(t1);
第二个参数是一个自定义转换函数(reviver),用得不多,但知道有这么个口子有好处。它会自底向上被调用一遍,让你决定每一层转成什么结构:
// 不常用
const t2 = Immutable.fromJS({a: {b: [10, 20, 30]}, c: 40}, function(key, value) {
// 定制转换方式,下这种就是将Array转换为List,Object转换为OrderedMap
const isIndexed = Immutable.Iterable.isIndexed(value);
return isIndexed ? value.toList() : value.toOrderedMap();
// true, "b", {b: [10, 20, 30]}
// false, "a", {a: {b: [10, 20, 30]}, c: 40}
// false, "", {"": {a: {b: [10, 20, 30]}, c: 40}}
});
console.log(t2);
注释里那三行是调用顺序,从最内层的 b 开始,一层层往外,最后一次的 key 是空字符串,代表根节点。这里默认把对象转成了 OrderedMap 而不是 Map,区别是前者保留插入顺序,遍历时顺序是确定的。
这里有个版本上的坑要注意,Immutable.Iterable 是 3.x 的写法。后来的版本把 Iterable 改名成了 Collection,判断函数也提到了顶层。跑不通的话看一眼你装的是哪个大版本,以官方文档为准,我这里保留 3.x 的原始写法。
fromJS 的代价是遍历整棵结构,数据量大的时候别在渲染里反复调用。
# 三、toJS
作用:将一个 Immutable 数据转换为 JS 类型的数据。用法:value.toJS()。
它和 fromJS 一样是深转换,整棵结构都会被还原成原生对象和数组。
正因为是深转换,toJS() 是个开销不小的操作,而且每次调用都返回全新的对象。所以在 React 里千万别在 render 里直接写 this.props.data.toJS(),每次渲染都会得到一个新引用,PureComponent 的浅比较必然判定为「变了」,你用 Immutable 换来的那点优化全被抵消掉。要取值就一路用 get / getIn,只在真正要交给第三方库的时候才转。
顺带提一句,如果只想转一层,用 toObject() 或 toArray(),它们不会递归。
# 四、is
作用:对两个对象进行比较。用法:is(map1, map2)。
import { Map, is } from 'immutable'
const map1 = Map({ a: 1, b: 1, c: 1 })
const map2 = Map({ a: 1, b: 1, c: 1 })
map1 === map2 //false
Object.is(map1, map2) // false
is(map1, map2) // true 只检测值是否相等
为什么 === 是 false?因为两次 Map() 造出来的是两个不同的对象,引用当然不同。is 做的是值比较,它先试引用相等,不等就调用两边的 equals 方法,Immutable 结构的 equals 会递归比较内容。
那既然有 is,为什么大家还是说用了 Immutable 就能用 === 做优化?因为在正常的数据流里,你只会通过 set / merge 这些方法产生新对象,没改动的部分引用会被原样复用。同一份数据没动过,拿到的就是同一个引用,=== 直接为 true。is 是给「两个来源不同但内容可能相同」的对象准备的兜底。
这套判断和普通 JS 对象的深拷贝、深比较是两条路子,如果你还在手写深拷贝来做状态隔离,可以对比着看 JS深拷贝的几种实现。
# 五、List 和 Map
# 5.1 创建
List有序索引密集的集合,和JS中的Array很像Map无序索引集,类似JavaScript中的Object
「无序」这个词要理解准确。Map 不保证遍历顺序和你插入的顺序一致,如果业务上依赖顺序,用 OrderedMap。同理 List 之外还有 Set 和 OrderedSet,需要去重时用得上。
# 5.2 判断
List.isList() 和 Map.isMap() 判断一个数据结构是不是 List/Map 类型。
这两个方法在写通用工具函数时很有用,比如你要写一个「递归遍历任意 Immutable 结构」的函数,就得靠它们分流。
# 5.3 长度
# 5.3.1 size 获取 List/Map 的长度
size 是属性不是方法,别写成 size():
// list
console.log(List([1,2,3,4]).size);// 4
console.log(List.of(1, 2, 3, 4).size);// 4
// map
console.log(Map({key: "value2", key1: "value1"}).size);// 2
console.log(Map.of({x:1}, 2, [3], 4).size);// 2
List([1,2,3,4]) 和 List.of(1,2,3,4) 的区别和原生 Array 的那对一样,前者接一个可迭代对象,后者接零散参数。Map.of 接的是键值交替的参数列表,所以 Map.of({x:1}, 2, [3], 4) 是两组键值对,size 为 2。
# 5.3.2 count()
count() 是方法,不传参数时和 size 等价。它的价值在于可以传一个判断函数,只统计满足条件的元素:
// map
console.log(Immutable.fromJS({key: "value2", key1: "value1"}).count());// 2
// 可以定制条件,来确定大小
console.log(Immutable.fromJS({key: 1, key1: 34}).count((value, key, obj) => {
return value > 3;
}));// 1 只有 34 大于 3
// list
console.log(Immutable.fromJS([1, 2, 5, 6]).count());// 4
// 可以指定条件,来确定大小
console.log(Immutable.fromJS([1, 2, 5, 6]).count((value, index, array) => {
return value > 3;
}));// 2 大于3的有两个
先说结论,能用 size 就用 size,它是缓存好的,读起来是常数时间;count() 带条件时要遍历一遍。
# 5.4 数据读取
# 5.4.1 get() 和 getIn()
获取数据结构中的数据。get 取一层,getIn 接一个路径数组取深层,路径中间断了会返回 undefined 而不是报错,这点比原生对象取值省心得多。
getIn(['a', 'b', 'c'], defaultValue) 还能传第二个参数当兜底值,取不到就返回它。
# 5.4.2 has() 和 hasIn()
判断是否存在某一个 key:
Immutable.fromJS([1,2,3,{a:4,b:5}]).has('0'); //true
Immutable.fromJS([1,2,3,{a:4,b:5}]).has(0); //true 字符串下标会被转成数字索引
Immutable.fromJS([1,2,3,{a:4,b:5}]).hasIn([3,'b']) //true
List 上的 has 判断的是下标存不存在,不是值。这里 '0' 和 0 都返回 true,因为内部会把能转成合法索引的字符串转成数字。
# 5.4.3 includes()
判断是否存在某一个 value: