初见
最近在阅读和实现React源码中,遇到了不少疑问,其中就有这么一个有趣的知识点。
相信大家也看到了这样一个文件,ReactFiberFlags源码
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分支上改了
源码具体位置,内部共享层源码
为什么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源码中的位运算技巧