资讯专栏INFORMATION COLUMN

Immutable.js 源码解析 --Map 类型

Alliot / 2281人阅读

摘要:那对于结构又要如何处理,没有结构的索引,那怎么办呢我们把键名变为哈希值就可以啦。表示存在,表示不存在。例如转换为二进制后为每位为一组,假定为取出需要的位。类对应带的,代表其子元素的数量。

上一片文章介绍的是 List 结构。那对于 Map 结构又要如何处理,没有 List 结构的索引,那怎么办呢? 我们把键名变为哈希值就可以啦~

HAMT:Hash Arrey Mapped Trie 。这个结构就是Map中所用到的。

immutable中的hash计算核心代码如下:

function hashString(string) {
  // This is the hash from JVM
  // The hash code for a string is computed as
  // s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + ... + s[n - 1],
  // where s[i] is the ith character of the string and n is the length of
  // the string. We "mod" the result to make it between 0 (inclusive) and 2^31
  // (exclusive) by dropping high bits.
  let hashed = 0;
  for (let ii = 0; ii < string.length; ii++) {
    hashed = (31 * hashed + string.charCodeAt(ii)) | 0;
  }
  return smi(hashed);
}
// v8 has an optimization for storing 31-bit signed numbers.
// Values which have either 00 or 11 as the high order bits qualify.
// This function drops the highest order bit in a signed number, maintaining
// the sign bit.
export function smi(i32) {
  return ((i32 >>> 1) & 0x40000000) | (i32 & 0xbfffffff);
}

上面只是一个计算hash值的函数,讨论的重点再下面呢。

一、Map 的结构

先看看Map的结构:

function makeMap(size, root, ownerID, hash) {
  const map = Object.create(MapPrototype);
  map.size = size;
  map._root = root;
  map.__ownerID = ownerID;
  map.__hash = hash;
  map.__altered = false;
  return map;
}

和list不同,Map中没有_tail,所有的数据都在_root里面。

再Map里,我们要分几种情况:
1、当键值对不超过8个
2、当键值对超过8个小于16个
3、当键值对超过16个
4、hash冲突怎么办

用下面这段代码调试看看每种情况的结构:

  let jsObj = {};
  for (let i = 0; i < 8; i++) {
      jsObj[Math.random()] = Math.random();
  }
  let map1 = Immutable.Map(jsObj);
  console.log(map1);
二、ArrayMapNode

当键值对不超过8个的时候采用的是这个结构。

这种方式是最简单的,所有键值对保存在 entries 里面。同时 get/set 方法都较为简单,直接遍历一下获取就好了。

从图中我们可以看到,immutable把key和value转换为一个数组的arr[0]和arr[1]来存储。

ArrayMapNode.prototype.update:

  update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
    .....
    const entries = this.entries;
    let idx = 0;
    const len = entries.length;
    for (; idx < len; idx++) { // 寻找需要更新的值
      if (is(key, entries[idx][0])) {
        break;
      }
    }
    const exists = idx < len;//是否存在这个key
    ......
    // 判断个数是否大于8 MAX_ARRAY_MAP_SIZE的值为8
    if (!exists && !removed && entries.length >= MAX_ARRAY_MAP_SIZE) {
      return createNodes(ownerID, entries, key, value);
    }
    ......
    const newEntries = isEditable ? entries : arrCopy(entries);

    if (exists) {
      if (removed) {
        idx === len - 1
          ? newEntries.pop()
          : (newEntries[idx] = newEntries.pop());
      } else {
        newEntries[idx] = [key, value];//存在就直接把值赋值给idx的位置
      }
    } else {
      newEntries.push([key, value]);//不存在 就是新增 push一个值进去
    }
    ......
    return new ArrayMapNode(ownerID, newEntries);
  }
}

上面代码中MAX_ARRAY_MAP_SIZE的定义:

const MAX_ARRAY_MAP_SIZE = SIZE / 4;
const MAX_BITMAP_INDEXED_SIZE = SIZE / 2;
const MIN_HASH_ARRAY_MAP_SIZE = SIZE / 4;
export const SIZE = 1 << SHIFT;
三、BitmapIndexedNode

当键值对超过8个小于16个的时候采用的是这个结构。

