react源码中的fiber架构

2022-10-19 09:41:18 浏览数 (2)

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

FiberNode

代码语言:javascript复制
// packages/react-reconciler/src/ReactFiber.old.js
function 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类型,所以
代码语言:javascript复制
// packages/react-reconciler/src/ReactInternalTypes.js
export 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
代码语言:go复制
我们看到这个`tag`为`WorkTag`类型,用来区分`React`组件的类型
代码语言:javascript复制
// packages/react-reconciler/src/ReactWorkTags.js
export 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 class
export 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
代码语言:shell复制
key为唯一值,type为与fiber关联的节点类型,都用于beginWork流程里面的reconcileChildren流程
elementType
代码语言:txt复制
元素类型
stateNode
代码语言:shell复制
- stateNode 用于记录当前 fiber 所对应的真实 dom 节点或者当前虚拟组件的实例。
- 便于实现Ref
- 便于追踪Rdom

fiber 链表树

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

  • return:指向父节点,没有父节点则为null。
  • child:指向下一个子节点,没有下一个子节点则为null。
  • sibling:指向兄弟节点,没有下一个兄弟节点则为null。
  • index:父fiber下面的子fiber下标 通过这些字段那么我们可以形成一个闭环链表,举个栗子。
代码语言:javascript复制
<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
代码语言:go复制
记录当前节点通过`reconcileChildren`之后的的副作用,如插入,删除等

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

代码语言:javascript复制
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 & Snapshot
export const LifecycleEffectMask = /*          */ 0b000000001110100100;

// Union of all host effects
export 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 subtreeFlags
export 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 invoking
export 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吧。

代码语言:javascript复制
// packages/react-reconciler/src/ReactFiberLane.js
export 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
代码语言:txt复制
本次渲染需要使用的 props 
memoizedProps: any
代码语言:txt复制
上次渲染使用的 props 
updateQueue: mixed
代码语言:txt复制
用于状态更新、回调函数、DOM更新的队列
memoizedState: any
代码语言:perl复制
上次渲染后的 state 状态 
dependencies: Dependencies | null
代码语言:matlab复制
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树了,举个例子:

代码语言:javascript复制
<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为例:

代码语言:javascript复制
<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的源码实现。

代码语言:javascript复制
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更新这里

0 人点赞