写点什么

react 源码中的 fiber 架构

作者:flyzz177
  • 2022-11-18
    浙江
  • 本文字数:11578 字

    阅读完需:约 38 分钟

先看一下FiberNode在源码中的样子

FiberNode

// packages/react-reconciler/src/ReactFiber.old.jsfunction FiberNode(  tag: WorkTag,  pendingProps: mixed,  key: null | string,  mode: TypeOfMode,) {  // Instance  this.tag = tag;  this.key = key;  this.elementType = null;  this.type = null;  this.stateNode = null;
// Fiber this.return = null; this.child = null; this.sibling = null; this.index = 0;
this.ref = null;
this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null;
this.mode = mode;
// Effects this.flags = NoFlags; this.nextEffect = null;
this.firstEffect = null; this.lastEffect = null;
this.lanes = NoLanes; this.childLanes = NoLanes;
this.alternate = null;
if (enableProfilerTimer) { // Note: The following is done to avoid a v8 performance cliff. // // Initializing the fields below to smis and later updating them with // double values will cause Fibers to end up having separate shapes. // This behavior/bug has something to do with Object.preventExtension(). // Fortunately this only impacts DEV builds. // Unfortunately it makes React unusably slow for some applications. // To work around this, initialize the fields below with doubles. // // Learn more about this here: // https://github.com/facebook/react/issues/14365 // https://bugs.chromium.org/p/v8/issues/detail?id=8538 this.actualDuration = Number.NaN; this.actualStartTime = Number.NaN; this.selfBaseDuration = Number.NaN; this.treeBaseDuration = Number.NaN;
// It's okay to replace the initial doubles with smis after initialization. // This won't trigger the performance cliff mentioned above, // and it simplifies other profiler code (including DevTools). this.actualDuration = 0; this.actualStartTime = -1; this.selfBaseDuration = 0; this.treeBaseDuration = 0; }
if (__DEV__) { // This isn't directly used but is handy for debugging internals: ... }}
复制代码


  • 我们看FiberNode这个构造函数里面只是赋值,我们再找一下链路上的Fiber,我们发现在函数createFiber的返回值类型里面出现了Fiber类型,所以


// packages/react-reconciler/src/ReactInternalTypes.jsexport type Fiber = {|  // These first fields are conceptually members of an Instance. This used to  // be split into a separate type and intersected with the other Fiber fields,  // but until Flow fixes its intersection bugs, we've merged them into a  // single type.
// An Instance is shared between all versions of a component. We can easily // break this out into a separate object to avoid copying so much to the // alternate versions of the tree. We put this on a single object for now to // minimize the number of objects created during the initial render.
// dom节点的相关信息 tag: WorkTag,// 组件的类型 key: null | string, // 唯一值 elementType: any,// 元素类型
// 判定fiber节点的类型,用于diff type: any,
// 真实 dom 节点 stateNode: any,
// Conceptual aliases // parent : Instance -> return The parent happens to be the same as the // return fiber since we've merged the fiber and instance. // Remaining fields belong to Fiber
// fiber 链表树 return: Fiber | null, // 父 fiber child: Fiber | null, // 第一个子 fiber sibling: Fiber | null, // 下一个兄弟 fiber index: number, // 在父 fiber 下面的子 fiber 中的下标
// The ref last used to attach this node. // I'll avoid adding an owner field for prod and model that as functions. ref: | null | (((handle: mixed) => void) & {_stringRef: ?string, ...}) | RefObject,
// 计算 state 和 props 渲染 pendingProps: any, // 本次渲染需要使用的 props memoizedProps: any, // 上次渲染使用的 props updateQueue: mixed, // 用于状态更新、回调函数、DOM更新的队列 memoizedState: any, // 上次渲染后的 state 状态 dependencies: Dependencies | null, // contexts、events 等依赖
// Bitfield that describes properties about the fiber and its subtree. E.g. // the ConcurrentMode flag indicates whether the subtree should be async-by- // default. When a fiber is created, it inherits the mode of its // parent. Additional flags can be set at creation time, but after that the // value should remain unchanged throughout the fiber's lifetime, particularly // before its child fibers are created. mode: TypeOfMode,
// Effect flags: Flags, // 记录更新时当前 fiber 的副作用(删除、更新、替换等)状态 subtreeFlags: Flags, // 当前子树的副作用状态 deletions: Array<Fiber> | null, // 要删除的子 fiber nextEffect: Fiber | null, // 下一个有副作用的 fiber firstEffect: Fiber | null, // 指向第一个有副作用的 fiber lastEffect: Fiber | null, // 指向最后一个有副作用的 fiber
// 渲染优先级 lanes: Lanes, childLanes: Lanes,
// This is a pooled version of a Fiber. Every fiber that gets updated will // eventually have a pair. There are cases when we can clean up pairs to save // memory if we need to. alternate: Fiber | null,// 指向 workInProgress fiber 树中对应的节点
// Time spent rendering this Fiber and its descendants for the current update. // This tells us how well the tree makes use of sCU for memoization. // It is reset to 0 each time we render and only updated when we don't bailout. // This field is only set when the enableProfilerTimer flag is enabled. actualDuration?: number,
// If the Fiber is currently active in the "render" phase, // This marks the time at which the work began. // This field is only set when the enableProfilerTimer flag is enabled. actualStartTime?: number,
// Duration of the most recent render time for this Fiber. // This value is not updated when we bailout for memoization purposes. // This field is only set when the enableProfilerTimer flag is enabled. selfBaseDuration?: number,
// Sum of base times for all descendants of this Fiber. // This value bubbles up during the "complete" phase. // This field is only set when the enableProfilerTimer flag is enabled. treeBaseDuration?: number,
// Conceptual aliases // workInProgress : Fiber -> alternate The alternate used for reuse happens // to be the same as work in progress. // __DEV__ only _debugID?: number, _debugSource?: Source | null, _debugOwner?: Fiber | null, _debugIsCurrentlyTiming?: boolean, _debugNeedsRemount?: boolean,
// Used to verify that the order of hooks does not change between renders. _debugHookTypes?: Array<HookType> | null,|};
复制代码


  • 整个 fiber 架构看起来可以分为 dom 信息、副作用、优先级、链表树等几个模块,那我们依次来拆分一下

dom 信息节点

tag: WorkTag
我们看到这个`tag`为`WorkTag`类型,用来区分`React`组件的类型
复制代码


// packages/react-reconciler/src/ReactWorkTags.jsexport type WorkTag =  | 0  | 1  | 2  | 3  | 4  | 5  | 6  | 7  | 8  | 9  | 10  | 11  | 12  | 13  | 14  | 15  | 16  | 17  | 18  | 19  | 20  | 21  | 22  | 23  | 24;
export const FunctionComponent = 0;export const ClassComponent = 1;export const IndeterminateComponent = 2; // Before we know whether it is function or classexport const HostRoot = 3; // Root of a host tree. Could be nested inside another node.export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.export const HostComponent = 5;export const HostText = 6;export const Fragment = 7;export const Mode = 8;export const ContextConsumer = 9;export const ContextProvider = 10;export const ForwardRef = 11;export const Profiler = 12;export const SuspenseComponent = 13;export const MemoComponent = 14;export const SimpleMemoComponent = 15;export const LazyComponent = 16;export const IncompleteClassComponent = 17;export const DehydratedFragment = 18;export const SuspenseListComponent = 19;export const FundamentalComponent = 20;export const ScopeComponent = 21;export const Block = 22;export const OffscreenComponent = 23;export const LegacyHiddenComponent = 24;
复制代码


相关参考视频讲解:进入学习


上述代码,区分了组件的类型,在后期协调阶段beginWorkcompleteWork的流程里根据不同的类型组件去做不同的fiber节点的处理

key: null | string、type: any
key为唯一值,type为与fiber关联的节点类型,都用于beginWork流程里面的reconcileChildren流程
复制代码
elementType
元素类型
复制代码
stateNode
- stateNode 用于记录当前 fiber 所对应的真实 dom 节点或者当前虚拟组件的实例。- 便于实现Ref- 便于追踪Rdom
复制代码

fiber 链表树

fiber链表树里面有四个字段returnchildsiblingindex


  • return:指向父节点,没有父节点则为 null。

  • child:指向下一个子节点,没有下一个子节点则为 null。

  • sibling:指向兄弟节点,没有下一个兄弟节点则为 null。

  • index:父 fiber 下面的子 fiber 下标通过这些字段那么我们可以形成一个闭环链表,举个栗子。


<div className='box'>  <h1 className='title' style={{'color':'red'}}>React源码解析</h1>  <ul>    <li>第一章</li>    <li>第二章</li>    <li>第三章</li>    <li>第四章</li>  </ul></div>
复制代码


根据上面的代码所对应的 fiber 链表树结构就是:


副作用相关

所谓副作用就是一套流程中我们不期望发生的情况。举个通俗的例子就是我们生活中去学游泳,在学会游泳的过程中呛了几口水,这个呛了几口水相对于成功学会游泳来说就是副作用,回归到react代码中,我们通过某些手段去修改propsstate等数据,数据修改完毕之后,但是同时引起了dom不必要的变化,那么这个变化就是副作用,当然这个副作用是必然存在的,就像游泳一样,必然会呛几口水,哈哈。

flags: Flags
记录当前节点通过`reconcileChildren`之后的的副作用,如插入,删除等
复制代码


例如Placement,表示插入,也叫新增。Deletion表示删除,Update表示更新


export type Flags = number;
// Don't change these two values. They're used by React Dev Tools.export const NoFlags = /* */ 0b000000000000000000;export const PerformedWork = /* */ 0b000000000000000001;
// You can change the rest (and add more).export const Placement = /* */ 0b000000000000000010;export const Update = /* */ 0b000000000000000100;export const PlacementAndUpdate = /* */ 0b000000000000000110;export const Deletion = /* */ 0b000000000000001000;export const ContentReset = /* */ 0b000000000000010000;export const Callback = /* */ 0b000000000000100000;export const DidCapture = /* */ 0b000000000001000000;export const Ref = /* */ 0b000000000010000000;export const Snapshot = /* */ 0b000000000100000000;export const Passive = /* */ 0b000000001000000000;// TODO (effects) Remove this bit once the new reconciler is synced to the old.export const PassiveUnmountPendingDev = /* */ 0b000010000000000000;export const Hydrating = /* */ 0b000000010000000000;export const HydratingAndUpdate = /* */ 0b000000010000000100;
// Passive & Update & Callback & Ref & Snapshotexport const LifecycleEffectMask = /* */ 0b000000001110100100;
// Union of all host effectsexport const HostEffectMask = /* */ 0b000000011111111111;
// These are not really side effects, but we still reuse this field.export const Incomplete = /* */ 0b000000100000000000;export const ShouldCapture = /* */ 0b000001000000000000;export const ForceUpdateForLegacySuspense = /* */ 0b000100000000000000;
// Static tags describe aspects of a fiber that are not specific to a render,// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).// This enables us to defer more work in the unmount case,// since we can defer traversing the tree during layout to look for Passive effects,// and instead rely on the static flag as a signal that there may be cleanup work.export const PassiveStatic = /* */ 0b001000000000000000;
// Union of side effect groupings as pertains to subtreeFlagsexport const BeforeMutationMask = /* */ 0b000000001100001010;export const MutationMask = /* */ 0b000000010010011110;export const LayoutMask = /* */ 0b000000000010100100;export const PassiveMask = /* */ 0b000000001000001000;
// Union of tags that don't get reset on clones.// This allows certain concepts to persist without recalculting them,// e.g. whether a subtree contains passive effects or portals.export const StaticMask = /* */ 0b001000000000000000;
// These flags allow us to traverse to fibers that have effects on mount// without traversing the entire tree after every commit for// double invokingexport const MountLayoutDev = /* */ 0b010000000000000000;export const MountPassiveDev = /* */ 0b100000000000000000;
复制代码


当然副作用不仅仅只是一个,所以React中在render阶段中采用的是深度遍历的策略去找出当前 fiber 树中所有的副作用,并维护一个副作用链表EffectList,与链表相关的字段还有firstEffectnextEffectlastEffect 我们来画一张图来简略示意一下。



解读一下就是,fristEffect指向第一个有副作用的fiber节点,lastEffect指向最后一个具有副作用的fiber节点,中间都是用nextEffect链接,这样组成了一个单向链表。render 阶段里面这一段处理就完了,在后面的commit阶段里面,React会根据EffectList里面fiber节点的副作用,会对应的处理相应的DOM,然后生成无副作用的虚拟节点,进行真实dom的创建。

优先级相关

当然React作为一个庞大的框架,肯定有自己的一套关于渲染的优先级机制,不然全都是一股脑按部就班的走,那肯定不行哒。


那么优先级我们就要关注一下lanealternateReact中每个fiber任务都有自己的lane(执行优先级),这样在render阶段react才知道,应该优先把哪个fiber任务提交到commit阶段去执行。而alternate是在render阶段中用来做为指针的,什么意思?React在状态发生改变的时候,就会根据当前的页面结构,生成两棵fiber树,一棵老的称之为current Fiber,而另一棵将要生成新的页面的树叫做workInProgress Fiber,而alternate作为指针,就是把current Fiber中的每一个节点指向workInProgress Fiber中的每一个节点。同样的他也会从workInProgress Fiber中指向 current Fiber



我们了解到了alternate,那就来说一说这个lane吧。


// packages/react-reconciler/src/ReactFiberLane.jsexport const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputDiscreteHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;const InputDiscreteLanes: Lanes = /* */ 0b0000000000000000000000000011000;
const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;const InputContinuousLanes: Lanes = /* */ 0b0000000000000000000000011000000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000100000000;export const DefaultLanes: Lanes = /* */ 0b0000000000000000000111000000000;
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000;const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000;
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
export const SomeRetryLane: Lanes = /* */ 0b0000010000000000000000000000000;
export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;
const NonIdleLanes = /* */ 0b0000111111111111111111111111111;
export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;const IdleLanes: Lanes = /* */ 0b0110000000000000000000000000000;
export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
复制代码


可见这个 lane 也是用 31 位二进制表示的唯一值,来进行优先级的判定的,并且位数越低,则优先级越大。

Props && State 相关

pendingProps: any
本次渲染需要使用的 props 
复制代码
memoizedProps: any
上次渲染使用的 props 
复制代码
updateQueue: mixed
用于状态更新、回调函数、DOM更新的队列
复制代码
memoizedState: any
上次渲染后的 state 状态 
复制代码
dependencies: Dependencies | null
contexts、events 等依赖
复制代码

Fiber 树的创建与更新的流程

上面一部分讲了React Fiber的基本架构,从真实dom信息副作用优先级等方面看了一下,为后面的render阶段协调调度以及commit阶段打下基础,那么接下来我们去探讨一下new FiberNode之后得到的什么样的rootFiber。我们在第二章节里面提到了整个的创建过程 # React 源码解析系列(二) -- 初始化组件的创建更新流程,那么这里深入探讨一下createFiber,在这个函数里面new FiberNode,创建了rootFiber,他也就是整个React应用的的根fiber。并且在createFiberRoot里面new FiberRootNode,创建了fiberRoot,它便是指向真实dom的根节点。所以在 # React 源码解析系列(二) -- 初始化组件的创建更新流程中我强调了root.currentuninitializedFiber.stateNode这两个东西,也就是这里说的rootFiberstateNode字段指向了 fiberRoot,并且fiberRoot的current指向了rootFiber,具体的示例图如下:



所以这里就完成了fiber树根节点的创建了。


拿到了上面创建完成的rootFiberfiberRoot之后那么我们接下来就是去根据我们的组件jsx去创建详细的dom树了,举个例子:


<div className='box'>  <h1 className='title' style={{'color':'red'}}>React源码解析</h1>  <ul>    <li>第一章</li>    <li>第二章</li>    <li>第三章</li>    <li>第四章</li>  </ul></div>
复制代码


现有上面的jsx,那么我们创建dom树的形式是深度优先遍历,已beginworkcompletework表示一个节点的创建过程,流程如下:



上面的图说明了,在初始化的时候我们的dom树是怎么被创建出来的,那么在状态发生改变的时候,我们会根据当前新的jsx内容创建新的workInProgress fiber,我们以新的jsx为例:


<div className='box'><h1 className='title' style={{'color':'red'}}>React源码解析</h1>+  <h1 className='title' style={{'color':'red'}}>React源码解析系列</h1>   <ul>     <li>第一章</li>     <li>第二章</li>     <li>第三章</li>-    <li>第四章</li>   </ul>+  <p>总结</p></div>
复制代码


上面的jsx表示,更改了h1的内容,删除了第四章,增加了总结这几个操作,那么react根据当前新的jsx调用createWorkInProgress方法创建workInProgress fiber,那么我们先去看一下createWorkInProgress的源码实现。


export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {  let workInProgress = current.alternate;  if (workInProgress === null) { // null为初始化,否为update    // We use a double buffering pooling technique because we know that we'll    // only ever need at most two versions of a tree. We pool the "other" unused    // node that we're free to reuse. This is lazily created to avoid allocating    // extra objects for things that are never updated. It also allow us to    // reclaim the extra memory if needed.    workInProgress = createFiber(      current.tag,      pendingProps,      current.key,      current.mode,    );    workInProgress.elementType = current.elementType;    workInProgress.type = current.type;    workInProgress.stateNode = current.stateNode;
if (__DEV__) { // DEV-only fields workInProgress._debugID = current._debugID; workInProgress._debugSource = current._debugSource; workInProgress._debugOwner = current._debugOwner; workInProgress._debugHookTypes = current._debugHookTypes; }
// current 指向 workInProgress workInProgress.alternate = current; // workInProgress 指向 current current.alternate = workInProgress; } else { // 上一次的props workInProgress.pendingProps = pendingProps; // Needed because Blocks store data on type. workInProgress.type = current.type;
// We already have an alternate. // Reset the effect tag.
//清除flags workInProgress.flags = NoFlags;
// The effect list is no longer valid. workInProgress.nextEffect = null; workInProgress.firstEffect = null; workInProgress.lastEffect = null;
if (enableProfilerTimer) { // We intentionally reset, rather than copy, actualDuration & actualStartTime. // This prevents time from endlessly accumulating in new commits. // This has the downside of resetting values for different priority renders, // But works for yielding (the common case) and should support resuming. workInProgress.actualDuration = 0; workInProgress.actualStartTime = -1; } }
// 绑定挂载的子fiber节点优先级、状态、props workInProgress.childLanes = current.childLanes; workInProgress.lanes = current.lanes;
workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue;
// Clone the dependencies object. This is mutated during the render phase, so // it cannot be shared with the current fiber. const currentDependencies = current.dependencies; workInProgress.dependencies = currentDependencies === null ? null : { lanes: currentDependencies.lanes, firstContext: currentDependencies.firstContext, };
// These will be overridden during the parent's reconciliation workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref;
if (enableProfilerTimer) { workInProgress.selfBaseDuration = current.selfBaseDuration; workInProgress.treeBaseDuration = current.treeBaseDuration; }
if (__DEV__) { workInProgress._debugNeedsRemount = current._debugNeedsRemount; switch (workInProgress.tag) { case IndeterminateComponent: case FunctionComponent: case SimpleMemoComponent: workInProgress.type = resolveFunctionForHotReloading(current.type); break; case ClassComponent: workInProgress.type = resolveClassForHotReloading(current.type); break; case ForwardRef: workInProgress.type = resolveForwardRefForHotReloading(current.type); break; default: break; } }
return workInProgress;}
复制代码


并且为其标记副作用,具体如下:



而前面所说的 alternate 在这里相互指向,其实也就是在reconciler阶段起到了复用节点的作用,因为我们所说的current fiber或者是workInProgress fiber都是视图的产物,是可以在"新"与"老"之间转换的。

为什么会出现 Fiber 架构呢?

相信在座的各位写 React 的同学出去面试,面试官总会问:”请问你知道React Fiber架构吗?请你说说Fiber架构吧“


为什么会出现?通过上面的React Fiber架构的讲解,我们可以get到几个点,那就是fiber针对每一个fiber节点都会有一套自己的独立的beginworkcompletework,并且能够在每一个具有副作用的节点上进行打标处理,而不是直接变更。而且生成的current fiberworkIProgress fiber可以相互转换,这里间接地可以称之为缓存吧。对比与以前的React应用来讲,以前的React应用是根据执行生命周期diffdom的更新一套流程同步走的,一套流程下来,不能中断,而且每一次的更新都是从根节点出发向下遍历的,我们可以设想一下处理庞大的结构的时候,那将是不可想象的性能开销,处理长时间任务耗时更长,更重要的是用户的交互,事件得不到及时响应,用户体验非常的差。


但是fiber这种结构,我们说的是一种时间分片的概念,通过时间分片把长任务,分成一个个独立的小单元去执行,返回。这样子就不会让js线程React应用独占,能有有空余去处理其他优先级较高的任务,任务得到了相应并且执行,当然了这种情况下页面就不会显得卡顿了。


所以总结来说就是React Fiber给我们提供了一种协调调度暂停中止调优的方式去更好的处理React应用浏览器的工作,保证了页面的性能与流畅度


总结

这一章讲述了整个的 fiber 架构与 fiber 树的创建与更新,那么这里从 React 应用的初始化挂载到 React 更新就形成了一部分的闭环完结,之后我们便是沿着流程走到了updateContainer更新这里


用户头像

flyzz177

关注

还未添加个人签名 2021-12-07 加入

还未添加个人简介

评论

发布
暂无评论
react源码中的fiber架构_React_flyzz177_InfoQ写作社区