immutable源码解析

immutable

状态机

trie树

位运算快,直接对整数在内存中的二进制位进行操作,不需要转成十进制

参考文章:http://www.hypirion.com/musings/understanding-persistent-vector-pt-1 系列

对数组的处理 vector trie

image.png

trie树
最大度为32的trie树,存储在_root里
不足32的放到_tail 里

update过程
向下遍历tree,并保存路径,直到找到包含该数据的叶子结点为止,然后拷贝该叶子结点的所有数据,生成一个新的对象,再沿路径返回,拷贝所有路径上的结点,更新引用

append过程
三个边界条件:
1.最右叶子结点有空余
2.最右叶子结点无空余,但根结点有空余
3.最有叶子结点无空余,根结点无空余

pop过程
1.最右侧结点有多余一个结点
2.最后侧结点只有一个结点

1.当size是32的幂次方时,再插入一个数据会产生跟结点溢出。深度+1,初始化一个新的tail

调用updateVNode更新

  • 先创建一个空的immutable List对象
emptyList()

_capacity = Array.size
_level = 每次-5,树的深度,为0时是叶子结点。默认值是5,存储指数部分,用于方便位运算
_origin?
_tail = 32个为一组,存放最后剩余的数据
_root = trie树实现
_altered
_hash
_ownerID: 新创建的节点ownerID总为undefined,表示结点之间的引用关系
  需要更新的时候,将ownerId置为空对象,并在寻找叶子结点的过程中将中间结点的ownerId的引用都改成该对象 ??
  • 将传入的数据序列化
    // ArraySeq
    iter = {
    size: 数组的length,
    _array: 传入数组的引用
    }

  • 将array分组,保存到_root 及_tail中

empty.withMutations(function (list) {
      // 根据size初始化树 -> setListBounds
      list.setSize(size);
      // 遍历ArraySeq,并向树中填充数据 -> updateList
      iter.forEach(function (v, i) { return list.set(i, v); });
})

setListBounds方法


setListBounds

// 根据newTailOffset,判断是否需要增加树的深度,默认为2。(1 << 5 + 5) = 1024
while (newTailOffset >= 1 << newLevel + SHIFT) {
    // 初始化增加的节点
    newRoot = new VNode(
      newRoot && newRoot.array.length ? [newRoot] : [],
      owner
    );
    newLevel += SHIFT;
  }


// Merge Tail into tree.
// 当新加入一个元素,并且加入后恰好超过32个元素
  if (
    oldTail &&
    newTailOffset > oldTailOffset &&
    newOrigin < oldCapacity &&
    oldTail.array.length
  ) {
    // 因为需要更新跟结点,所以需要复制root
    newRoot = editableVNode(newRoot, owner);
    var node = newRoot;

    // 往下找,直到找到叶子结点的父结点,将tail插入末尾
    for (var level = newLevel; level > SHIFT; level -= SHIFT) {
      var idx = oldTailOffset >>> level & MASK;
      node = (node.array[idx] = editableVNode(node.array[idx], owner));
    }
    node.array[oldTailOffset >>> SHIFT & MASK] = oldTail;
  }

updateList方法
找出trailOffset位置:size - 1 >>> SHIFT << SHIFT

  • SIZE=32 32为一组
    SHIFT=5 2^5次方是32
    ::size - 1 >>> SHIFT << SHIFT::
// 每32个为一组,拿到最末尾的起始位置
function getTailOffset(size) {
  return size < SIZE ? 0 : size - 1 >>> SHIFT << SHIFT;
}

size - 1 >>> SHIFT << SHIFT 先无符号右移,再有符号左移
找到下一个数组元素开始的位置。
eg: size=33 -> 33 -1 >>> 5 << 5 = 32
size=65 -> 65 - 1 >>> 5 << 5 = 32
size=68 -> 68 - 1 >>> 5 << 5 = 64
找到的位置总是2^5的倍数

updateList

