前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

梳理Immutable常用API 从fromJS到merge的完整清单

首页2018-02-04 16:10:24Front-End
ImmutableReact不可变数据

在 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:

fe
  • 一、先说清楚这些 API 的共同点
  • 二、fromJS
  • 三、toJS
  • 四、is
  • 五、List 和 Map
    • 5.1 创建
    • 5.2 判断
    • 5.3 长度
      • 5.3.1 size 获取 List/Map 的长度
      • 5.3.2 count()
    • 5.4 数据读取
      • 5.4.1 get() 和 getIn()
      • 5.4.2 has() 和 hasIn()
      • 5.4.3 includes()
      • 5.4.4 first() 和 last()
    • 5.5 数据修改
      • 5.5.1 set()
      • 5.5.2 setIn()
      • 5.5.3 deleteIn()
      • 5.5.4 更新 update()
      • 5.5.5 clear()
    • 5.6 List中的删除与插入
      • 5.6.1 数组方法
    • 5.7 关于merge
    • 5.8 序列算法
      • 5.8.1 concat()
      • 5.8.2 map()
      • 5.8.3 mapKeys()
      • 5.8.4 mapEntries()
      • 5.8.5 filter
      • 5.8.6 reverse
      • 5.8.7 sort & sortBy
      • 5.8.8 groupBy
    • 5.9 查找数据
      • 5.9.1 indexOf 和 lastIndexOf
      • 5.9.2 findIndex() 和 findLastIndex()
      • 5.9.3 find() 和 findLast()
      • 5.9.4 findKey() 和 findLastKey()
      • 5.9.5 findEntry() 和 findLastEntry()
      • 5.9.6 keyOf() 和 lastKeyOf()
      • 5.9.7 max() 和 maxBy()
      • 5.9.8 min() 和 minBy()
    • 5.10 创建子集
      • 5.10.1 slice()
      • 5.10.2 rest()
      • 5.10.3 butLast()
      • 5.10.4 skip()
      • 5.10.5 skipLast()
      • 5.10.6 skipWhile()
      • 5.10.7 take()
      • 5.10.8 takeLast()
      • 5.10.9 takeWhile()
    • 5.11 处理数据
      • 5.11.1 reduce()
      • 5.11.2 reduceRight()
      • 5.11.3 every()
      • 5.11.4 some()
      • 5.11.5 join()
      • 5.11.6 isEmpty()
      • 5.11.7 countBy()
  • 六、这篇写于 2018 年,现在还该用 Immutable.js 吗
  • 总结
  • 参考

← pm2用法之ecosystem部署nodejs项目VS Code 配置越堆越乱?一套能跟团队共享的整理方法 →