在Vue3的源码中,对于PatchFlag定义的代码如下:
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,
HOISTED = -1,
BAIL = -2,
}
它是使用二进制格式记录的,并且每个标志仅有一位为1
,这样可以通过位运算获知一个复合状态里包含哪些状态。
看到这段代码的时候,我还是颇受震撼的,因为位运算虽然大学课里面就学到过了,但是在平时工作中确实是完全没有使用过,也没想过能怎么用。
那今天我们就一起来学习一下在实际开发中有什么场景可以使用位运算吧。
位运算基础
按位与(&)
概念: 将两个操作数的每个对应位进行与运算,结果中每个位都是两个操作数对应位上都为1时才为1,否则为0。
console.log(5 & 3); // 输出: 1 (二进制: 101 & 011 = 001)
按位或(|)
概念: 将两个操作数的每个对应位进行或运算,结果中每个位都是两个操作数对应位上至少有一个为1时才为1,否则为0。
console.log(5 | 3); // 输出: 7 (二进制: 101 | 011 = 111)
按位异或(^)
概念: 将两个操作数的每个对应位进行异或运算,结果中每个位都是两个操作数对应位上不相同时才为1,相同时为0。
console.log(5 ^ 3); // 输出: 6 (二进制: 101 ^ 011 = 110)
按位非 (~)
概念: 将操作数的每一位取反。
console.log(~5); // 输出: -6 (二进制: ~000...0101 = 111...1010)
左移(<<)
概念: 将第一个操作数的二进制表示向左移动指定的数量,并在右侧填充零。
console.log(5 << 1); // 输出: 10 (二进制: 101 << 1 = 1010)
右移(<<)
概念: 将第一个操作数的二进制表示向右移动指定的数量,并在左侧填充符号位(对于负数是1,对于正数是0)。
console.log(-5 >> 1); // 输出: -3 (二进制: ...11111011 >> 1 = ...11111101)
无符号右移 (>>>)
概念: 类似于右移运算符,但它总是使用0来填充左侧空缺的位置,即使操作数是负数。
console.log(-5 >>> 1); // 输出: 2147483646 (二进制: ...11111011 >>> 1 = 01011111111111111111111111111110)
回顾Vue3源码
回到文章开篇时的Vue3源码,PatchFlag通过左移定义了不同的类型。PatchFlag是在模板编译的时候会通过分析给虚拟DOM打上的标记,他用于在虚拟DOM更新时做优化。
那为什么它要用位运算去定义呢,我们继续看一下源码就知道了
根据上面这段代码,我们很容易发现一个特点,使用位运算符比较很方便。举个例子,我们想得到一个组合类型PatchFlags.TEXT
和 PatchFlags.STYLE
的组合类型,我们只需要这样:
const combinedFlag = PatchFlags.TEXT | PatchFlags.STYLE; // 0001 | 0100 = 0101
如果我们想校验,它是否存在某一个标记,我们只需要这样:
const hasText = combinedFlag & PatchFlags.TEXT; // 0101 & 0001 = 0001 (truthy)
const hasClass = combinedFlag & PatchFlags.CLASS; // 0101 & 0010 = 0000 (falsy)
业务应用场景
权限比较
这应该是目前最位运算应用得最多的应用场景。我们可以通过按位或运算符 |
来组合权限,按位与运算符 &
来检查权限,按位取反运算符 ~
和按位与运算符 &
来移除权限。
// 定义权限常量
const READ = 1; // 二进制 0001
const WRITE = 2; // 二进制 0010
const DELETE = 4; // 二进制 0100
const EXECUTE = 8; // 二进制 1000
// 检查用户是否有权限
function hasPermission(permissions, targetPermission) {
return (permissions & targetPermission) === targetPermission;
}
console.log(hasPermission(userPermissions, READ)); // 输出: true
console.log(hasPermission(userPermissions, DELETE)); // 输出: false
// 给用户添加删除权限
userPermissions |= DELETE; // 二进制 0111
console.log(hasPermission(userPermissions, DELETE)); // 输出: true
// 移除用户的写入权限
userPermissions &= ~WRITE; // 二进制 0101
console.log(hasPermission(userPermissions, WRITE)); // 输出: false
数据压缩与存储
我们可以通过位运算将多个布尔值或小整数压缩到一个整数中,从而节省存储空间。
举个例子,假如我们现在开发中有4个变量:
let isRed = true;
let isGreen = false;
let isBlue = true;
let isYellow = false;
如果像上面这样定义的话,会很繁琐,我们可以通过将每个布尔值映射到一个二进制位,然后使用按位或运算符 |
将它们组合成一个整数,从而实现数据的压缩。在需要使用这些布尔值时,再使用按位与运算符 &
从整数中提取出来。
// 将布尔值压缩到一个整数中
let colorFlags = (isRed ? 1 : 0) |
(isGreen ? 2 : 0) |
(isBlue ? 4 : 0) |
(isYellow ? 8 : 0);
// 从整数中解压缩布尔值
isRed = (colorFlags & 1) === 1;
isGreen = (colorFlags & 2) === 2;
isBlue = (colorFlags & 4) === 4;
isYellow = (colorFlags & 8) === 8;
console.log(isRed, isGreen, isBlue, isYellow); // 输出: true false true false
处理颜色值
我们可以通过位运算快速的得到各个颜色通道的颜色值 (rgb颜色值)。
// 定义一个 32 位的颜色值(RGBA)
let color = 0xFFAABBCC;
// 提取红色通道值
let red = (color >> 16) & 0xFF;
// 提取绿色通道值
let green = (color >> 8) & 0xFF;
// 提取蓝色通道值
let blue = color & 0xFF;
console.log(red, green, blue); // 输出: 255 170 188
找出数组中只出现一次的数字
这是Leetcode中一道题目,在开发中也会有这种场景,当遇到这种场景可不用在傻乎乎的遍历了,可以通过异或预算快速解决。
举个例子:
输入: [2,2,1] 输出: 1
这时候我们就可以利用异或的特点去解决:
- 交换律:a ^ b = b ^ a,即异或运算的顺序不影响结果。
- 结合律:(a ^ b) ^ c = a ^ (b ^ c),可以对多个数连续进行异或操作。
- 自反性:a ^ a = 0,相同的两个数进行异或运算结果为 0。
- 与 0 异或特性:a ^ 0 = a,任何数与 0 进行异或运算都等于其本身。
var singleNumber = function(nums) {
let result = 0;
for (let num of nums) {
result ^= num;
}
return result;
};
总结一下
当你需要定义多个布尔值且需要灵活组合判断的场景下,你就可以考虑一下位运算了。它可以通过按位或运算符 |
来组合,按位与运算符 &
来检查,按位取反运算符 ~
和按位与运算符 &
来移除。