// 若index大于等于size,则调用setListBounds处理边界问题、更新tree node,再将数据set进对应位置
if (index >= list.size || index < 0) {
    return list.withMutations(function (list) {
      index < 0
        ? setListBounds(list, index).set(0, value)
        : setListBounds(list, 0, index + 1).set(index, value);
    });
  }

// 若index小于size,则分情况update tree node
if (index >= getTailOffset(list._capacity)) {
    // 将数据保存在_trail中
    newTail = updateVNode(newTail, list.__ownerID, 0, index, value, didAlter);
  } else {
    // 将数据保存在_root中
    newRoot = updateVNode(
      newRoot,
      list.__ownerID,
      list._level,
      index,
      value,
      didAlter
    );
  }

updateVNode 方法
index >>> level & MASK


updateVNode

// 找到index在第几层的位置
// level是5的倍数。level=10是第三层,5是第一层,0是最后一层叶子结点
 var idx = index >>> level & MASK;

 if (level > 0) {
    不断调用updateVNode寻找下一层节点
 }

 // 浅copy所有node.array,并生成一个新的vNode对象
 newNode = editableVNode(node, ownerID);
 // 更新
 newNode.array[idx] = value;

 return newNode
  • 拿到rowIndex数据
    listNodeFor方法可以找到当前index属于叶子结点的第几组,并返回list

根据rowIndex拿到rowIndex存在的列表
::1 << list._level + SHIFT::
::rawIndex >>> level & MASK::

// ListNodeFor

// 判断index是否属于tail部分
if (rawIndex >= getTailOffset(list._capacity)) {
  // 返回_tail中的数据
}

// 判断index是否属于root部分
// eg:如果size=10000,_level=10, 则index肯定在2^10 ~ 2^11次方之间
if (rawIndex < 1 << list._level + SHIFT) {
   var node = list._root;
   var level = list._level;
   // level的变化10 -> 5 -> 0
   while (node && level > 0) {
      // 从根节点开始深度优先遍历找每一层
      node = node.array[rawIndex >>> level & MASK];
      level -= SHIFT;
   }
   // 返回_root中叶子结点
   return node;
  
  // rawIndex >>> level & MASK得到第几个组(32个为一组)
  // eg:  23 >>> 5 & 31 = 0 (在第0组里)
  //      32 >>> 5 & 31 = 1 (在第1组里)
}

根据列表找到具体的rowIndex数据

// eg: index = 45 则 45 & 31 = 13,即在第13位
node.array[index & MASK]

对Map的处理 HAMT

image.png

MAX_ARRAY_MAP_SIZE=8 一个ArrayMapNode里最多放8个entities
经过Map()包装,会成为一个ArrayMapNode类的实例
immutable.Map({
‘a’: ‘value’
})
当size>8时,key会被hash化,Hashing strings · jsPerf
构建BitmapIndexedNode第九个元素会放到这里,之后把前8个遍历一遍都转化成放到这里,剩下的都是在bitmapNode上update

ValueNode 叶子结点,即数据存储位置

BitmapIndexedNode 子结点,压缩后的hashtable,头部增加bitmap用于表示压缩后元素的位置

BitmapIndexedNode.png

ArrayMapNode 数组,在size<8时的数据结构

HashArrayMapNode hashtable

HashCollisionNode 用于存放hash碰撞后的ValueNode

  • 判断是否是Map
    如果不是:创建一个新的Map
size: 0,
_root: undefined,
__ownerId = undefined,
__hash = undefined,
__altered: false,
…其他Map定义的方法及prototype
  • 序列化map
