ImmutableJS 入门教学
前言
一般来说在 JavaScript 中有两种资料类型:Primitive(String、Number、Boolean、null、undefinded)和 Object(Reference)。在 JavaScript 中物件的操作比起 Java 容易很多,但也因为相对弹性不严谨,所以产生了一些问题。在 JavaScript 中的 Object(物件)资料是 Mutable(可以变的),由于是使用 Reference 的方式,所以当修改到复制的值也会修改到原始值。例如下面的 map2
值是指到 map1
,所以当 map1
值一改,map2
的值也会受影响。
var map1 = { a: 1 }; var map2 = map1; map2.a = 2
通常一般作法是使用 deepCopy
来避免修改,但这样作法会产生较多的资源浪费。为了很好的解决这个问题,我们可以使用 Immutable Data
,所谓的 Immutable Data 就是一旦建立,就不能再被修改的数据资料。
为了解决这个问题,在 2013 年时 Facebook 工程师 Lee Byron 打造了 ImmutableJS,但并没有被预设放到 React 工具包中(虽然有提供简化的 Helper),但 ImmutableJS
的出现确实解决了 React
甚至 Redux
所遇到的一些问题。
以下范例即是引入了 ImmutableJS
的效果,读者可以发现,虽然我们操作了 map1
的值,但会发现原本的 map1
并未受到影响(因为任何修改都不会影响到原始资料),虽然使用 deepCopy
也可以模拟类似的效果但会浪费过多的计算资源和记忆体,ImmutableJS
则可以容易地共享没有被修该到的资料(例如下面的资料 b
即为 map1
所 map2
共享),因而有更好的效能表现。
import Immutable from 'immutable'; var map1 = Immutable.Map({ a: 1, b: 3 }); var map2 = map1.set('a', 2); map1.get('a'); // 1 map2.get('a'); // 2
ImmutableJS 特性介绍
ImmutableJS 提供了 7 种不可修改的资料类型:List
、Map
、Stack
、OrderedMap
、Set
、OrderedSet
、Record
。若是对 Immutable 物件操作都会回传一个新值。其中比较常用的有 List
、Map
和 Set
:
-
Map:类似于 key/value 的 object,在 ES6 也有原生
Map
对应const Map= Immutable.Map; // 1. Map 大小 const map1 = Map({ a: 1 }); map1.size // => 1 // 2. 新增或取代 Map 元素 // set(key: K, value: V) const map2 = map1.set('a', 7); // => Map { "a": 7 } // 3. 删除元素 // delete(key: K) const map3 = map1.delete('a'); // => Map {} // 4. 清除 Map 内容 const map4 = map1.clear(); // => Map {} // 5. 更新 Map 元素 // update(updater: (value: Map<K, V>) => Map<K, V>) // update(key: K, updater: (value: V) => V) // update(key: K, notSetValue: V, updater: (value: V) => V) const map5 = map1.update('a', () => (7)) // => Map { "a": 7 } // 6. 合并 Map const map6 = Map({ b: 3 }); map1.merge(map6); // => Map { "a": 1, "b": 3 }
-
List:有序且可以重复值,对应于一般的 Array
const List= Immutable.List; // 1. 取得 List 长度 const arr1 = List([1, 2, 3]); arr1.size // => 3 // 2. 新增或取代 List 元素内容 // set(index: number, value: T) // 将 index 位置的元素替换 const arr2 = arr1.set(-1, 7); // => [1, 2, 7] const arr3 = arr1.set(4, 0); // => [1, 2, 3, undefined, 0] // 3. 删除 List 元素 // delete(index: number) // 删除 index 位置的元素 const arr4 = arr1.delete(1); // => [1, 3] // 4. 插入元素到 List // insert(index: number, value: T) // 在 index 位置插入 value const arr5 = arr1.insert(1, 2); // => [1, 2, 2, 3] // 5. 清空 List // clear() const arr6 = arr1.clear(); // => []
-
Set:没有顺序且不能重复的列表
const Set= Immutable.Set; // 1. 建立 Set const set1 = Set([1, 2, 3]); // => Set { 1, 2, 3 } // 2. 新增元素 const set2 = set1.add(1).add(5); // => Set { 1, 2, 3, 5 } // 由于 Set 为不能重复集合,故 1 只能出现一次 // 3. 删除元素 const set3 = set1.delete(3); // => Set { 1, 2 } // 4. 取联集 const set4 = Set([2, 3, 4, 5, 6]); set1.union(set4); // => Set { 1, 2, 3, 4, 5, 6 } // 5. 取交集 set1.intersect(set4); // => Set { 2, 3 } // 6. 取差集 set1.subtract(set4); // => Set { 1 }
ImmutableJS 的特性整理
-
Persistent Data Structure 在
ImmutableJS
的世界里,只要资料一被创建,就不能修改,维持Immutable
。就不会发生下列的状况:var obj = { a: 1 }; funcationA(obj); console.log(obj.a) // 不确定结果为多少?
使用
ImmutableJS
就没有这个问题:// 有些开发者在使用时会在 ``Immutable` 变数前加 `$` 以示区隔。 const $obj = fromJS({ a: 1 }); funcationA($obj); console.log($obj.get('a')) // 1
-
Structural Sharing 为了维持资料的不可变,又要避免像
deepCopy
一样复制所有的节点资料而造成的资源损耗,在ImmutableJS
使用的是 Structural Sharing 特性,亦即如果物件树中一个节点发生变化的话,只会修改这个节点和和受它影响的父节点,其他节点则共享。const obj = { count: 1, list: [1, 2, 3, 4, 5] } var map1 = Immutable.fromJS(obj); var map2 = map1.set('count', 4); console.log(map1.list === map2.list); // true
-
Support Lazy Operation
Immutable.Range(1, Infinity) .map(n => -n) // Error: Cannot perform this action with an infinite size. Immutable.Range(1, Infinity) .map(n => -n) .take(2) .reduce((r, n) => r + n, 0); // -3
-
丰富的 API 并提供快速转换原生 JavaScript 的方式 在 ImmutableJS 中可以使用
fromJS()
、toJS()
进行 JavaScript 和 ImmutableJS 之间的转换。但由于在转换之间会非常耗费资源,所以若是你决定引入ImmutableJS
的话请尽量维持资料处在Immutable
的状态。 -
支持 Functional Programming
Immutable
本身就是 Functional Programming(函数式程式设计)的概念,所以在ImmutableJS
中可以使用许多 Functional Programming 的方法,例如:map
、filter
、groupBy
、reduce
、find
、findIndex
等。 -
容易实现 Redo/Undo 历史回顾
React 效能优化
ImmutableJS
除了可以和 Flux/Redux
整合外,也可以用于基本 react 效能优化。以下是一般使用效能优化的简单方式:
传统 JavaScript 比较方式,若资料型态为 Primitive 就不会有问题:
// 在 shouldComponentUpdate 比较接下来的 props 是否一致,若相同则不重新渲染,提升效能 shouldComponentUpdate (nextProps) { return this.props.value !== nextProps.value; }
但当比较的是物件的话就会出现问题:
// 假设 this.props.value 为 { foo: 'app' } // 假设 nextProps.value 为 { foo: 'app' }, // 虽然两者值是一样,但由于 reference 位置不同,所以视为不同。但由于值一样应该要避免重复渲染 this.props.value !== nextProps.value; // true
使用 ImmutableJS
:
var SomeRecord = Immutable.Record({ foo: null }); var x = new SomeRecord({ foo: 'app' }); var y = x.set('foo', 'azz'); x === y; // false
在 ES6 中可以使用官方文件上的 PureRenderMixin
进行比较,可以让程式码更简洁:
import PureRenderMixin from 'react-addons-pure-render-mixin'; class FooComponent extends React.Component { constructor(props) { super(props); this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); } render() { return <div className={this.props.className}>foo</div>; } }
总结
虽然 ImmutableJS
的引入可以带来许多好处和效能的提升但由于引入整体档案较大且较具侵入性,在引入之前可以自行评估看看是否合适于目前的专案。接下来我们将在后面的章节讲解如何将 ImmutableJS
和 Redux
整合应用到实务上的范例。