聊聊React源码中的位运算技巧

开发 前端
在涉及状态、标记位、优先级操作的地方大量使用了位运算。本文会讲解其中比较有代表性的部分。学到之后,当遇到类似场景时露一手,你就是业务线最靓的仔。

[[428027]]

大家好,我卡颂。

这两年有不少朋友和我吐槽React源码,比如:

  • 调度器为什么用小顶堆这种数据结构,直接用数组不行?
  • 源码里各种单向链表、环状链表,直接用数组不行?
  • 源码里各种位运算,有必要么?

作为业务依赖的框架,为了提升一点点运行时性能,React从不吝惜将源码写的很复杂。

[[428028]]

在涉及状态、标记位、优先级操作的地方大量使用了位运算。

本文会讲解其中比较有代表性的部分。学到之后,当遇到类似场景时露一手,你就是业务线最靓的仔。

几个常用位运算

在JS中,位运算的操作数会先转换为Int32(32位有符号整型),执行完位运算会Int32对应浮点数。

在React中,主要用到3种位运算符 —— 按位与、按位或、按位非。

按位与(&)

对于两个二进制操作数的每个bit,如果都为1,则结果为1,否则为0。

举个例子,计算3 & 2,首先将操作数转化为Int32:

  1. // 3对应的 Int32 
  2. 0b000 0000 0000 0000 0000 0000 0000 0011  
  3. // 2对应的 Int32 
  4. 0b000 0000 0000 0000 0000 0000 0000 0010  

为了直观,我们排除前面的0,只保留最后8位(实际参与计算的应该是32位):

  1.   0000 0011 
  2. & 0000 0010 
  3. ----------- 
  4.   0000 0010 

所以3 & 2计算结果转化为浮点数后为2。

按位或(|)

对于两个二进制操作数的每个bit,如果都为0,则结果为0,否则为1。

计算10 | 3:

  1.   0000 1010 
  2. | 0000 0011 
  3. ----------- 
  4.   0000 1011 

计算结果转化为浮点数后为11。

按位非(~)

对一个二进制操作数的每个bit,逐位进行取反操作(0、1互换)

对于~3,将3转化为Int32后逐位取反:

  1. // 3对应的 Int32 
  2. 0b000 0000 0000 0000 0000 0000 0000 0011  
  3. // 逐位取反 
  4. 0b111 1111 1111 1111 1111 1111 1111 1100 

计算结果转化为浮点数后为-4。

如果你对这个结果有疑惑,可以去了解补码相关知识

让我们从易到难,看看位运算在React中的应用。

标记状态

React源码内部有多个上下文环境,在执行函数时经常需要判断当前处在哪个上下文环境中。

假设共有三种上下文情况:

  1. // A上下文 
  2. const A = 1; 
  3. // B上下文 
  4. const B = 2; 
  5. // 没有处在上下文 
  6. const NoContext = 0; 

当进入某个上下文时,可以使用按位或操作标记进入:

  1. // 当前所处上下文 
  2. let curContext = 0; 
  3.  
  4. // 进入A上下文 
  5. curContext |= A; 

我们用8位二进制举例(同样,实际应该是Int32,这里是为了简化),curContext与A执行按位或操作:

  1.   0000 0000  // curContext 
  2. | 0000 0001  // A 
  3. ----------- 
  4.   0000 0001 

此时可以结合按位与操作与NoContext来判断是否处在某一上下文中:

  1. // 是否处在A上下文中 true 
  2. (curContext & A) !== NoContext 
  3.  
  4. // 是否处在B上下文中 false 
  5. (curContext & B) !== NoContext 

离开某上下文后,结合按位与、按位非移除标记:

  1. // 从当前上下文中移除上下文A 
  2. curContext &= ~A; 
  3.  
  4. // 是否处在A上下文中 false 
  5. (curContext & A) !== NoContext 

curContext与~A执行按位与操作:

  1.   0000 0001  // curContext 
  2. & 1111 1110  // ~A 
  3. ----------- 
  4.   0000 0000 

即从curContext中移除A。

当业务中需要同时处理多个状态时,可以使用如上位运算技巧。

优先级计算

在React中,不同情况下调用this.setState触发的更新会拥有不同优先级。优先级之间的比较、挑选同样使用了位运算。

具体来说,React中用31个bit位保存「更新」(之所以是31而不是32是因为Int32的最高位是符号位,不保存具体的数)。

处在越低bit位的更新优先级越高(越需要优先处理)。

举个例子,假设当前应用存在2个更新:

  1. 0b000 0000 0000 0000 0000 0000 0001 0001 

其中第1位的更新优先级最高(需要同步处理),第5位为默认优先级。

React经常需要找出当前最高优先级的更新在哪一位(如上例子中在第一位),方法如下:

  1. function getHighestPriorityLane(lanes) { 
  2.   return lanes & -lanes; 

解释下,由于Int32采用「补码」表示,所以-lanes可以看作如下两步操作:

  1. lanes取反(~lanes)
  2. 加1

为了直观,用8位表示:

  1. lanes  0001 0001 
  2. ~lanes 1110 1110 // 第一步 
  3. +1     1110 1111 // 第二步 

则lanes & -lanes如下:

  1.   0001 0001 // lanes   
  2. & 1110 1111 // -lanes 
  3. ----------- 
  4.   0000 0001 

取到的就是第一位(已有更新中最高的优先级)。

总结

虽然业务中不常使用位操作,但在特定场景下位操作时很方便、高效的方式。

这波操作你爱了么?

 

责任编辑:姜华 来源: 魔术师卡颂
相关推荐

2023-04-26 07:46:22

React隐藏彩蛋

2020-10-27 07:51:12

Shell脚本日期

2024-01-26 08:06:43

2022-04-14 11:50:39

函数组件hook

2021-01-23 12:22:59

位运算编程语言开发

2022-05-18 16:06:15

位运算异或运算

2023-04-07 08:02:54

源码位逻辑运算符

2021-02-21 06:36:57

运算技巧按位

2022-03-31 17:54:29

ReactHooks前端

2020-10-21 08:38:47

React源码

2022-09-07 00:04:37

JavaScript运算符技巧

2014-08-14 10:12:45

SwiftNil Coalesc

2020-11-20 07:51:02

JavaSPI机制

2017-03-06 20:39:41

整洁代码Clean Code

2022-05-23 15:02:19

异或运算面试真题

2022-07-31 19:57:26

react项目VSCode

2021-06-08 06:13:16

React开发开发技术

2022-03-11 10:23:02

React性能优化

2022-03-27 09:06:04

React类型定义前端

2021-07-07 05:00:17

初始化源码
点赞
收藏

51CTO技术栈公众号