BitmapIndexedNode 的子节点是 ValueNode或者BitmapIndexedNode。在 BitmapIndexedNode 里面查/增/改元素,都需要用到 bit-map(位图)算法,BitmapIndexedNode.bitmap 存储的是键名和存储顺序的位图信息。例如 get 方法,通过 BitmapIndexedNode.bitmap,以及 key 名就可以获取该键值对的排列序号,从而获取到相应的 ValueNode。

BitmapIndexedNode.prototype.update:

update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
    if (keyHash === undefined) {
      keyHash = hash(key);//如果没有hash值计算hash值
    }
    // 根据BitmapIndexedNode中存储的bitmap判断当前传入的key是否在某个位置已经存在。bitmap用32 位(二进制)来记录元素是否存在。1表示存在,0表示不存在。
  
    // 例如:keyHash 转换为二进制后为11101110000110000001101001101 ,每5位为一组,shift假定为5
    // (keyHash >>> shift)& MASK 取出需要的5位。第一次取到从低位开始的第一个5位:01101。keyHashFrag的十进制的值为13
    // 1 << keyHashFrag 除第13位(从0开始)外,其他位都为0 即:10000000000000
    // bit & bitmap 得出bitmap的第13位是否为1 
    // eg:bitmap=010101110111010000101011100010100   即 010101110111010000111011100010100 & 10000000000000 发现第13位为1 则存在这个元素
    const keyHashFrag = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
    const bit = 1 << keyHashFrag;
    const bitmap = this.bitmap;
    const exists = (bitmap & bit) !== 0;
    ......
    // 计算1的数量,即算出key在BitmapIndexedNode的存储位置
    // eg:bitmap=010101110111010000111011100010100
    // bitmap & (bit - 1) 即 010101110111010000111011100010100 & 1111111111111 = 1011100010100
    // 使用 popCount 函数计算有多少个1 计算出 有 6 个 1
    // 即 idx = 6
    // 所以我们需要查找的元素在BitmapIndexedNode的存储位置为6
    const idx = popCount(bitmap & (bit - 1));
    // 如果这个位置有数据,取出当前BitmapIndexedNode中对应的数据,如果不存在,置为undefined
    const nodes = this.nodes;
    const node = exists ? nodes[idx] : undefined;
    const newNode = updateNode(
      node,
      ownerID,
      shift + SHIFT,
      keyHash,
      key,
      value,
      didChangeSize,
      didAlter
    );

    if (newNode === node) {
      return this;
    }

    // 判断是否超过16
    if (!exists && newNode && nodes.length >= MAX_BITMAP_INDEXED_SIZE) {
      return expandNodes(ownerID, nodes, bitmap, keyHashFrag, newNode);
    }

    ......
    // 生成新的Bitmap
    const newBitmap = exists ? (newNode ? bitmap : bitmap ^ bit) : bitmap | bit;

    // 生成新的nodes
    // eg:exists=false, idx=1情况:
    // oldArray: [vA, undefind, vC, vD]
    // newArray: [vA, newVNode, vC, vD]
    // exits=true情况,idx=8
    // 原来位置8指向新生成的BitmapIndexedNode
    const newNodes = exists
      ? newNode
        ? setAt(nodes, idx, newNode, isEditable)
        : spliceOut(nodes, idx, isEditable)
      : spliceIn(nodes, idx, newNode, isEditable);

    if (isEditable) {
      this.bitmap = newBitmap;
      this.nodes = newNodes;
      return this;
    }

    return new BitmapIndexedNode(ownerID, newBitmap, newNodes);
  }
}

这里我把popCount的源码也贴出来:

function popCount(x) {
  x -= (x >> 1) & 0x55555555;
  x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
  x = (x + (x >> 4)) & 0x0f0f0f0f;
  x += x >> 8;
  x += x >> 16;
  return x & 0x7f;
}
四、HashArrayMapNode

当键值对超过16个采用的是这个结构。

HashArrayMapNode 的亲子元素可以是 HashArrayMapNode/BitmapIndexedNode/ValueNode。由此看来巨量的键值对,将有 HashArrayMapNode/BitmapIndexedNode/ValueNode 组合而成,而每个 HashArrayMapNode 最多有32个亲子元素,BitmapIndexedNode 最多有16个亲子元素。 HashArrayMapNode 类对应带的 count,代表其子元素的数量。当需要读取的时候,直接键名的哈希值,就能够实现了

来一个庞大点的数据:

HashArrayMapNode.prototype.update:

  update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
    if (keyHash === undefined) {
      keyHash = hash(key);
    }
    // 计算当前这个层级的idx
    const idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
    const removed = value === NOT_SET;
    const nodes = this.nodes;
    const node = nodes[idx];
    ......
    const newNode = updateNode(
      node,
      ownerID,
      shift + SHIFT,
      keyHash,
      key,
      value,
      didChangeSize,
      didAlter
    );
    ......
    if (isEditable) {
      this.count = newCount;
      this.nodes = newNodes;
      return this;
    }
    return new HashArrayMapNode(ownerID, newCount, newNodes);
  }
}
function updateNode(
  node,
  ownerID,
  shift,
  keyHash,
  key,
  value,
  didChangeSize,
  didAlter
) {
  //没有子节点了,即找到了这个值
  if (!node) {
    if (value === NOT_SET) {
      return node;
    }
    SetRef(didAlter);
    SetRef(didChangeSize);
    return new ValueNode(ownerID, keyHash, [key, value]);
  }
  // 当还有子节点,则继续递归查找
  return node.update(
    ownerID,
    shift,
    keyHash,
    key,
    value,
    didChangeSize,
    didAlter
  );
}
五、HashCollisionNode

虽然说hash冲突的情况是很少的,但是也有这种情况出现的。比如 key = "Aa" key = "BB",其哈希值是完全一样的,这个时候就会启动 HashCollisionNode 对象,将相同的哈希值的对象都放在同一个 HashCollisionNode 里面,而这里面就是简单的线性读写数组了,没有之前的 Bitmapped 操作,毕竟一次性不可能有太多相同哈希值的键名出现。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99365.html

相关文章

  • Immutable.js 初识

    摘要:文章博客地址所创建的数据有一个迷人的特性数据创建后不会被改变。是的基类,使用该类时需要至少继承其子类中的一个。总结所提供的和固有的各有优势,未来有可能制定一套原生的规范,在这之前,是一个不错的选择。参考资料官方文档 文章博客地址:http://pinggod.com/2016/Immutable/ Immutable.js 所创建的数据有一个迷人的特性:数据创建后不会被改变。我们使用 ...

    Olivia 评论0 收藏0
  • 读懂immutable-js中的Map数据结构

    摘要:一向量字典树字典树,一种用空间换取时间的树形数据结构,主要特点是利用字符串的公共前缀来挺升查询性能。还有最终的数组表示的真实存储的键值,存储了,存储了。这其中还有一种节点进行了冲突的处理。 本文受深入探究Immutable.js的实现机制这篇文章启发,结合自己对Map源码的解读,谈谈我对immutable-js中map数据结构的理解,若有不正确的地方,欢迎指正。 一、Vector Tr...

    jone5679 评论0 收藏0
  • Immutable.js 源码解析 --List 类型

    摘要:首先是把原生的转换为的类型此时的就是上面的数组判断是否为空判断是否已经是的类型序列化数组判断是否超过。。。。。。若没有超过,所有数据都放在中。通过计算要寻找的节点的位置在这个例子中,的值依次是。我上面所解析的情况有构建修改查询。 一、存储图解 我以下面这段代码为例子,画出这个List的存储结构: let myList = []; for(let i=0;i 0 && size < SI...

    Doyle 评论0 收藏0
  • 如何优化你的超大型React应用 【原创精读】

    摘要:往往纯的单页面应用一般不会太复杂,所以这里不引入和等等,在后面复杂的跨平台应用中我会将那些技术一拥而上。构建极度复杂,超大数据的应用。 showImg(https://segmentfault.com/img/bVbvphv?w=1328&h=768); React为了大型应用而生,Electron和React-native赋予了它构建移动端跨平台App和桌面应用的能力,Taro则赋...

    cfanr 评论0 收藏0
  • 如何优化你的超大型React应用 【原创精读】

    摘要:往往纯的单页面应用一般不会太复杂,所以这里不引入和等等,在后面复杂的跨平台应用中我会将那些技术一拥而上。构建极度复杂,超大数据的应用。 showImg(https://segmentfault.com/img/bVbvphv?w=1328&h=768); React为了大型应用而生,Electron和React-native赋予了它构建移动端跨平台App和桌面应用的能力,Taro则赋...

    codecook 评论0 收藏0

发表评论

0条评论

Alliot

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<