先看一下FiberNode
在源码中的样子
FiberNode
// 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:
...
}
}
复制代码
// 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,
|};
复制代码
dom 信息节点
tag: WorkTag
我们看到这个`tag`为`WorkTag`类型,用来区分`React`组件的类型
复制代码
// 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;
复制代码
相关参考视频讲解:进入学习
上述代码,区分了组件的类型,在后期协调阶段beginWork
、completeWork
的流程里根据不同的类型组件去做不同的fiber
节点的处理
key: null | string、type: any
key为唯一值,type为与fiber关联的节点类型,都用于beginWork流程里面的reconcileChildren流程
复制代码
elementType
stateNode
- stateNode 用于记录当前 fiber 所对应的真实 dom 节点或者当前虚拟组件的实例。
- 便于实现Ref
- 便于追踪Rdom
复制代码
fiber 链表树
fiber
链表树里面有四个字段return
、child
、sibling
、index
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
代码中,我们通过某些手段去修改props
、state
等数据,数据修改完毕之后,但是同时引起了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 & 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
,与链表相关的字段还有firstEffect
、nextEffect
和 lastEffect
我们来画一张图来简略示意一下。
解读一下就是,fristEffect
指向第一个有副作用的fiber
节点,lastEffect
指向最后一个具有副作用的fiber
节点,中间都是用nextEffect
链接,这样组成了一个单向链表。render 阶段里面这一段处理就完了,在后面的commit
阶段里面,React
会根据EffectList
里面fiber
节点的副作用,会对应的处理相应的DOM
,然后生成无副作用的虚拟节点
,进行真实dom
的创建。
优先级相关
当然React
作为一个庞大的框架,肯定有自己的一套关于渲染的优先级机制,不然全都是一股脑按部就班
的走,那肯定不行哒。
那么优先级我们就要关注一下lane
与alternate
,React
中每个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.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
memoizedProps: any
updateQueue: mixed
memoizedState: any
dependencies: Dependencies | null
Fiber 树的创建与更新的流程
上面一部分讲了React Fiber
的基本架构,从真实dom信息
、副作用
、优先级
等方面看了一下,为后面的render阶段
的协调
与调度
以及commit阶段
打下基础,那么接下来我们去探讨一下new FiberNode
之后得到的什么样的rootFiber
。我们在第二章节里面提到了整个的创建过程 # React 源码解析系列(二) -- 初始化组件的创建更新流程,那么这里深入探讨一下createFiber
,在这个函数里面new FiberNode
,创建了rootFiber
,他也就是整个React
应用的的根fiber
。并且在createFiberRoot
里面new FiberRootNode
,创建了fiberRoot
,它便是指向真实dom的根节点
。所以在 # React 源码解析系列(二) -- 初始化组件的创建更新流程中我强调了root.current
、uninitializedFiber.stateNode
这两个东西,也就是这里说的rootFiber
的stateNode
字段指向了 fiberRoot
,并且fiberRoot的current
指向了rootFiber
,具体的示例图如下:
所以这里就完成了fiber
树根节点的创建了。
拿到了上面创建完成的rootFiber
和fiberRoot
之后那么我们接下来就是去根据我们的组件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
树的形式是深度优先遍历,已beginwork
和completework
表示一个节点的创建过程,流程如下:
上面的图说明了,在初始化的时候我们的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
节点都会有一套自己的独立的beginwork
和completework
,并且能够在每一个具有副作用
的节点上进行打标处理,而不是直接变更。而且生成的current fiber
与workIProgress fiber
可以相互转换,这里间接地可以称之为缓存吧。对比与以前的React
应用来讲,以前的React
应用是根据执行生命周期
、diff
、dom的更新
一套流程同步走的,一套流程下来,不能中断,而且每一次的更新都是从根节点出发向下遍历的,我们可以设想一下处理庞大的结构的时候,那将是不可想象的性能开销
,处理长时间任务耗时更长,更重要的是用户的交互,事件得不到及时响应,用户体验非常的差。
但是fiber
这种结构,我们说的是一种时间分片
的概念,通过时间分片把长任务,分成一个个独立的小单元
去执行,返回。这样子就不会让js线程
被React应用
独占,能有有空余去处理其他优先级较高
的任务,任务得到了相应并且执行,当然了这种情况下页面就不会显得卡顿了。
所以总结来说就是React Fiber
给我们提供了一种协调
,调度
,暂停
,中止
,调优
的方式去更好的处理React应用
与浏览器
的工作,保证了页面的性能与流畅度
总结
这一章讲述了整个的 fiber 架构与 fiber 树的创建与更新,那么这里从 React 应用的初始化挂载到 React 更新就形成了一部分的闭环完结,之后我们便是沿着流程走到了updateContainer
更新这里
评论