Immutable.js 原理与源码解析
本文主要对于 Immtable.js 的一些基本原理并以其中的 Map 数据结构为例,结合代码对具体的实现进行分析。
实际上,各色博客、专栏中关于 Immutable.js 的各种相关资料已经介绍的相当详尽了,本文不过是拾人牙慧而已。
前言
“不可变数据(Immutable data)”是什么?
不可变数据是指一旦创建就不可再被修改的数据。看到这一定义,你也许会说:“哦,这不就是 const / final / balabala 嘛”。
并不是。以 const (ES6)关键字为例,我们搬出 MDN 大法的定义来看:
const声明创建一个值的只读引用。但这并不意味着它所持有的值是不可变的,只是变量标识符不能重新分配。例如,在引用内容是对象的情况下,这意味着可以改变对象的内容(例如,其参数)。
来源:MDN const
划重点,const声明创建一个值的只读引用,const 关键词仅仅保证了引用是不可变的,但无法保证其引用对象的内容不可变。
与不可变数据不同,“持久化数据(Persistent data)” 强调的则是当数据被改变时,其修改前一版本的数据仍会被保留下来。
结合不可变数据以及持久化数据可以使得开发过程更为可控,状态变化的追踪更为有效,对于应用开发大有裨益。比起 Clojure 等原生支持持久化不可变数据结构的函数式语言,想要基于 JavaScript 进行开发就需要借助一些现有的库,例如 Facebook 推出的开源库:Immutable.js 。
FB对于函数式的偏爱是显而易见的,从 React 中的各种设计就可以看出一些端倪,结合 Immutable.js 也就往函数式的道路更近一步了。
Immutable.js
Immutable.js 为应用开发提供了一系列基础的不可变数据结构,例如 Map、Set、List 等。
简单来讲,Immutable.js 也就是将一些数据结构进行了包装,并对外提供了创建、修改、删除的 API,隐藏了内部可变的细节,表现出了外部不可变的特性。同时,为了降低重复创建对象、拷贝数据所带来的内存以及 CPU 开销,Immutable 采用了结构共享等措施以提升数据操作效率。
事实上,Immutable.js 所提供的几个数据结构的本质原理均相类似,本文后续内容将基于 Map 数据结构对于 Immutable.js 的原理以及部分的具体实现进行简单的介绍。
树结构
JavaScript 中普通的 Map 结构也可以看作是一个展平的臃肿的树,事实上,通过每次使用 Object.assign
进行对象的拷贝,也能够实现一个简单的不可变数据结构。但是这一方式需要巨大的内存与 CPU 开销,因此在实际使用中没有价值。
那么,Immutable.js 中是怎样处理的呢?可以通过一张图来简单一窥 Immutable.js 的结构共享机制。
Immutable.js 将所有的数据处理为一个树结构,每一次修改操作仅造成对应节点以及其父节点的新建,不受影响的节点仍然保持原先的引用,从而节约了大量内存,减少了数据拷贝的操作。
结构共享的原理较为明晰,但是其中的树结构该如何组织呢?
对于 Map 结构,Immutable.js 计算每个键的的 Hash 值,并通过该 Hash 值为该键值对寻找对应的位置。
以简单的二叉树为例,一个 Hash 值为 10011
的键值对,其寻址过程如下图所示:
由于二叉树每一个节点所能够容纳的子节点较少,在处理较大数据量时会导致树高过大,对于性能的提升有限。因此,在 Immutable.js 中所实际使用的树为 32 叉树,也就意味着每一次向下搜索的过程均需要根据 Hash 值中最末 5bit 的值计算位置。
Map 数据结构
要介绍 Map 数据结构,首先让我们来看看 Map 类的定义(其中部分不重要或者暂不介绍的函数以及具体实现已被省略):
export class Map extends KeyedCollection {
constructor(value) {
return value === null || value === undefined
? emptyMap()
: isMap(value) && !isOrdered(value)
? value
: emptyMap().withMutations(map => {
const iter = KeyedCollection(value);
assertNotInfinite(iter.size);
iter.forEach((v, k) => map.set(k, v));
});
}
// 与构造函数相类似都是将多个键值对取出并在 withMutations 中完成赋值,只不过参数不同
static of(...keyValues) {
balabala... }
// Map 类的 get set remove 方法基本上就是将具体操作交给节点执行,只是作为入口 Api 提供
get(k, notSetValue) {
return this._root
? this._root.get(0, undefined, k, notSetValue)
: notSetValue;
}
set(k, v) {
return updateMap(this, k, v);
}
remove(k) {
return updateMap(this, k, NOT_SET);
}
__ensureOwner(ownerID) {
if (ownerID ===