React源码中有意思的位运算

初见

最近在阅读和实现 React 源码中,遇到了不少疑问,其中就有这么一个有趣的知识点。

相信大家也看到了这样一个文件,ReactFiberFlags 源码[1]

import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';

export type Flags = number;

// Don't change these two values. They're used by React Dev Tools.
export const NoFlags = /*                      */ 0b00000000000000000000000000;
export const PerformedWork = /*                */ 0b00000000000000000000000001;

// You can change the rest (and add more).
export const Placement = /*                    */ 0b00000000000000000000000010;
export const Update = /*                       */ 0b00000000000000000000000100;
export const Deletion = /*                     */ 0b00000000000000000000001000;
export const ChildDeletion = /*                */ 0b00000000000000000000010000;
export const ContentReset = /*                 */ 0b00000000000000000000100000;
export const Callback = /*                     */ 0b00000000000000000001000000;
export const DidCapture = /*                   */ 0b00000000000000000010000000;
export const ForceClientRender = /*            */ 0b00000000000000000100000000;
export const Ref = /*                          */ 0b00000000000000001000000000;
export const Snapshot = /*                     */ 0b00000000000000010000000000;
export const Passive = /*                      */ 0b00000000000000100000000000;
export const Hydrating = /*                    */ 0b00000000000001000000000000;
export const Visibility = /*                   */ 0b00000000000010000000000000;
export const StoreConsistency = /*             */ 0b00000000000100000000000000;

有没有跟我一样的强迫症发现,这一条斜线的 1,看着好有感觉 🐶

有意思的小话题

那就是 React 源码中的注释,甚至某些变量名,语气还是蛮有气势的,比如还有个非常有名的变量 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED

说人话就是 共享层,不要用,否则你会被炒鱿鱼。

我就想说 不用这个变量就不会被炒鱿鱼吗【我真的没用过啊】

在最新的 React 源码中这个变量已经换了一个名 __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE

看了下已经发布的 Tags 上都还没变,仅仅是 Main 分支上改了

源码具体位置,内部共享层源码[2]

image-20240528113513949

为什么 Fiber 的标记要以二进制来存储呢?

既然要了解这么做的目的,比较好的方式,我们看下源码引用,哪边用到了它。

其实注释里面就说了其中一个引用方,不要改这些值,React-Dev-Tools 会用到

除了开发者工具,我们也可以从 React-Reconciler 也就是协调器中去找找这些变量的身影,比如我们抽取两个地方来看看

我们接下来的代码就以 React 源码的 18.3.1 的 Tag 来看

// 源码位置https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberBeginWork.old.js

    const child = mountChildFibers(
        workInProgress,
        null,
        nextChildren,
        renderLanes,
      );
      workInProgress.child = child;

      let node = child;
      while (node) {
        // Mark each child as hydrating. This is a fast path to know whether this
        // tree is part of a hydrating tree. This is used to determine if a child
        // node has fully mounted yet, and for scheduling event replaying.
        // Conceptually this is similar to Placement in that a new subtree is
        // inserted into the React tree here. It just happens to not need DOM
        // mutations because it already exists.
        node.flags = (node.flags & ~Placement) | Hydrating;
        node = node.sibling;
      }

那么在 beginwork 对应的 completework 的文件中,也会有对应的操作的代码

// https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberCompleteWork.old.js

function markUpdate(workInProgress: Fiber) {
  // Tag the fiber with an update effect. This turns a Placement into
  // a PlacementAndUpdate.
  workInProgress.flags |= Update;
}

给当前的 workInProgress 节点的 flags 打上更新标记,本身这段代码的含义表示的是,在递归的归阶段,如果 workInProgress 的 tag 是 HostText 也就是我们使用的文本节点时,如果对比前后的文本内容不一样,那么就打上更新的标记。

当然 React 源码中类似的操作还很多,Lanes 使用单个 32 位二进制变量即可代表多个不同的任务,再比如

// 这个是自实现React版本,跟官方实现可能有些出入
const commitMutationEffectOnFiber = (finishedWork: FiberNode) => {
 const flags = finishedWork.flags;
 if ((flags & Placement) !== NoFlags) {
  commitPlacement(finishedWork);
  finishedWork.flags &= ~Placement;
 }
 // flags Update
 if ((flags & Update) !== NoFlags) {
  commitUpdate(finishedWork);
  finishedWork.flags &= ~Update;
 }

 // flag ChildDeletion
 if ((flags & ChildDeletion) !== NoFlags) {
  const deletions = finishedWork.deletions;
  deletions?.forEach((childToDelete) => {
   commitDeletion(childToDelete);
  });
  finishedWork.flags &= ~ChildDeletion;
 }
};

((flags & Placement) !== NoFlags ,(flags & Update) !== NoFlags 都是先位与,然后跟 NoFlags 进行比较,如果返回值未 true,那么就表示 flags 包含对应的 Placement 或者 Update 标记。

可能在源码中去解释,尤其是有些同学并不是特别了解 React 源码过程,我们接下来拆开单独看

// 比如我们定义几个状态
const NoEffect = 0b000000000000;
const PerformedWork = 0b000000000001;
const Placement = 0b000000000010;
const Update = 0b000000000100;
const PlacementAndUpdate = Placement | Update;

let effectTag = NoEffect;

effectTag |= Placement; // 2
effectTag |= Update; // 6

// 通过按位与(&)操作,React可以快速检查某个节点是否需要执行特定的副作用
if ((effectTag & Update) !== NoEffect) {
  // 是否包含Update,返回是true
}

if ((effectTag & PlacementAndUpdate) !== NoEffect) {
  // 是否包含Placement和Update,返回是true
}

 

是不是很神奇,一个字段,几个标记就能够实现状态切换,甚至是多个状态的表达形式,如果是以前,我们可能是用一个字段表示状态,常见的然后定义了 N 种字面量的状态比如,00A,00B,00C,00D 等等,缺点就是无法直接运算,还占用更多的空间,代码语义上的表达还不够清晰,对不对。

React 源码中的位操作在性能优化、内存使用和代码简化方面发挥了重要作用。通过深入理解这些位操作,我们可以更好地理解 React 的内部机制,并应用这些技巧优化我们自己的应用程序。在日常开发中,适当使用位操作可以带来显著的性能提升和代码简洁性,值得每个开发者学习和掌握。

其他场景

主要的场景比如 React 中大量使用的状态管理,优先级计算等等,比如前端代码中的权限管理,也可以参照去实现

权限管理

const READ = 0b0001;
const WRITE = 0b0010;
const EXECUTE = 0b0100;

let userPermissions = READ | WRITE; // 0b0001 | 0b0010 = 0b0011

function hasPermission(permissions, permission) {
  return (permissions & permission) === permission;
}

console.log(hasPermission(userPermissions, READ)); // true
console.log(hasPermission(userPermissions, EXECUTE)); // false

 

总结

本文因为篇幅原因,对于位运算的定义和基本操作没有进行介绍,主要还是大佬们已经科普的比较多了,而且大学计算机课程中也有涉及,这里贴个卡颂大佬之前写过关于这个知识点的文章,欢迎去看看,React 源码中的位运算技巧[3]

参考资料

[1] ReactFiberFlags 源码: github.com/facebook/re…

[2] 内部共享层源码: github.com/facebook/re…

[3] React 源码中的位运算技巧: juejin.cn/post/701762…

文章转自:https://juejin.cn/post/7373573927212384291

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值