2024-08-27 10:10:05 +08:00

381 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# React 中的位运算
> 面试题React 中哪些地方用到了位运算?
## 位运算的基础知识
所谓二进制,指的就是以二为底的一种计数方式。
| 十进制 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| -------- | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: |
| 二进制 | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 |
| 八进制 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 十六进制 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
我们经常会使用二进制来进行计算,基于二进制的位运算能够很方便的表达“增、删、查、改”。
例如一个后台管理系统,一般的话会有针对权限的控制,一般权限的控制就使用的是二进制:
```js
# 各个权限
permissions = {
"SYS_SETTING" : {
"value" : 0b10000000,
"info" : "系统重要设置权限"
},
"DATA_ADMIN" : {
"value" : 0b01000000,
"info" : "数据库管理权限"
},
"USER_MANG" : {
"value" : 0b00100000,
"info" : "用户管理权限"
},
"POST_EDIT" : {
"value" : 0b00010000,
"info" : "文章编辑操作权限"
},
"POST_VIEW" : {
"value" : 0b00001000,
"info" : "文章查看权限"
}
}
```
再例如,在 linux 操作系统里面x 代表可执行权限w代表可写权限r 代表可读权限对应的权限值分别就是1、2、42 的幂次方)
使用二进制来表示权限,首先速度上面会更快一些,其次在表示多种权限的时候,会更加方便一些。
比如,现在有 3 个权限 A、B、C...
根据不同的权限做不同的事情:
```js
if(value === A){
// ...
} else if(value === B){
// ...
}
```
在上面的代码中,会有一个问题,目前仅仅只是一对一的关系,但是在实际开发中,往往有很多一对多的关系,一个 value 可能会对应好几个值。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-01-03-055329.png" alt="image-20230103135329257" style="zoom: 33%;" />
复习一下和二进制相关的运算:
- 与( & ):只要有一位数为 0那么最终结果就是 0也就是说必须两位都是 1最终结果才是 1
- 或( | : 只要有一位数是 1那么最终结果就是 1也就是说必须两个都是 0最终才是 0
- ~ : 对一个二进制数逐位取反,也就是说 0、1 互换
- 异或( ^ : 如果两个二进制位不相同,那么结果就为 1相同就为 0
```js
1 & 1 = 1
0000 0001
0000 0001
---------
0000 0001
1 & 0 = 0
0000 0001
0000 0000
---------
0000 0000
1 | 0 = 1
0000 0001
0000 0000
---------
0000 0001
1 ^ 0 = 1
0000 0001
0000 0000
---------
0000 0001
~3
0000 0011
// 逐位取反
1111 1100
// 计算结果最终为 -4涉及到补码的知识
```
接下来我们来看一下位运算在权限系统里面的实际运用:
| 下载 | 打印 | 查看 | 审核 | 详细 | 删除 | 编辑 | 创建 |
| :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
如果是 0代表没有权限如果是 1代表有权限
0000 0001 代表只有创建的权限0010 0011 代表有查看、编辑以及创建的权限
**添加权限**
直接使用或运算即可。
0000 0011 目前有创建和编辑的权限,我们要给他添加一个查看的权限 0010 0000
```js
0000 0011
0010 0000
---------
0010 0011
```
**删除权限**
可以使用异或
0010 0011 目前有查看、编辑和创建,取消编辑的权限 0000 0010
```js
0010 0011
0000 0010
---------
0010 0001
```
**判断是否有某一个权限**
可以使用与来进行判断
0011 1100查看、审核、详细、删除判断是否有查看0010 0000权限、再判断是否有创建0000 0001权限
```js
0011 1100
0010 0000
---------
0010 0000
// 判断是否有“查看”权限,做与操作时得到了“查看”权限值本身,说明有这个权限
```
```js
0011 1100
0000 0001
---------
0000 0000
// 最终得到的值为 0说明没有此权限
```
通过上面的例子,我们会发现使用位运算确确实实非常的方便,接下来我们就来看一下 React 中针对位运算的使用。
## React 中的位运算
- fiber 的 flags
- lane 模型
- 上下文
**fiber 的 flags**
在 React 中,用来标记 fiber 操作的 flags使用的就是二进制
```js
export const NoFlags = /* */ 0b000000000000000000000000000;
export const PerformedWork = /* */ 0b000000000000000000000000001;
export const Placement = /* */ 0b000000000000000000000000010;
export const DidCapture = /* */ 0b000000000000000000010000000;
export const Hydrating = /* */ 0b000000000000001000000000000;
// ...
```
这些 flags 就是用来标记 fiber 状态的。
之所以要专门抽离 fiber 的状态,是因为这种操作是非常高效的。针对一个 fiber 的操作,可能有增加、删除、修改,但是我不直接进行操作,而是给这个 fiber 打上一个 flag接下来在后面的流程中针对有 flag 的 fiber 统一进行操作。
通过位运算,就可以很好的解决一个 fiber 有多个 flag 标记的问题,方便合并多个状态
```js
// 初始化一些 flags
const NoFlags = 0b00000000000000000000000000;
const PerformedWork =0b00000000000000000000000001;
const Placement = 0b00000000000000000000000010;
const Update = 0b00000000000000000000000100;
// 一开始将 flag 变量初始化为没有 flag也就是 NoFlags
let flag = NoFlags
// 这里就是在合并多个状态
flag = flag | PerformedWork | Update
// 要判断是否有某一个 flag直接通过 & 来进行判断即可
//判断是否有 PerformedWork 种类的更新
if(flag & PerformedWork){
//执行
console.log('执行 PerformedWork')
}
//判断是否有 Update 种类的更新
if(flag & Update){
//执行
console.log('执行 Update')
}
if(flag & Placement){
//不执行
console.log('执行 Placement')
}
```
**lane 模型**
lane 模型也是一套优先级机制,相比 Schedulerlane 模型能够对任务进行更细粒度的控制。
```js
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000000100;
// ...
```
例如在 React 源码中,有一段如下的代码:
```js
// lanes 一套 lane 的组合
function getHighestPriorityLanes(lanes) {
// 从 lanes 这一套组合中,分离出优先级最高的 lane
switch (getHighestPriorityLane(lanes)) {
case SyncLane:
return SyncLane;
case InputContinuousHydrationLane:
return InputContinuousHydrationLane;
case InputContinuousLane:
return InputContinuousLane;
// ...
return lanes;
}
}
// lane 在表示优先级的时候,大致是这样的:
// 0000 0001
// 0000 0010
// 0010 0000
// lanes 表示一套 lane 的组合,比如上面的三个 lane 组合到一起就变成了一个 lanes 0010 0011
// getHighestPriorityLane 这个方法要做的事情就是分离出优先级最高的
// 0010 0011 ----> getHighestPriorityLane -----> 0000 0001
export function getHighestPriorityLane(lanes) {
return lanes & -lanes;
}
```
假设现在我们针对两个 lane 进行合并
```js
const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000000100;
```
合并出来就是一个 lanes合并出来的结果如下
```js
0b0000000000000000000000000000001
0b0000000000000000000000000000100
---------------------------------
0b0000000000000000000000000000101
```
0b0000000000000000000000000000101 是我们的 lanes接下来取负值
```js
-lanes = 0b1111111111111111111111111111011
```
最后一步,再和本身的 lanes 做一个 & 操作:
```js
0b0000000000000000000000000000101
0b1111111111111111111111111111011
---------------------------------
0b0000000000000000000000000000001
```
经过 & 操作之后,就把优先级最高的 lane 给分离出来了。
**上下文**
在 React 源码内部,有多个上下文:
```js
// 未处于 React 上下文
export const NoContext = /* */ 0b000;
// 处于 batchedUpdates 上下文
const BatchedContext = /* */ 0b001;
// 处于 render 阶段
export const RenderContext = /* */ 0b010;
// 处于 commit 阶段
export const CommitContext = /* */ 0b100;
```
当执行流程到了 render 阶段,那么接下来就会切换上下文,切换到 RenderContext
```js
let executionContext = NoContext; // 一开始初始化为没有上下文
executionContext |= RenderContext;
```
在执行方法的时候,就会有一个判断,判断当前处于哪一个上下文
```js
// 是否处于 RenderContext 上下文中,结果为 true
(executionContext & RenderContext) !== NoContext
// 是否处于 CommitContext 上下文中,结果为 false
(executionContext & CommitContext) !== NoContext
```
如果要离开某一个上下文
```js
// 从当前上下文中移除 RenderContext 上下文
executionContext &= ~RenderContext;
// 是否处于 RenderContext 上下文中,结果为 false
(executionContext & CommitContext) !== NoContext
```
## 真题解答
> 题目React 中哪些地方用到了位运算?
>
> 参考答案:
>
> 位运算可以很方便的表达“增、删、改、查”。在 React 内部,像 flags、状态、优先级等操作都大量使用到了位运算。
>
> 细分下来主要有如下的三个地方:
>
> - fiber 的 flags
> - lane 模型
> - 上下文