Angular NodeInjector实现中有不少二进制运算,比如它的布隆过滤器,InjectorIndex值的存储。二进制运算简单,计算效率高,值得掌握。下面我们一起看下NodeInjector实现中的二进制运算。
如果你对NodeInjector还不太熟悉,为了不影响这篇文章,请先阅读下Angular DI - 了解Ivy NodeInjector
据我所知,NodeInjector中多处使用位运算的地方,他们是
- 存储特定的位到布隆过滤器
- 检查特定的位是否设置到布隆过滤器
- InjectorIndex值的存储与获取
存储特定的位到布隆过滤器中
Angular使用或运算把由NG_ELEMENT_ID求得的特定位存入布隆过滤器。
let nextNgElementId = 0;
id = (type as any)[NG_ELEMENT_ID] = nextNgElementId++;
const BLOOM_MASK = 255;
const bloomHash = id & BLOOM_MASK;
const mask = 1 << bloomHash;
const BLOOM_BUCKET_BITS = 5;
(tView.data as number[])[bloomHash >> BLOOM_BUCKET_BITS] |= mask;
如代码所示,生成一个NG_ELEMENT_ID整数值(JS的一个整数由32个位表示),与BLOOM_MASK(值为255)做与运算。255的二进制表示是000...011111111(除了最后8位是1,其他24都是0),当与NG_ELEMENT_ID 32个位按位与时,结果只会保留NG_ELEMENT_ID最后8位的值,等于说bloomHash最大值是255。可以看到,与运算能够用来提取特定位,比如这里提取NG_ELEMENT_ID最后8位。
然后把1向右移动bloomHash表示的数值位,即使bloomHash值可能大于32,但一个整数值只有32位,因此mask只能是一个2^0和2^31之间的数字。我们去console验证一下,从移动32位开始,结果开始与移动0位,1位等重复。
我们看下,最后一条语句中包含的位操作
-
bloomHash >> BLOOM_BUCKET_BITS(BLOOM_BUCKET_BITS的值等于5)
右移位运算(>>)能够用来做除法运算。每右移一位等于把值除以2并向下取整,比如5 >> 1, 等于Math.floor(5/2)。这里bloomHash >> 5 就等于Math.floor(bloomHash / 2^5),等于Math.floor(bloomHash / 32)。32是一个JS整数的位数,也是tView.data整数数组中一个值的长度,bloomHash/32向下取整就得到数组的索引index,即存放的位置。
-
(tView.data as number[])[index]|= mask
或运算可以用来设置保存特定位。像前面说的,mask是一个2^0和2^31之间的数字。换句话说,它是一个从1到32位中有且只有一个位等于1。由上一步求得的索引位置,我们可以得到tView.data数组中的一个整数,它与mask做或运算,结果是与mask等于1的位置相对应的位将等于1。也就是说,特定的位被保存了下来。
我们结合可视化tView.data,举个例子更好地理解这些位操作。
例子假设id等于254,最后mask的32位整数与tView.data的第7个整数做或运算,把mask的第30位值1设置到tView.data的第7个整数上。
检查特定的位是否设置到布隆过滤器
前面把由NG_ELEMENT_ID求得的特定位存入了布隆过滤器,现在我们来看下Angular是如何检查布隆过滤器设置了某个特定位。
const mask = 1 << bloomHash;
const value = tView.data[bloomHash >> BLOOM_BUCKET_BITS];
return !!(value & mask);
很简单,Angular使用与存入时相同的bloomHash和算法,再求一遍mask值,以及在tView.data上的目标整数值。然后把他们做一次与运算,检查结果是否为真。结果为真,表示特定位被设置了,否则,没有被设置。由此可见,与运算还能用来检查特定位是否被设置。
比如,我们延用上面的例子,在存入id值254后,当前tView.data数组的第7个整数状态是0100...001,和与存入相同的mask:0100...000,他们做与运算得到的结果是0100...000。显而易见,结果为真。
InjectorIndex值的存储与获取
如果你阅读了Angular DI - 了解Ivy NodeInjector,应该已经知道InjectorIndex是什么。简单来说,它是一个索引值,用来在tView.data上找到布隆过滤器开始的位置,因为tView.data是一个数组。
InjectorIndex的存储
首先,我们看下Angular是如何存储InjectorIndex的
tView.data[parentLocationIndex] = injectorIndex | (declarationViewOffset << 16)
为了抓住重点,这里简化了代码。可以看到,Angular把InjectorIndex和declarationViewOffset两个值一块作为一个整数值,保存到了tView.data数组的某个位置(这里由parentLocationIndex索引指定)。declarationViewOffset占据整数的前16位,InjectorIndex占据后16位,加起来正好是一个整数的长度32位,很巧妙。这里我们又一次看到了或运算用来设置保存特定位的使用场景。
下面,我们通过可视化这个过程,看下InjectorIndex是如何保存到tView.data的
图上比较清晰,declarationViewOffset向右移16位,使得低16位转移到了高16位,再与InjectorIndex做或运算,原本的低16位保存到了tView.data[parentLocationIndex]指向的整数的高16位上,而InjectorIndex的低16位在tView.data[parentLocationIndex]上仍然作为低16位来保存。
InjectorIndex的获取
我想你已经知道怎么来获取InjectorIndex的值了,没错,就是使用与运算来拿到tView.data[parentLocationIndex]上低16位值。
const injectorIndex = tView.data[parentLocationIndex] & 0b1111111111111111;
把tView.data[parentLocationIndex] 与二进制16位数0b1111111111111111(0b在JS中表示二进制数)做与运算就可以提取出tView.data[parentLocationIndex]的低16位值。如下图所示:
总结
我们一起仔细看了三处Angular NodeInjector实现中的位运算,运用到的位运算有与运算(&)、或运算(|)、位右移运算(>>)、位左移运算(<<)。他们能被用来做的事情有:
- 与运算(&)
- 用来提取特定位
- 检查特定位是否被设置
- 或运算(|)
- 用来设置保存特定位
- 位右移运算(>>)
- 做除法运算且向下取整
- 位左移运算(<<)
- 把整数的低16位转入高16位