React 中的二进制运算
4月 07, 2022 • ☕️☕️☕️ 30 min read
React 在添加了 lanes 模型以后,计算是否是同一批次更新和计算更新的优先级时涉及到了一些二进制的运算,为了能理解 React 是如何进行计算的,我们需要先对二进制运算有一定了解。
JS 中如何声明二进制
使用 0b 或者 0B 开头
const b1 = 0b0001 // 十进制 - 1
const b2 = 0b0101 // 十进制 - 5
与运算 &
与运算符用 & 表示,进行与运算的两位只要有一个 0,那与运算的结果就是 0,反之为 1。
// 每一位的与运算规则
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
// 根据上述可得
0b0101 & 0b0011 = 0b0001
或运算 |
或运算符用 | 表示,进行运算的两位只要有一个是 1,那运算的结果就是 1,反之为 0。
// 每一位的或运算规则
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
// 根据上述可得
0b0101 | 0b0011 = 0b0111
异或运算 ^
异或运算符用 ^ 表示,进行运算的两位只要相同,那运算的结果就是 0,反之为 1。
// 每一位的异或运算规则
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
// 根据上述可得
0b0101 ^ 0b0011 = 0b0110
左移运算符 <<
左移运算符用 << 表示,他的意思就是将一个运算对象的各二进制位全部左移若干位(左边的二进制丢弃,右边补 0)。
0b1011 << 2 = 0b101100
右移运算符 >>
右移运算符用 >> 表示,他的意思就是将一个运算对象的各二进制位全部右若干位(超出的位则抛弃)。
0b1011 >> 2 = 0b0010
按位非 ~
按位非运算符用 ~ 表示,在了解它之前首先要了解一下原码、反码、补码。
原码
简单讲,原码就是第一位是符号位的二进制,1 代表负数,0 代表正数,例如 10001 代表的就是 -1,00001 代表 1。这是最简单的二进制表示,但是使用原码在进行运算时需要先判断某个二进制是正数还是负数,然后再进行运算,否则会出现 1 + (-1) 转化为二进制是 00001 + 10001 = 10011 = -3 而不等于 0 的情况。
反码
那有没有一种方法可以在运算时忽略符号位,让符号位也参加运算的方式呢,这个就是反码。一个正数的反码还是他本身,一个负数的反码就是在原先基础上符号位保持不变,其余位数取反。
// 带符号位八位的二进制表示
十进制 原码 反码
8 0000 1000 0000 1000
-6 1000 0110 1111 1001
// 计算 8 - 6
0000 1000 // 反码
+ 1111 1001 // 反码
-----------------
0000 0001 // 反码
0000 0001 // 原码 = 1,比实际结果2要小1
补码
根据反码的例子可见,反码的运算结果也并非准确。而且反码还会存在+0 和-0 的情况,但实际上+0 和-0 所代表的的意义并无区别。为了解决反码带来的问题,人们又引入了补码的概念,其实参考生活中的场景,例如一个时钟,当前时间是 10 点,那么无论是减去 2 还是加上 10,最终指针指向的都是 8 这个位置,因为一个时钟所能代表的最大数为 12,所以当超过 12 时,超过 12 的部分就会被忽略,这也就是我们常说的取模。那转换到二进制这里,我们继续看 8 和 6 的运算。
// 不带符号位八位的二进制表示
十进制 原码
8 0000 1000
6 0000 0110
我们计算 8 - 6,根据上述时钟的思想,a - b 就等于 a 加上当前二进制所能代表的最多个数减去 b,也就是 a + (Max - b),八位二进制所能代表的最多个数是 256,所以 8 - 6 可以转换为 8 + (256 - 6) = 258,258 对应的二进制数为 100000010,移除最左侧超出的位数,00000010 结果正好等于 2,所以 256 - 6 这个数也就是 6 的补码,我们再观察 -6 和 256 - 6 = 250 的关系发现,-6 的绝对值加上 250 等于八位二进制所能代表的最大数 1111 1111 加上 1,所以负数的补码就是这个负数对应的反码加上 1。
// 带符号位八位的二进制表示
十进制 原码 补码
8 0000 1000 0000 1000
-6 1000 0110 1111 1010
// 计算 8 - 6
0000 1000 // 补码
+ 1111 1010 // 补码
-----------------
0000 0010 // 补码
0000 0010 // 原码 = 2,符合真实结果
在了解了原码、反码、补码以后,我们再来看按位非 ~ 的运算,按位非就是对二进制的每一位都取反,因为计算机所存储的数字都是以补码的形式存储,所以
const a = 5 // 补码 00000000000000000000000000000101
const b = -3 // 补码 11111111111111111111111111111101
console.log(~a) // 对 a 的每一位取反得新的补码 11111111111111111111111111111010
// 补码转反码(补码减1) 11111111111111111111111111111001
// 反码转原码(除符号位取反)00000000000000000000000000000110
// 十进制 -6
console.log(~b) // 00000000000000000000000000000010
// 十进制 2
在对二进制运算有了一定了解以后,我们来看一下 React 当中都运用到了哪些二进制运算
mergeLanes
合并两个 lane 到一个 lanes 当中,用来将多个更新优先级合并到一个批次
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b
}
intersectLanes
TODO
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a & b
}
getHighestPriorityLane
获取当前更新批次中优先级最高的优先级(lanes 中越靠右优先级越高),在上文我们已经提到过, 在计算机中的数字都是通过补码的形式存储的,所以 -lanes 就是 lanes 的反码再加 1,也就是 ~lanes + 1, 所以 lanes & (~lanes + 1)最右侧的 1 会一直进位到第一个 0,例如 lanes 为 00100, 则~lanes + 1 为 11100,00100 & 11100 = 00100,这样就将优先级最高的 lane 取出来了。
export function getHighestPriorityLane(lanes: Lanes): Lane {
return lanes & -lanes
}