本文受深刻探究Immutable.js的实现机制这篇文章启发,结合本身对Map源码的解读,谈谈我对immutable-js中map数据结构的理解,如有不正确的地方,欢迎指正。数组
Trie 字典树,一种用空间换取时间的树形数据结构,主要特色是利用字符串的公共前缀来挺升查询性能。好比一组字符串 ["abc","ab","bd","dda"] 它的字典树结构以下:数据结构
红色节点表明按查找路径下来能够组成一个单词,这样在查找是否存在"abc"时,每一个字符逐个进行对比,时间复杂度为O(len) = 3,len为要查找的字符串的长度,而按照通常逐个对比的方式,时间复杂度为O(lenxn) = 4x3 = 12,n为字符串的个数,显然字典树的方式效率更高。app
Vector Trie 是在 Trie 的基础上实现了以向量数组的形式进行数据分组存储,每个被存储的值所对应的key都映射为数组的下标。
好比这样的数据结构 {‘000’: ‘banana’, ‘001’: ‘grape’, ‘010’: ‘lemon’, ‘011’: ‘orange’, ‘100’: ‘apple’},每一个key映射为数组的索引值,这样生成简单的两位数组的Vector Trie是这样子的:post
咱们能够将key映射为数字,而后再对应到数组中,好比一个key为 8899 映射为 5 个长度(即5进制)的数字是241044, 那么他要生成深度为6的数据结构,每一层的索引就是每位的数字,这样对每一个索引都要进行数学计算,而immutable-js中使用了效率更高的位运算来生成索引,将数据进行分区。好比,8899 映射为二进制位:10001011000011,1表明此位有数据,0表明没有数据。这样若是每层的数组长度是7,第一层的存储状况能够直接位运算 8899 >> 7 获得 1000101(69),第二层 8899 & 127 (Math.pow(2, 7) - 1) 获得 1000011 (67)。
位运算能够很方便获得每位是否存储值的状况,计算速度也要比数学运算快不少。性能
Map中有主要的这几种节点类型:ArrayMapNode,ValueNode,BitmapIndexedNode,HashArrayMapNode。在每次set时,节点类型会在这几个之间转换。还有最终的entry数组表示的真实存储的键值,entry[0]存储了key,entry[1]存储了value。ValueNode能够看做叶子节点,存储了entry值。
首先,若是存储的键值对不大于8,那么生成entry直接存储在ArrayMapNode数组中,ArrayMapNode做为_root节点返回,再继续存储值,会调用_root的update方法,也就是ArrayMapNode的update方法,若是存储的数量大于8,会建立ValueNode,并调用它的update方法,在这里会进行一个mergeIntoNode操做,即若是有相同索引到此处的节点,那么要进行一次合并操做,合并后会生成BitmapIndexedNode。
BitmapIndexedNode是通过压缩处理的层,最多存储16个长度的内容,bitmap属性就表示了这个层的存储状况。好比值为1935909891它的存储状况是"1110011011000111010010000000011",最多32位,不足的话,至关于高位补0。去除0后,1的数量就是这层实际存储的个数。为何说它通过压缩呢,由于这个bitmap表示了这层的存储状况,是长度为32的数组的每位的存储状况,但这层实际存储的数量最多只是它的一半,为了减小空间占用和查找效率,就不必记录不存储的位了,那就有疑问了,那直接往数组里存储就好了,要bitmap有什么用,咱们继续往下看,再新增数据,使BitmapIndexedNode这层的数据量超过16,此时就会进行一次转换expandNodes展开节点操做,将这层的结构转为HashArrayMapNode,这是一个长度为32的数组,此时,bitmap记录的位存储状况就是把以前的数据一个一个放到32位数组里对应的地方,没有值的地方就是undefined。
再日后若是HashArrayMapNode的存储数量下降小于16了,又会进行packNodes转为BitmapIndexedNode。
Map中,每次set都将对应的层的节点进行类型转换,updateNode这个方法会将受影响的节点生成新的结构返回,不影响其它层的节点,这就实现告终构共享。
这其中还有一种节点HashCollisionNode进行了hash冲突的处理。spa
在Map中为了找到数据存储位置,使用了不少的位操做,如今对一组map数据进行分析,看看它是如何进行计算的。这里用到的常量,
31 和 5 在TrieUtils.js文件中:code
export const SHIFT = 5; // Resulted in best performance after ______? export const SIZE = 1 << SHIFT; export const MASK = SIZE - 1;
咱们生成500个长度的map数据结构,使它包含BitmapIndexedNode,HashArrayMapNode这几种结构:orm
好比咱们选取"KEY6787241"这个节点进行分析,它的hash是这样计算出来的:索引
咱们根据这个hash计算出它在第一层即_root下面的位置:图片
看到它确实放在了对应位置下的HashArrayMapNode中,那在HashArrayMapNode中12的位置是怎么算出来的呢,咱们执行这样的操做:
而后来看看这个bitmap 524352:
能够看出它只有两位保存了数据。为了支持不定长的宽度,位置的计算是从后往前算的,那么,存储数据的状况就是倒数第7位,至关于index为6和倒数第20位,至关于index为19,那么咱们再计算下hash:785947024和-137605744是否在对应位置:
每深刻一层,将位右移动5位,而且与上31来算出对应位置。
那为啥是 31 和 5 呢,在代码中能够看到:
就是上面对应的两个常量。
785947024的二进制是"101110110110001001100110010000",31的二进制是"000000000000000000000000011111",&,位与操做后意思就是留下最后的5位,"10000" 16。再位移5位并位与31后就是,"01100" 12。2 ^ 5 = 32,因此5位二进制就能保存32个数,也就能知足咱们每一层最多32个的状况。
那32是怎么来的呢,这里统计了位分区的查找更新效率状况:
能够看到64位查询速度最快,8位更新速度最快。immutable-js选择32是由于实际使用中,查询的频率要比更新多,因此选择了查询速度较优,更新速度不是最差的32。