Immutable.js 教程:拥抱不可变数据
目录
- 为什么需要 Immutable.js? - 解决的核心问题
- 核心概念 - 深入理解不可变性
- 快速入门 - 安装与第一个示例
- 核心 API 详解 - 常用数据类型的操作
- 性能优势 - 为什么不可变数据更快?
- 与 React 的结合 - 在 React 应用中的最佳实践
- 进阶主题 -
Record,Seq,fromJS等 - 优缺点与替代方案 - Immutable.js vs. 其他方案
为什么需要 Immutable.js?
在 JavaScript 中,对象和数组是可变的,这意味着你可以直接修改它们的内容,而不会创建新的实例。

const user = { name: 'Alice', age: 30 };
// 直接修改原对象
user.age = 31;
console.log(user); // { name: 'Alice', age: 31 } - 原对象被改变了
这种可变性在大型、复杂的应用(尤其是前端框架如 React)中会带来很多问题:
- 难以追踪状态变化:当你不知道一个对象在何时、何地、被谁修改时,调试会变得异常困难,状态变化变得不可预测。
- 性能问题:React 的
shouldComponentUpdate或React.memo依赖于props或state的浅比较,如果父组件的状态更新,即使子组件用到的数据没有变,因为引用地址变了,子组件也会不必要的重新渲染。 - 副作用:意外的修改会导致难以发现的 bug,一个函数意外地修改了传入的对象,而这个对象又被其他地方使用。
Immutable.js 的解决方案: Immutable.js 通过提供一套持久化的数据结构,确保任何修改操作都会返回一个全新的对象,而原始对象保持不变,这使得数据变化变得可预测、可追踪,并能与 React 的渲染机制完美配合。
核心概念
在深入学习 API 之前,必须理解 Immutable.js 的几个核心概念:
a. 持久化数据结构
当你修改一个 Immutable 对象时,它会创建一个新的对象,但它并不是将整个旧数据都复制一遍,而是通过结构共享来高效地复用未改变的部分,这使得操作的性能非常高,尤其是在处理大型数据集时。

b. 不可变
一旦创建,一个 Immutable 对象就不能被改变,任何试图修改它的操作都会返回一个包含新值的新对象。
c. 结构共享
这是 Immutable.js 性能的关键,当你修改一个对象时,只有被修改的路径会创建新的数据节点,其他未受影响的节点会直接被复用,形成新的“分支”指向旧的“树”。
一个简单的比喻: 想象一棵树(你的数据)。
- 可变:你直接爬上树,把一片叶子摘下来换成了新的,整棵树还是原来的那棵。
- 不可变:你不想改变原来的树,你找到了那片叶子,复制了它,换上新叶子后,把这片新叶子连同它所在的树枝、树干都复制了一遍,形成一棵新树,原来的树完好无损,结构共享就像是,只有那片被换掉的叶子是新做的,树枝和树干都是直接从旧树上“借”过来的。
快速入门
安装
npm install immutable # 或者 yarn add immutable
第一个示例:创建和修改
import { Map, List } from 'immutable';
// 1. 创建一个不可变的 Map (类似 JS 的 Object)
const originalMap = Map({ a: 1, b: 2, c: { d: 3 } });
console.log(originalMap.get('a')); // 1
// 2. 修改 Map - 这会返回一个全新的 Map
const newMap = originalMap.set('a', 99);
console.log('Original:', originalMap.toJS()); // { a: 1, b: 2, c: { d: 3 } }
console.log('New: ', newMap.toJS()); // { a: 99, b: 2, c: { d: 3 } }
// 原始 Map 没有被改变!
// 3. 深度嵌套的修改
const newNestedMap = originalMap.setIn(['c', 'd'], 999); // 使用 setIn 修改深层属性
console.log('New Nested:', newNestedMap.toJS()); // { a: 1, b: 2, c: { d: 999 } }
// 4. 创建一个不可变的 List (类似 JS 的 Array)
const originalList = List([1, 2, 3]);
const newList = originalList.push(4);
console.log('Original List:', originalList.toJS()); // [1, 2, 3]
console.log('New List: ', newList.toJS()); // [1, 2, 3, 4]
关键点:

