Skip to content

初见

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

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

js
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分支上改了

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

image-20240528113513949

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

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

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

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

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

js
// 源码位置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的文件中,也会有对应的操作的代码

js
// 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位二进制变量即可代表多个不同的任务,再比如

ts
// 这个是自实现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源码过程,我们接下来拆开单独看

js
// 比如我们定义几个状态
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中大量使用的状态管理,优先级计算等等,比如前端代码中的权限管理,也可以参照去实现

权限管理

js
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源码中的位运算技巧