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 结构共享
图片来自:Immutable 详解及 React 中实践

Immutable.js 将所有的数据处理为一个树结构,每一次修改操作仅造成对应节点以及其父节点的新建,不受影响的节点仍然保持原先的引用,从而节约了大量内存,减少了数据拷贝的操作。

结构共享的原理较为明晰,但是其中的树结构该如何组织呢?
对于 Map 结构,Immutable.js 计算每个键的的 Hash 值,并通过该 Hash 值为该键值对寻找对应的位置。
以简单的二叉树为例,一个 Hash 值为 10011的键值对,其寻址过程如下图所示:
Hash 寻址过程
由于二叉树每一个节点所能够容纳的子节点较少,在处理较大数据量时会导致树高过大,对于性能的提升有限。因此,在 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 === 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值