- 使用
Map,List等构造函数来创建不可变对象。 - 使用
.set(),.push(),.delete()等方法进行修改。 - 这些方法不会修改原对象,而是返回新对象。
- 使用
.toJS()将 Immutable 对象转换回普通的 JavaScript 对象,方便调试或传递给需要普通 JS 对象的库。
核心 API 详解
Immutable.js 提供了多种数据结构,覆盖了 JavaScript 的原生类型。
| Immutable.js | JavaScript | 描述 |
|---|---|---|
Map |
Object |
键值对集合,键可以是任意类型。 |
List |
Array |
有序的索引值集合。 |
OrderedMap |
Object |
保留了键插入顺序的 Map。 |
Set |
Set |
唯一值的集合。 |
OrderedSet |
Set |
保留了值插入顺序的 Set。 |
Stack |
Array |
后进先出 的栈。 |
Record |
Class |
一个具有固定集合属性的 Map 的子类。 |
常用操作(以 Map 和 List 为例)
Map 操作
import { Map } from 'immutable';
const map1 = Map({ a: 1, b: 2 });
const map2 = Map({ b: 20, c: 30 });
// 合并
const mergedMap = map1.merge(map2);
console.log(mergedMap.toJS()); // { a: 1, b: 20, c: 30 } (b 被覆盖)
// 删除
const mapWithoutA = map1.delete('a');
console.log(mapWithoutA.toJS()); // { b: 2 }
// 获取
console.log(map1.get('a')); // 1
console.log(map1.has('c')); // false
// 转换
console.log(map1.toJS()); // { a: 1, b: 2 }
console.log(map1.toObject()); // { a: 1, b: 2 }
console.log(map1.toArray()); // [ [ 'a', 1 ], [ 'b', 2 ] ]
List 操作
import { List } from 'immutable';
const list1 = List([1, 2, 3]);
const list2 = List([4, 5, 6]);
// 添加元素
const newList = list1.push(4);
console.log(newList.toJS()); // [1, 2, 3, 4]
// 合并 Lists
const concatenatedList = list1.concat(list2);
console.log(concatenatedList.toJS()); // [1, 2, 3, 4, 5, 6]
// 获取和设置
console.log(list1.get(0)); // 1
const updatedList = list1.set(0, 99);
console.log(updatedList.toJS()); // [99, 2, 3]
// 转换
console.log(list1.toJS()); // [1, 2, 3]
console.log(list1.toArray()); // [1, 2, 3]
深度操作
当处理嵌套结构时,使用 In 后缀的方法非常方便。
const nestedMap = Map({
user: Map({
name: 'Alice',
address: Map({
city: 'New York',
zip: '10001'
})
})
});
// 修改深层嵌套属性
const updatedNestedMap = nestedMap.setIn(['user', 'address', 'zip'], '10003');
console.log(updatedNestedMap.toJS());
// {
// user: {
// name: 'Alice',
// address: {
// city: 'New York',
// zip: '10003' // 已更新
// }
// }
// }
// 获取深层嵌套属性
const zipCode = nestedMap.getIn(['user', 'address', 'zip']);
console.log(zipCode); // '10001'
// 删除深层嵌套属性
const mapWithoutZip = nestedMap.deleteIn(['user', 'address', 'zip']);
console.log(mapWithoutZip.toJS());
// {
// user: {
// name: 'Alice',
// address: {
// city: 'New York' // zip 已删除
// }
// }
// }
性能优势
Immutable.js 的性能优势主要体现在两个方面:
-
减少渲染:在 React 中,组件的重新渲染通常是由于
state或props的引用地址改变,使用 Immutable.js,当状态的一部分发生变化时,只有那一小部分会创建新对象,如果传递给子组件的props恰好是那部分未改变的数据,它的引用地址就不会变,React.memo或shouldComponentUpdate就能正确地阻止不必要的重新渲染。 -
高效的比较:比较两个普通 JS 对象是否相等(深度比较)是一个昂贵的操作,时间复杂度为 O(n),而比较两个 Immutable 对象是否相等,只需要比较它们的根引用即可,时间复杂度为 O(1),Immutable.js 提供了
is()函数来做这件事。
import { is, Map } from 'immutable';
const map1 = Map({ a: 1, b: 2 });
const map2 = Map({ a: 1, b: 2 });
const map3 = map1.set('a', 99);
console.log(is(map1, map2)); // true,内容相同,但它们是不同的实例
console.log(is(map1, map3)); // false,内容不同
// 在 React 中,可以这样用
shouldComponentUpdate(nextProps, nextState) {
return !is(this.props, nextProps) || !is(this.state, nextState);
}
与 React 的结合
这是 Immutable.js 最经典的应用场景。
最佳实践:将 state 设为 Immutable 对象
import React, { Component } from 'react';
import { Map, fromJS } from 'immutable';
class MyComponent extends Component {
constructor(props) {
super(props);
// 使用 fromJS 可以方便地将普通 JS 对象转换为 Immutable 对象
this.state = fromJS({
user: {
name: 'Alice',
todos: [
{ id: 1, text: 'Learn Immutable.js', completed: true },
{ id: 2, text: 'Build an app', completed: false }
]
}
});
}
toggleTodo = (todoId) => {
// 使用 updateIn 和一个函数来更新状态
// 函数接收旧值,返回新值
this.setState(state =>
state.updateIn(['user', 'todos'], todos =>
todos.map(todo =>
todo.get('id') === todoId
? todo.set('completed', !todo.get('completed'))
: todo
)
)
);
};
render() {
// 使用 .toJS() 将 Immutable 对象转回普通 JS 对象,供组件使用
const { name, todos } = this.state.getIn(['user']).toJS();
return (
<div>
<h1>{name}'s Todos</h1>
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => this.toggleTodo(todo.id)}>
{todo.text} - {todo.completed ? 'Done' : 'Pending'}
</li>
))}
</ul>
</div>
);
}
}
使用 React.memo 优化子组件
// 子组件
const TodoItem = React.memo(({ todo, onToggle }) => {
console.log('TodoItem rendered for:', todo.id); // 只有当 todo 对象引用改变时才会打印
return (
<li onClick={() => onToggle(todo.id)}>
{todo.text} - {todo.completed ? 'Done' : 'Pending'}
</li>
);
});
// 父组件
// ... 在父组件的 render 方法中
return (
<ul>
{todos.map(todo => (
// 传递一个普通的 JS 对象 (通过 .toJS() 获取)
<TodoItem key={todo.id} todo={todo} onToggle={this.toggleTodo} />
))}
</ul>
);
由于 TodoItem 被 React.memo 包裹,只有当 todo prop 的引用地址改变时它才会重新渲染,因为我们使用了 Immutable.js,只有当 todo 的某个属性真正改变时,才会创建一个新的 todo 对象,从而保证了性能。
进阶主题
Record
Record 是 Map 的一个子类,它定义了一组固定的属性,当你尝试访问一个未定义的属性时,它会返回 undefined 而不是抛出错误。
import { Record } from 'immutable';
// 定义 Record 的结构
const TodoRecord = Record({
id: null,
text: '',
completed: false,
});
const myTodo = new TodoRecord({ id: 1, text: 'Learn Records' });
console.log(myTodo.get('id')); // 1
console.log(myTodo.get('completed')); // false (默认值)
console.log(myTodo.get('priority')); // undefined (未定义的属性)
// 创建一个新的 Record 实例
const updatedTodo = myTodo.set('completed', true);
console.log(updatedTodo === myTodo); // false, 总是返回新实例
Seq
Seq (Sequence) 是 Immutable.js 中最强大的概念之一,它是一个惰性求值的序列,意味着它只在需要时才计算值,这对于处理大数据集非常有用,可以极大地提高性能和节省内存。
import { Seq } from 'immutable';
const mySeq = Seq({ a: 1, b: 2, c: 3 });
// Seq 是惰性的,下面的操作不会立即执行
const mappedSeq = mySeq.map(x => x * 2);
// 只有在需要结果时(转换为 JS 对象或数组),计算才会发生
console.log(mappedSeq.toJS()); // { a: 2, b: 4, c: 6 }
// 也可以转换为数组
console.log(mappedSeq.toArray()); // [2, 4, 6]
fromJS 和 toJS
fromJS: 一个强大的工具函数,可以递归地将普通的 JavaScript 对象和数组转换为 Immutable 的Map和List,非常适合初始化状态。toJS: 将 Immutable 对象递归地转换回普通的 JavaScript 对象,通常在需要将数据传递给非 Immutable 库(如 API 请求、DOM 操作)或用于调试时使用。
优缺点与替代方案
优点
- 可预测性:状态变化清晰明了,易于调试。
- 高性能:结构共享和 O(1) 的比较,优化了 React 渲染。
- 函数式编程:鼓励使用纯函数,减少副作用。
- 强大的 API:提供了丰富的、链式调用的 API。
缺点
- 学习曲线:需要理解新的数据结构和 API。
- 包体积:引入整个 immutable 库会增加应用的打包体积。
- 与原生 API 不兼容:不能直接使用
map.forEach()或Object.keys()等,必须使用 Immutable.js 提供的方法。
替代方案
-
Immer
-
理念:使用“可写”的语法来生成不可变的数据,它通过一个
produce函数,让你在一个代理对象上像写普通代码一样修改数据,然后在函数结束时,Immer 会为你计算出最小化的、不可变的新数据。 -
优点:
- 学习成本低:语法非常直观,几乎和写普通 JS 一样。
- 代码简洁:避免了深层嵌套的
setIn调用。 - 包体积小:比 Immutable.js 小得多。
-
示例:
import { produce } from 'immer'; const nextState = produce(baseState, draft => { // draft 是一个代理,你可以直接修改它 draft.user.todos[0].completed = true; }); -
目前社区更推荐使用 Immer,因为它在易用性和性能之间取得了更好的平衡。
-
-
原生 JavaScript (Spread/Rest 和
Object.freeze)- 对于简单的状态管理,可以使用 ES6 的展开运算符来创建新的对象和数组。
Object.freeze可以防止对象被扩展或修改,但它是浅冻结。- 缺点:对于深层嵌套的结构,代码会变得非常冗长和繁琐。
| 特性 | Immutable.js | Immer |
|---|---|---|
| 核心思想 | 提供一套全新的不可变数据结构 | 使用代理让你用“可写”语法生成不可变数据 |
| 易用性 | 较低,需要学习新 API | 极高,语法直观 |
| 代码风格 | 链式调用,如 map.setIn(...).delete(...) |
类似普通 JS,如 draft.a.b = c |
| 性能 | 极高,结构共享 | 极高,通过 Proxy 优化 |
| 包体积 | 较大 | 较小 |
| 推荐度 | 经典方案,功能强大,但社区热度下降 | 当前社区首选,更现代、易用 |
如何选择?
- 新项目:强烈推荐从 Immer 开始,它几乎在所有方面都优于 Immutable.js,并且更容易被团队接受。
- 维护旧项目:如果你的项目已经在使用 Immutable.js 并且运行良好,没有遇到明显的性能或维护问题,那么继续使用它是完全可以的,迁移到 Immer 需要一定的成本。
- 需要特定功能:如果你需要
Record或Seq这类非常特定的、高级的功能,Immer 的实现方式不能满足你,Immutable.js 仍然是一个不错的选择。
无论选择哪个,拥抱不可变数据的思想都是现代前端开发中一项非常重要的技能。