function keyedSeqFromValue(value) {
  var seq = Array.isArray(value)
    ? new ArraySeq(value)
    : isIterator(value)
        ? new IteratorSeq(value)
        : hasIterator(value) ? new CollectionSeq(value) : undefined;
  if (seq) {
    return seq.fromEntrySeq();
  }
  if (typeof value === 'object') {
    return new ObjectSeq(value);
  }
  throw new TypeError(
    'Expected Array or collection object of [k, v] entries, or keyed object: ' +
      value
  );

ArrayMapNode?
当size大于8时,用BitmapIndexedNode


MergeIntoNodes

// 根据keyHash计算idx, idx在1-31之间
var idx1 = (shift === 0 ? node.keyHash : node.keyHash >>> shift) & MASK;
  var idx2 = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;

...

// 总是根据id顺序放
idx1 < idx2
        ? [node, newNode]
        : [newNode, node])

return new BitmapIndexedNode(ownerID, 1 << idx1 | 1 << idx2, nodes);

BitmapIndexedNode.prototype.update


// hash(key)
if (keyHash === undefined) {
    keyHash = hash(key);
  }

// 根据BitmapIndexedNode中存储的bitmap判断当前传入的key是否在某个位置已经存在。bitmap为00001010(一共有32位,这里只做为实例显示几位),其中二进制为1的表示元素存在
// 例如:keyHash 转换为二进制后为11101110000110000001101000001 ,每5位为一组,shift假定为5
// (keyHash >>> shift)& MASK 取出需要的5位,结果为26
// 1 << keyHashFrag 除第26位外,其他位都为0
// bit & bitmap 得出bitmap的第26位是否为1
  var keyHashFrag = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
  var bit = 1 << keyHashFrag;
  var bitmap = this.bitmap;
  var exists = (bitmap & bit) !== 0;

// 省略其他代码...

// 计算1的数量,即算出key在BitmapIndexedNode的存储位置
// eg:101101,idx为4
var idx = popCount(bitmap & bit - 1);

// 如果这个位置有数据,取出当前BitmapIndexedNode中对应的数据,如果不存在,置为undefined
var nodes = this.nodes;
var node = exists ? nodes[idx] : undefined;

// 更新node
// BitmapIndeedNode情况
// 1.如果两个key有hash碰撞,则new HashCollisionNode
// 2.通过keyhash算出idx的大小,按大小顺序生成一个
BitmapIndexedNode
// undefined情况
// 直接生成一个新的ArrayNdoe
var newNode = updateNode(
    node,
    ownerID,
    shift + SHIFT,
    keyHash,
    key,
    value,
    didChangeSize,
    didAlter
  );

// ...

var isEditable = ownerID && ownerID === this.ownerID;
// 生成新的Bitmap
var newBitmap = exists ? newNode ? bitmap : bitmap ^ bit : bitmap | bit;
// 生成新的nodes
// eg:exists=false, idx=1情况:
// oldArray: [vA, vC, vD]
// newArray: [vA, newVNode, vC, vD]
//   exits=true情况,idx=8
// 原来位置8指向新生成的BitmapIndexedNode
var newNodes = exists
    ? newNode
        ? setIn(nodes, idx, newNode, isEditable)
        : spliceOut(nodes, idx, isEditable)
    : spliceIn(nodes, idx, newNode, isEditable);

当BitmapIndexedNode.nodes.length > 16时,用HashArrayMapNode,即未压缩之前的hashtable

HashArrayMapNode

HashArrayMapNode.prototype.update

    // hash(key)
    if (keyHash === undefined) {
      keyHash = hash(key);
    }
    // 第一次寻找时shift为0,keyHash & MASK取低5位,找到下一层结点的位置
    const idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
    const removed = value === NOT_SET;
    const nodes = this.nodes;
    const node = nodes[idx];

    if (removed && !node) {
      return this;
    }

    // 到下一层中寻找 shift + 5,再向左取5位
    // 按BitMapIndexedNode或HashArrayMapNode继续寻找
    // 如果node是ValueNode,则进入MergeIntoNodes逻辑
    // 如果node是unefined,则会创建一个valueNode挂在当前HashArrayMapNode上
    const newNode = updateNode(
      node,
      ownerID,
      shift + SHIFT,
      keyHash,
      key,
      value,
      didChangeSize,
      didAlter
    );
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容