Immutable
Javascript 中不可变的集合 官网
immutable 引入
在 JavaScript 中有引用类型和基本类型。
- 如果变量 a 是基本类型,将 a 赋值给变量 b,再修改 b 的值,则变量 a 不受影响
- 如果变量 a 是引用类型,将 a 赋值给变量 b, 则 b 与 a 是同一个对象的引用,若修改 b 对象的属性,a 对象的该属性也会跟着变化。
解决方案就是深拷贝,比如最简单的方式JSON.parse(JSON.stringify(obj))
。
但是深拷贝这种方式比较耗费性能(空间和时间),有没有折中的方案呢?即修改 b 的同时不影响 a,且比深拷贝节省性能。
所以有了 immutable.js,简单说就是按需深拷贝。
此外还有一个重要原因,开发中经常遇到函数带有副作用,副作用的函数修改了一个引用类型某个属性的值或者浅 copy 的问题,immutable 为此类问题提供了一种优雅的解决方案。
immutable 简介
- Immutable Data 就是一旦创建,就不能再被更改的数据
- 对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象;
- Immutable 实现的原理是 Persistent Data Structure(持久化数据结构): 也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。

immutable.js
Facebook 工程师 Lee Byron 花费 3 年时间打造,与 React 同期出现,但没有被默认放到 React 工具集里(React 提供了简化的 Helper)。它内部实现了一套完整的 Persistent Data Structure,还有很多易用的数据类型。像 Collection、List、Map、Set、Record、Seq。有非常全面的 map、filter、groupBy、reduce、find 函数式操作方法。同时 API 也尽量与 Object 或 Array 类似。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| let foo = { a: { b: 1 } }; let bar = foo; bar.a.b = 2; console.log(foo.a.b); console.log(foo === bar);
import Immutable from "immutable"; var foo = Immutable.fromJS({ a: { b: 1 } }); var bar = foo.setIn(["a", "b"], 2); console.log(foo.getIn(["a", "b"]));
console.log(foo === bar);
|
API
Map 类型
1 2 3 4 5 6 7 8 9 10 11
| const { Map } = require("immutable"); const map1 = Map({ a: 1, b: 2, c: [1, 2] });
const map2 = map1.set("b", 50);
console.log(map1.equals(map2));
console.log(map1.get("b"), map2.get("b")); console.log(map1.get("c") === map2.get("c"));
|
不可变类型比较
引用类型的比较是基于引用地址,而不可变对象的比较是基于集合的值是否一一相同
1 2 3 4 5 6 7 8 9 10 11 12 13
| const { Map } = require("immutable"); const map1 = Map({ a: 1, b: 2, c: 3 }); const map2 = Map({ a: 1, b: 2, c: 3 }); map1.equals(map2);
map1 === map2;
const map3 = map2.set("b", 2); map3 === map2;
|
补充: 不可变对象的比较是基于值,算法复杂度 O(N), 而引用类型比较算法复杂度 O(1), 因此使用时要考虑性能权衡。
JavaScript 优先的 API
JavaScript |
immutable |
Map |
Map |
Set |
Set |
Array |
List |
Object |
fromJS or Map |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const { Map, List } = require("immutable"); const map1 = Map({ a: 1, b: 2, c: 3, d: 4 }); const map2 = Map({ c: 10, a: 20, t: 30 }); const obj = { d: 100, o: 200, g: 300 }; const map3 = map1.merge(map2, obj);
const list1 = List([1, 2, 3]); const list2 = List([4, 5, 6]); const array = [7, 8, 9]; const list3 = list1.concat(list2, array);
const { List } = require("immutable"); const aList = List([1, 2, 3]); const anArray = [0, ...aList, 4, 5];
|
嵌套结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const { fromJS } = require("immutable"); const nested = fromJS({ a: { b: { c: [3, 4, 5] } } }); console.log(typeof nested.toJS());
const nested2 = nested.mergeDeep({ a: { b: { d: 6 } } });
console.log(nested2.getIn(["a", "b", "d"])); console.log(nested2.getIn(["a"]).getIn(["b", "d"]));
const nested3 = nested2.updateIn(["a", "b", "d"], (value) => value + 1); console.log(nested3);
const nested4 = nested3.updateIn(["a", "b", "c"], (list) => list.push(6));
|
批处理突变
默认情况下 immutable.js 中的 api 每执行一次就会返回一个新的 immutable 对象
那么如果我只需要最后生成的 immutable 对象,中间的对象都不要,可不可以只返回新一个新对象呢?
这就用到了 withMutations
,批量处理节省开销,目前只有少数方法 set push pop 可直接应用于持久化数据结构
1 2 3 4 5 6 7 8
| const { List } = require("immutable"); const assert = require("assert"); const list1 = List([1, 2, 3]); const list2 = list1.withMutations(function (list) { list.push(4).push(5).push(6); }); console.log(list1.size); console.log(list2.size);
|
惰性序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const { Seq } = require('immutable'); const oddSquares = Seq([1, 2, 3, 4, 5, 6, 7, 8]) .filter(x => { console.log('filter', x) return x % 2 !== 0 }) .map(x => x * x);
console.log(oddSquares.toJS());
const { Range } = require('immutable'); const a = Range(990, 1010)
.map(n => { console.log(n) return -n; }) .reduce((r, n) => r \* n, 1);
console.log('---', a)
|
cursor
由于 Immutable 数据一般嵌套非常深,为了便于访问深层数据,Cursor 提供了可以直接访问这个深层数据的引用。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import Immutable from "immutable"; import Cursor from "immutable/contrib/cursor";
let data = Immutable.fromJS({ a: { b: { c: 1 } } });
let cursor = Cursor.from(data, ["a", "b"], (newData) => { console.log(newData); });
cursor.get("c"); cursor = cursor.update("c", (x) => x + 1); cursor.get("c");
|
优缺点
优点
- 降低了 mutable 带来的复杂性
1 2 3 4 5 6
| function touchAndLog(touchFn) { let data = { key: "value" }; touchFn(data); console.log(data.key); }
|
- 节省内存
会尽量复用内存,甚至以前使用的对象也可以再次被复用。没有被引用的对象会被垃圾回收。
1 2 3 4 5 6 7 8 9
| import { Map } from "immutable"; let a = Map({ select: "users", filter: Map({ name: "Cam" }), }); let b = a.set("select", "people");
a === b; a.get("filter") === b.get("filter");
|
- Undo/Redo,Copy/Paste
因为每次数据都是不一样的,所有可以存储在数组里,想回退到哪里就拿出对应数据即可
缺点
- 需要学习新的 API
- 容易与原生对象混淆
- 虽然 Immutable.js 尽量尝试把 API 设计的原生对象类似,有的时候还是很难区别到底是 Immutable 对象还是原生对象,容易混淆操作。
- Immutable 中的 Map 和 List 虽对应原生 Object 和 Array,但操作非常不同,比如你要用 map.get(‘key’) 而不是 map.key,array.get(0) 而不是 array[0]。另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值;
- 当使用外部库的时候,一般需要使用原生对象,也很容易忘记转换。
下面给出一些办法来避免类似问题发生:
- 使用 TypeScript 这类有静态类型检查的工具;
- 约定变量命名规则: 如所有 Immutable 类型对象以 $$ 开头;
- 使用 Immutable.fromJS 而不是 Immutable.Map 或 Immutable.List 来创建对象,这样可以避免 Immutable 和原生对象间的混用;
使用 immutable.js 优化 react
- React 可以使用 shouldComponentUpdate()进行性能优化,但它默认返回 true,即始终会执行 render() 方法,然后做 Virtual DOM 比较,并得出是否需要做真实 DOM 更新;
- 可以在 shouldComponentUpdate 周期里执行 deepCopy 和 deepCompare 避免无意义的 render,但 deepFn 也很耗时;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { is } from "immutable";
shouldComponentUpdate: (nextProps = {}, nextState = {}) => { const thisProps = this.props || {}, thisState = this.state || {};
if ( Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length ) { return true; }
for (const key in nextProps) { if (!is(thisProps[key], nextProps[key])) { return true; } }
for (const key in nextState) { if ( thisState[key] !== nextState[key] && !is(thisState[key], nextState[key]) ) { return true; } } return false; };
|
immer
官方地址
对于处理修改引用类型副作用问题,ImmutableJS 有两个较大的不足:
- 需要使用者学习它的数据结构操作方式,没有 Immer 提供的使用原生对象的操作方式简单、易用;
- 它的操作结果需要通过toJS方法才能得到原生对象,这使得在操作一个对象的时候,时刻要知道操作的是原生对象还是 ImmutableJS 的返回结果,稍不注意,就会产生问题;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| let currentState = { x: [2], };
let o1 = currentState; o1.a = 1;
let o2 = { ...currentState, }; o2.x.push(3); console.log(currentState);
import produce from "immer"; let state = { x: [2], }; let obj1 = produce(state, (draft) => { draft.a = 1; }); let obj2 = produce(state, (draft) => { draft.x.push(3); });
console.log(state, obj1, obj2);
|
概念说明
produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState
- currentState: 被操作对象的最初状态
- draftState: 根据 currentState 生成的草稿状态,它是 currentState 的代理,对 draftState 所做的任何修改都将被记录并用于生成 nextState 。在此过程中,currentState 将不受影响
- nextState: 根据 draftState 生成的最终状态
- produce: 用来生成 nextState 或 producer 的函数
- producer: 通过 produce 生成,用来生产 nextState ,每次执行相同的操作
- recipe: 用来操作 draftState 的函数
使用 immer 优化 react
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const { members } = this.state; this.setState({ members: [ { ...members[0], age: members[0].age + 1, }, ...members.slice(1), ], });
const { members } = this.state; this.setState( this.setState( produce(members, (draft) => { draft.members[0].age++; }) ) );
|
reduce 中使用 immer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function reducer(state = { name: "章三", age: 12 }, action) { return immer.produce(state, (draft) => { switch (action.type) { case "ADD": draft.age++; return; default: return; } }); }
const store = Redux.createStore(reducer); class App extends React.Component { componentDidMount() { store.subscribe(() => { this.forceUpdate(); }); } render() { const state = store.getState(); return ( <div> <p> {state.name}今年{state.age}周岁 </p> <button onClick={() => store.dispatch({ type: "ADD" })}>过生日</button> </div> ); } } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />);
|