你真的了解位运算吗?
最近在研究react v18的源码,看到一些位运算很好的写法,让我对位运算产生了兴趣,所以写篇文章记录一下
阅读源码总是能发现很多很好的写法,很有意思,了解位运算当然要了解二进制
文中引用的react源码是github仓库最新的代码 main分支 ,引用的vue3源码是github仓库最新的代码 main分支
二进制基础知识补充
概念:
二进制(Binary)是一种基于2的计数系统,它只使用两个数字符号:0 和1。与我们日常使用的十进制(Decimal,基于10)不同,二进制的每一位代表的是2的幂次方。
二进制属于进位计数制,和八进制、十进制一样
进位计数制:
- 如果采用的数制有R个基本符号,则称为R数制,R成为数制的基数
- 进位计数制的编号符号逢R进位的规则
- 数制中每一固定的位置对应的单位值成为权
- 各位的权是以R为底的幂
- 一个数可按权展开成为多项式
js
十进制 13.75
按权展开式就是
1 * 10的1次方 + 3 * 10的0次方 + 7 * 10的-1次方 + 5 * 10的-2次方
二进制 1101.11
按权展开就是
1 * 2的3次方 + 1 * 2的2次方 + 0 * 2的1次方 + 1 * 2的0次方 + 1 * 2的-1次方 + 1 * 2的-2次方
为什么计算机世界不使用我们熟悉的十进制来存储数据呢?
因为二进制有以下优势:
-
易于物理实现
- 电子器件的双稳态特性:计算机的基本电子元件(如晶体管)天然具有两种稳定状态:(开/关,高电压/低电压)
两种状态可以完美对应二进制的0和1,实现简单可靠
- 如果使用十进制,就需要电子元件区分10种不同状态,这会导致电路设计复杂度指数级上升,抗干扰能力大幅下降,制造成本增加,可靠性降低等等
-
运算规则简单 (R(R + 1) / 2)
二进制与布尔代数天然对应:1对应true, 0对应false
所有复杂运算都可以通过三种基本逻辑运算实现:与、或、非
-
机器可靠性高
在第二点中提到,复杂进制会导致可靠性降低
-
通用性强(逻辑运算)适用于所有类型的数据
简单的使用:一些常见的逻辑运算
js
// 逻辑与 两个操作数都为1时结果为1,否则为0
0b1100 & 0b1010 = 0b1000
// 分解:
// 1100
// & 1010
// ----
// 1000
// 逻辑或 两个操作数中至少一个为1时结果为1,否则为0
0b1100 | 0b1010 = 0b1110
// 分解:
// 1100
// | 1010
// ----
// 1110
// 逻辑非 对操作数取反
~0b1100 = 0b0011 // 实际结果取决于位数,这里假设4位
// 注意:在JavaScript中会得到32位取反结果
// 逻辑与非 先与在进行取反
~(a & b)
// 逻辑或非 先或再进行取反
~(a | b)
// 逻辑同或 两个操作数相同时结果为1,不同时为0
~(a ^ b)
// 逻辑异或 两个操作数不同时结果为1,相同时为0
0b1100 ^ 0b1010 = 0b0110
// 分解:
// 1100
// ^ 1010
// ----
// 0110
计算机不是只能处理二进制
现代计算机底层确实是二进制,但通过抽象层(编程语言)可以让我们使用十进制等其他进制工作
量子计算机不限于二进制
传统计算机基于二进制,量子计算机使用量子位,可以同时表示0和1的叠加态
二进制不是唯一可能的计算机基础
历史上存在过三进制计算机(例如苏联的Setun),但是二进制最终因其简单可靠以及其他优点能够脱颖而出
位运算
概念
位运算是对整数在内存中的二进制位进行操作的一组运算符
主要包括:
-
按位与(&):两位都为1时结果为1
jsconst a = 5; // 二进制: 0101 const b = 3; // 二进制: 0011 console.log(a & b); // 输出: 1 (二进制: 0001) // 过程 0101 (5) & 0011 (3) ---- 0001 (1)
-
按位或(|):两位有一个为1时结果为1
jsconst a = 5; // 0101 const b = 3; // 0011 console.log(a | b); // 输出: 7 (二进制: 0111) // 过程 0101 (5) | 0011 (3) ---- 0111 (7)
-
按位异或(^):两位不同时结果为1
jsconst a = 5; // 0101 const b = 3; // 0011 console.log(a ^ b); // 输出: 6 (二进制: 0110) // 过程 0101 (5) ^ 0011 (3) ---- 0110 (6)
-
按位非(~):对每一位取反
反转操作数的所有二进制位 , 在JavaScript中,数字使用32位表示,所以结果是32位有符号整数的补码形式
jsconst a = 5; // 二进制: 0000 0101 console.log(~a); // 输出: -6 (二进制: 1111 1010) // 过程 ~ 0000 0101 (5) -------- 1111 1010 (-6)
-
左移(<<):将二进制位向左移动,右侧补0
jsconst a = 5; // 二进制: 0101 console.log(a << 1); // 输出: 10 (二进制: 1010) console.log(a << 2); // 输出: 20 (二进制: 10100) // 过程 0101 (5) << 1 = 1010 (10) 0101 (5) << 2 = 10100 (20)
-
右移(>>):将二进制位向右移动,左侧补符号位
jsconst a = 5; // 二进制: 0101 const b = -5; // 二进制: 1111 1011 (补码表示) console.log(a >> 1); // 输出: 2 (二进制: 0010) console.log(b >> 1); // 输出: -3 (二进制: 1111 1101) // 过程 0101 (5) >> 1 = 0010 (2) 1111 1011 (-5) >> 1 = 1111 1101 (-3)
-
无符号右移(>>>):将二进制位向右移动,左侧补0
jsconst a = 5; // 二进制: 0101 const b = -5; // 二进制: 1111 1011 (补码表示) console.log(a >>> 1); // 输出: 2 (二进制: 0010) console.log(b >>> 1); // 输出: 2147483645 // 过程 0101 (5) >>> 1 = 0010 (2) 1111 1011 (-5) >>> 1 = 0111 1101 (2147483645)
程序中有哪些地方用到了位运算或者逻辑运算
一些简单的位运算的场景
权限管理
位运算结合逻辑运算
js
const PERM = {
READ: 1 << 0, // 1
WRITE: 1 << 1, // 2
DELETE: 1 << 2 // 4
};
let userPerm = PERM.READ | PERM.WRITE; // 赋予读写权限
// 检查是否有写权限
if (userPerm & PERM.WRITE) {
console.log("有写权限");
}
下面有五个权限 0代表没有此权限 1代表有此权限
查看 | 详情 | 删除 | 编辑 | 创建 |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
js
// 添加权限 使用或运算即可
00011 // 有编辑和创建的权限
10000 // 添加查看的权限
-----
10011
// 删除权限 异或
00011 // 有编辑和创建的权限
00010 // 取消编辑的权限
-----
00001
// 判断是否有某一个权限 与
// 得到的值与要查看权限的值相等 就说明有这个权限
00011 // 有编辑和创建的权限
00010 // 查看是否有编辑权限
-----
00010 // 有
判断奇偶
js
function isEven(n) {
return (n & 1) === 0;
}
console.log(isEven(4)); // true
console.log(isEven(5)); // false
交换两个变量的值
js
let a = 5, b = 7;
a ^= b;
b ^= a;
a ^= b;
console.log(a, b); // 7, 5
前端中也有一些包含进制转换的方法
js
// 十进制转其他进制
const num = 255;
console.log(num.toString(2)); // "11111111" (二进制)
console.log(num.toString(8)); // "377" (八进制)
console.log(num.toString(16)); // "ff" (十六进制)
// 其他进制转十进制
console.log(parseInt('11111111', 2)); // 255
console.log(parseInt('377', 8)); // 255
console.log(parseInt('ff', 16)); // 255
// ES6二进制和八进制字面量
const binary = 0b11111111; // 255
const octal = 0o377; // 255
const hex = 0xff; // 255
hex和rgba互换
js
/**
* 颜色转换工具
*/
const ColorConverter = {
/**
* HEX 转 RGBA
* @param {string} hex - HEX颜色值,支持3位或6位,可带#前缀
* @param {number} [alpha=1] - 透明度,0-1之间
* @returns {string} rgba颜色字符串
*/
hexToRgba: function(hex, alpha = 1) {
// 移除#号
hex = hex.replace('#', '');
// 处理3位缩写形式
if (hex.length === 3) {
hex = hex.split('').map(char => char + char).join('');
}
// 解析RGB值
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// 确保alpha在0-1之间
const a = Math.min(1, Math.max(0, alpha));
return `rgba(${r}, ${g}, ${b}, ${a})`;
},
/**
* RGBA 转 HEX
* @param {string} rgba - rgba颜色字符串
* @returns {string} HEX颜色值,带#前缀
*/
rgbaToHex: function(rgba) {
// 提取rgba中的数值
const rgbaValues = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i);
if (!rgbaValues) {
throw new Error('Invalid RGBA color format');
}
// 解析RGB值
const r = parseInt(rgbaValues[1]);
const g = parseInt(rgbaValues[2]);
const b = parseInt(rgbaValues[3]);
// 将RGB转换为HEX
const toHex = (value) => {
const hex = value.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
};
// 使用
console.log(ColorConverter.hexToRgba('#ff0000')); // rgba(255, 0, 0, 1)
console.log(ColorConverter.hexToRgba('#f00', 0.5)); // rgba(255, 0, 0, 0.5)
console.log(ColorConverter.rgbaToHex('rgba(255, 0, 0, 1)')); // #ff0000
console.log(ColorConverter.rgbaToHex('rgb(0, 255, 0)')); // #00ff00
React中的位运算
fiber的flags
在 React 中,用来标记 fiber 操作的 flags,使用的就是二进制:
js
// 源码文件路径 packages/react-reconciler/src/ReactFiberFlags.js
export const NoFlags = /* */ 0b0000000000000000000000000000000;
export const PerformedWork = /* */ 0b0000000000000000000000000000001;
export const Placement = /* */ 0b0000000000000000000000000000010;
export const DidCapture = /* */ 0b0000000000000000000000010000000;
export const Hydrating = /* */ 0b0000000000000000001000000000000;
// You can change the rest (and add more).
export const Update = /* */ 0b0000000000000000000000000000100;
export const Cloned = /* */ 0b0000000000000000000000000001000;
export const ChildDeletion = /* */ 0b0000000000000000000000000010000;
export const ContentReset = /* */ 0b0000000000000000000000000100000;
export const Callback = /* */ 0b0000000000000000000000001000000;
/* Used by DidCapture: 0b0000000000000000000000010000000; */
export const ForceClientRender = /* */ 0b0000000000000000000000100000000;
export const Ref = /* */ 0b0000000000000000000001000000000;
export const Snapshot = /* */ 0b0000000000000000000010000000000;
export const Passive = /* */ 0b0000000000000000000100000000000;
/* Used by Hydrating: 0b0000000000000000001000000000000; */
export const Visibility = /* */ 0b0000000000000000010000000000000;
export const StoreConsistency = /* */ 0b0000000000000000100000000000000;
这些flags是用来标记fiber状态的
之所以要专门抽离fiber状态,是因为这种操作是非常高效的。针对一个 fiber 的操作,可能有增加、删除、修改,但是我不直接进行操作,而是给这个 fiber 打上一个 flag,接下来在后面的流程中针对有 flag 的 fiber 统一进行操作。
js
// 源码文件路径 packages/react-reconciler/src/ReactFiber.js 170行
// 初始化的时候是没有状态的
// 这里将flags以及子树状态初始化为没有状态
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
// 创建wip tree node,用于fiber双缓冲
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate; // 存储旧的fiber对象 fiber双缓冲,新旧Fiber Tree的节点通过alternate相互指向
if (workInProgress === null) {
// 如果wip node是空,说明是初次渲染
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
} else {
// 反之,则说明已经有了alternate,重置flags
// ...
workInProgress.flags = NoFlags
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null; // 用户diff比较是的待删除数组
// ...
}
// ...
return workInProgress;
}
js
// 小例子:合并多个状态
flag = flag | PerformedWork | Update
// 要判断是否有某一个 flag,直接通过 & 来进行判断即可
// 判断是否有 PerformedWork 种类的更新
if(flag & PerformedWork){
console.log('执行 PerformedWork')
}
js
// 源码文件路径: packages/react-reconciler/src/ReactFiberFlags.js
// 我在文件里找到了一个变量 LifecycleEffectMask
// 用到了 按位或(|) 运算符在其中一个或两个操作数对应的二进制位为 1 时,该位的结果值为 1
// 那么LifecycleEffectMask变量值就是同时拥有了六种 flags 状态
export const LifecycleEffectMask = Passive | Update | Callback | Ref | Snapshot | StoreConsistency;
// 然后搜了一下这个用法,因为既然声明了联合的状态,那么肯定有用到判断的
// 源码路径:packages/react-reconciler/src/ReactFiberThrow.js 282行 在这个文件里面找到了用法,这个文件是用来抛出错误的
// We're going to commit this fiber even though it didn't complete.
// But we shouldn't call any lifecycle methods or callbacks. Remove
// all lifecycle effect tags.
// 大致意思:我们将要提交一个不完整的节点,但是我们不能调用任何生命周期方法或者回调函数,移除所有的生命周期副作用标签
// Incomplete的值是0b0000000000000001000000000000000 上方有提到
sourceFiber.flags &= ~(LifecycleEffectMask | Incomplete)
// 拆解
// 1. sourceFiber.flags = sourceFiber.flags & ~(LifecycleEffectMask | Incomplete)
// 2. ~(LifecycleEffectMask | Incomplete)
// 第二步可以拆解为 LifecycleEffectMask | Incomplete,这里就是在之前的状态上面又增加了一种状态,现在是七种状态 结果是 52804
// 对得到的结果进行按位非(~)操作,将运算数的位反转 我运算了一下得到的结果是 -52805
// 3. sourceFiber.flags & ~(LifecycleEffectMask | Incomplete) flags是number类型,可以是任意数
扩展:fiber是什么?
可以从三个方面来解释:架构、数据类型、动态的工作单元
架构:React从v16之后重构了之前的架构,新架构称之为fiber架构
数据类型:Fiber本质上也是一个对象,是在之前React元素基础上的一种升级版本。每个FiberNode对象里面会包含React元素的类型、周围连接的FiberNode以及DOM相关信息
动态的工作单元:在每个FiberNode中,保存了本次更新中该React元素变化的数据,还有就是要执行的工作以及副作用的信息
js
// 源码位置:packages/react-reconciler/src/ReactFiber.js 138行
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.refCleanup = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
// 副作用相关
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
// 与调度优先级有关
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
// ...
}
至于里面每个标签就不再进行详述了,要不然扩展的有点多,有点儿喧宾夺主的意思了,下一篇更新一下fiber双缓冲以及渲染流程,在里面再进行详述
lane模型
lane 模型是一套优先级机制,相比 Scheduler,lane 模型能够对任务进行更细粒度的控制。
js
// 源代码文件路径:packages/react-reconciler/src/ReactFiberLanes.js 39行
export const TotalLanes = 31;
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
export const SyncUpdateLanes: Lane =
SyncLane | InputContinuousLane | DefaultLane;
export const GestureLane: Lane = /* */ 0b0000000000000000000000001000000;
// ...
获取最高级别的lanes
js
function getHighestPriorityLanes(lanes) {
// 在lanes组合中,得到优先级最高的lane
switch (getHighestPriorityLane(lanes)) {
case SyncHydrationLane:
return SyncHydrationLane;
case SyncLane:
return SyncLane;
case InputContinuousHydrationLane:
return InputContinuousHydrationLane;
case InputContinuousLane:
return InputContinuousLane;
case DefaultHydrationLane:
return DefaultHydrationLane;
case DefaultLane:
return DefaultLane;
case GestureLane:
return GestureLane;
case TransitionHydrationLane:
return TransitionHydrationLane;
// ...
default:
return lanes;
}
}
// lane 在表示优先级的时候,大致是这样的:
// 0000 0001
// 0000 0010
// 0010 0000
// lanes表示一套lane的组合 上面三个lane组合到一起就变成了一个lanes 0010 0011
// getHighestPriorityLanes 这个方法要做的事情就是分离出优先级最高的
// 0010 0011 --> getHighestPriorityLanes 处理 --> 0000 0001
function getHighestPriorityLane(lanes) {
return lanes & -lanes;
}
// 假设要对以下两个lane进行合并
const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000000100;
// 合并结果就是 lanes --- 0b0000000000000000000000000000101
// 取负值后就是 -lanes --- 0b1111111111111111111111111111011
// & 操作后就是 0b0000000000000000000000000000001
// SyncLane 优先级高于 InputContinuousLane
上下文
js
// 源码文件路径:packages/react-reconciler/src/ReactFiberWorkLoop.js 373行
export const NoContext = /* */ 0b000;
const BatchedContext = /* */ 0b001;
export const RenderContext = /* */ 0b010;
export const CommitContext = /* */ 0b100;
// 初始化的时候没有上下文
let executionContext: ExecutionContext = NoContext;
// 到了渲染阶段的时候,改为RenderContext
executionContext |= RenderContext;
// 执行方法的时候,会先判断处于哪一个上下文中
// 函数位于 1807 行,函数大意是对于时间函数而言,是否无效的上下文
export function isInvalidExecutionContextForEventFunction(): boolean {
return (executionContext & RenderContext) !== NoContext; // 是否处于 RenderContext 上下文中
}
// 3279行代码 commitRoot 函数中
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
// 如果要离开某一个上下文
executionContext &= ~RenderContext; // 从当前上下文中移除 RenderContext 上下文
Vue中的位运算
ShapeFlags 来标记组件的类型和子节点类型:
js
// 源码文件路径:packages/shared/src/ShapeFlags.ts
export enum ShapeFlags {
ELEMENT = 1, // 0001
FUNCTIONAL_COMPONENT = 1 << 1, // 0010
STATEFUL_COMPONENT = 1 << 2, // 0100
TEXT_CHILDREN = 1 << 3, // 1000
ARRAY_CHILDREN = 1 << 4,
SLOTS_CHILDREN = 1 << 5,
TELEPORT = 1 << 6,
SUSPENSE = 1 << 7,
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT,
}
// 使用示例:packages/runtime-core/src/renderer.ts 430行
if (shapeFlag & ShapeFlags.ELEMENT) {
// 检查标志,处理元素节点
} else if(shapeFlag & ShapeFlags.COMPONENT) {
// 检查标志,处理组件节点
}
// ...
补丁标志 PatchFlags 用于优化虚拟 DOM 的 diff 算法:
是编译器生成的优化提示。
在进行差异比较(diff)过程中,当遇到带有动态子元素(dynamicChildren)的块时,算法会进入 "优化模式"。在这种模式下,我们知道虚拟 DOM(vdom)是由编译器生成的渲染函数产生的,所以算法只需要处理由这些补丁标志明确标记的更新。
补丁标志可以使用按位或运算符(|)进行组合,并使用按位与运算符(&)进行检查。
PatchFlags 确实是 Vue 3 相比 Vue 2 在性能上实现质的飞跃的核心机制之一。让diff过程从vue2的全量对比到vue3的靶向更新。
文件内有详细的英文注解,下面只介绍了两个状态的说明 CACHED、BAIL
js
// 源码文件路径:packages/shared/src/PatchFlags.ts
export enum PatchFlags {
TEXT = 1,
CLASS = 1 << 1,
STYLE = 1 << 2,
PROPS = 1 << 3,
FULL_PROPS = 1 << 4,
NEED_HYDRATION = 1 << 5,
STABLE_FRAGMENT = 1 << 6,
KEYED_FRAGMENT = 1 << 7,
UNKEYED_FRAGMENT = 1 << 8,
NEED_PATCH = 1 << 9,
DYNAMIC_SLOTS = 1 << 10,
DEV_ROOT_FRAGMENT = 1 << 11,
// 表示一个缓存的静态虚拟节点(vnode)。这也是在hydration过程中的一个提示,即跳过整个子树,因为静态内容永远无需更新。
CACHED = -1,
// 这是一种特殊标志,它指示差异对比算法应退出优化模式。例如,对于由 renderSlot() 创建的块片段 ,当遇到非编译器生成的插槽时(即手动编写的渲染函数,对 // 于这种情况应始终进行完整的差异对比),或者遇到手动克隆的虚拟节点(cloneVNodes)时,就需要使用该标志。
BAIL = -2,
}
// 使用示例:
// 检查是否是动态属性 处理动态 props
if (patchFlag & PatchFlags.PROPS) {}
还有一些标志信息,EffectFlags、SlotFlags等等
end!感谢大家能看完,写的不好的地方或者有什么要补充的可以评论出来,感谢!