React源码阅读-fiber核心构建原理
React源码阅读(2)-fiber核心构建原理
好的,我明白了。您提供的文本主要介绍了 React 源码中 Fiber 核心的构建原理,涵盖了从执行上下文到构建、提交、调度等关键阶段,以及相关的代码实现。
您提出的关联问题也很重要,它们深入探讨了 Fiber 的优化、代码的扩展性和调度的细节理解。
这些都是理解 React Fiber 工作机制的关键点。如果您想进一步探讨这些问题,例如:
-
Fiber 在哪些方面进行了优化,以及具体的优化手段是什么?
-
手写 Fiber 实现可以如何扩展,用于不同的渲染目标或特性?
-
React 的调度(Scheduler)是如何工作的,其细节如何帮助提升用户体验?
欢迎随时提出,我很乐意和您一起探讨。
react-reconciler核心逻辑理解并手写
想了很久该怎么写,才能写出来让大家看懂,就我们先抛开复杂的车道优先级和调度优先级以及调度等级以及react大量的时间操作等到后面分析18源码的时候一起讲。就从最简单的开始手写fiber-dom。
1.react构建fiber核心代码
对应源码位于ReactFiberWorkLoop.js,我们先大致了解一下整体的走向图。
1.1 入口
在全局变量中有executionContext
, 代表渲染期间
的执行上下文
, 它也是一个二进制表示的变量, 通过位运算进行操作. 在源码中一共定义了 8 种执行栈:
type ExecutionContext = number;
export const NoContext = /* */ 0b0000000;
const BatchedContext = /* */ 0b0000001;
const EventContext = /* */ 0b0000010;
const DiscreteEventContext = /* */ 0b0000100;
const LegacyUnbatchedContext = /* */ 0b0001000;
const RenderContext = /* */ 0b0010000;
const CommitContext = /* */ 0b0100000;
这里我们只分析Legacy
模式(对启动模式不清楚的可以看看[开篇] Legacy
模式下的首次更新, 会直接进行构建,然后经过提交到页面上,并不会进入去注册调度任务 接下来从输入[阅读须知]开始我们看核心的输入源码
export function scheduleUpdateOnFiber(
fiber: Fiber,lane: Lane,eventTime: number,
) {if (lane === SyncLane) {// legacy模式if (// 首次无上下文也无渲染上下文(executionContext & LegacyUnbatchedContext) !== NoContext &&(executionContext & (RenderContext | CommitContext)) === NoContext) {// 初次更新performSyncWorkOnRoot(root);} else {// 后续的更新,注册调度任务ensureRootIsScheduled(root, eventTime);}}
}
在 render 过程中, 每一个阶段都会改变executionContext
(render 之前, 会设置executionContext |= RenderContext
; commit 之前, 会设置executionContext |= CommitContext
, 假设在render
过程中再次发起更新(如在UNSAFE_componentWillReceiveProps
生命周期中调用setState
)则可通过executionContext
来判断当前的render
状态。
上面我们已经看到了入口的函数scheduleUpdateOnFiber
,接下来我们分析入口函数中的2个关键的函数performSyncWorkOnRoot
和ensureRootIsScheduled
。
1.2 performSyncWorkOnRoot
首先我们来分析performSyncWorkOnRoot
,里面的主要做的事情就是做fiber树
的构建以及最后的提交commitRoot
。在这个函数中我们也可以分为2个阶段,一个阶段是fiber树
的构建,另一个阶段是commitRoot
的提交(输出)。
阶段1.fiber树的构建
function performSyncWorkOnRoot(root) {let lanes;let exitStatus;// workInProgressRoot当前构建的任务if (root === workInProgressRoot &&includesSomeLane(root.expiredLanes, workInProgressRootRenderLanes)) {// 初次构建的时候从root节点开始, 形成一个完成的fiberdom树lanes = workInProgressRootRenderLanes;exitStatus = renderRootSync(root, lanes);} else {// 1. 获取本次render的优先级, 初次构造返回 NoLaneslanes = getNextLanes(root, NoLanes);// 2. 从root节点开始, 至上而下更新,形成一个完成的fiberdom树exitStatus = renderRootSync(root, lanes);}// 将最新的fiber树挂载到root.finishedWork节点上const finishedWork: Fiber = (root.current.alternate: any);root.finishedWork = finishedWork;root.finishedLanes = lanes;// 进入commit阶段,渲染到页面上commitRoot(root);
}
实际我们看到不管是初次构建还是后续更新都会走到`renderRootSync
`上去构建fiber树,那它又做了什么那
function renderRootSync(root: FiberRoot, lanes: Lanes) {const prevExecutionContext = executionContext;executionContext |= RenderContext;// 如果fiberRoot变动, 或者update.lane变动,update(链表我们后面讲)if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {// 刷新栈帧prepareFreshStack(root, lanes);}do {try {// 核心代码循环构建fiberworkLoopSync();break;} catch (thrownValue) {handleError(root, thrownValue);}} while (true);executionContext = prevExecutionContext;// 重置全局变量, 表明render结束workInProgressRoot = null;workInProgressRootRenderLanes = NoLanes;return workInProgressRootExitStatus;
}
如果是初读源码我们可以只关心一个地方那就是workLoopSync
,当然这是在legacy
模式下,concurrent
模式下走的是workLoopConcurrent
去实现时间分片和循环可中断。下一步我们就可以看workLoopSync
function workLoopSync() {while (workInProgress !== null) {performUnitOfWork(workInProgress)}
}
这里就相当于把循环构建fiberDom树了,而前面备注中也写到了workInProgress指向当前构建的任务或者说节点更适合一些,来到构建fiberDom树的performUnitOfWork
.
function performUnitOfWork(unitOfWork: Fiber): void {// 指向当前页面的`fiber`节点. 初次构造时, 页面还未渲染, 就是空的const current = unitOfWork.alternate;let next;if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {startProfilerTimer(unitOfWork);next = beginWork(current, unitOfWork, subtreeRenderLanes);stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);} else {//构建出最终的fiberDom,并且返回dom结构的chilrden处理之后的childnext = beginWork(current, unitOfWork, subtreeRenderLanes);}unitOfWork.memoizedProps = unitOfWork.pendingProps;if (next === null) {// 没新节点的时候就可以准备回溯洛completeUnitOfWork(unitOfWork);} else {workInProgress = next;}ReactCurrentOwner.current = null;
}
进入beginWork
这里主要做的事情是通过ReactElement去完成fiber树的更新和构建,他的细节是在在于针对每一个fiber类型分别有不同的update
函数去做处理,值得一提的是这里基本完成了fiber本身基础状态的一些设置,我觉得我们这个阅读阶段我们先关注主流程的事情,也就是updateXXX
的处理。
function beginWork(
current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,
): Fiber | null {// 忽略代码// 这里是对于所有fiber类型的不同处理,我们关心不同tag怎么处理的就行了,等下手写的时候写简单一点switch (workInProgress.tag) {case IndeterminateComponent: {return mountIndeterminateComponent(current,workInProgress,workInProgress.type,renderLanes,);}case LazyComponent: {const elementType = workInProgress.elementType;return mountLazyComponent(current,workInProgress,elementType,updateLanes,renderLanes,);}case FunctionComponent: {const Component = workInProgress.type;const unresolvedProps = workInProgress.pendingProps;const resolvedProps =workInProgress.elementType === Component? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);return updateFunctionComponent(current,workInProgress,Component,resolvedProps,renderLanes,);}case ClassComponent: {const Component = workInProgress.type;const unresolvedProps = workInProgress.pendingProps;const resolvedProps =workInProgress.elementType === Component? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);return updateClassComponent(current,workInProgress,Component,resolvedProps,renderLanes,);}case HostRoot:return updateHostRoot(current, workInProgress, renderLanes);case HostComponent:return updateHostComponent(current, workInProgress, renderLanes);case HostText:return updateHostText(current, workInProgress);//忽略代码}
}
举个例子updateHostRoot
,这里我就可以看到一些具体的fiber构建的细节了。
function updateHostRoot(current, workInProgress, renderLanes) {// 1. 状态计算, 更新整合到 workInProgress.memoizedState中来const updateQueue = workInProgress.updateQueue;const nextProps = workInProgress.pendingProps;const prevState = workInProgress.memoizedState;const prevChildren = prevState !== null ? prevState.element : null;cloneUpdateQueue(current, workInProgress);// 遍历updateQueue.shared.pending, 提取有足够优先级的update对象, 计算出最终的状态 workInProgress.memoizedStateprocessUpdateQueue(workInProgress, nextProps, null, renderLanes);const nextState = workInProgress.memoizedState;// 2. 获取下级`ReactElement`对象const nextChildren = nextState.element;const root: FiberRoot = workInProgress.stateNode;if (root.hydrate && enterHydrationState(workInProgress)) {// ...服务端渲染相关, 此处省略} else {// 3. 根据`ReactElement`对象, 调用`reconcileChildren`生成`Fiber`子节点(只生成`次级子节点`)reconcileChildren(current, workInProgress, nextChildren, renderLanes);}// 返回child进行下一次循坏return workInProgress.child;
}
而在此之后那我们就进去一个由下往上的回溯阶段(递归中的归),也就是completeUnitOfWork
(performUnitOfWork
中的方法),同样我们关注在当前阅读这个主流程上函数做了什么,首先他给stateNode
去指向了一个Dom实例,设置属性绑定事件,设置flag标记,紧接着处理副作用链表队列[就是更新头尾指针],emm有数据结构算法基础的可以看看),然后有一些兄弟节点子节点的判断。
function completeUnitOfWork(unitOfWork: Fiber): void {let completedWork = unitOfWork;// 外层循环控制并移动指针(`workInProgress`,`completedWork`等)do {const current = completedWork.alternate;const returnFiber = completedWork.return;if ((completedWork.flags & Incomplete) === NoFlags) {let next;// 1. 处理Fiber节点, 关联Fiber节点和dom对象, 绑定事件next = completeWork(current, completedWork, subtreeRenderLanes); // 处理单个节点if (next !== null) {// 子节点, 则回到beginWork阶段进行处理workInProgress = next;return;}// 重置子节点的优先级resetChildLanes(completedWork);if (returnFiber !== null &&(returnFiber.flags & Incomplete) === NoFlags) {// 2. 收集当前Fiber节点以及其子树的副作用effects// 2.1 把子节点的副作用队列添加到父节点上,只有父节点的firstEffect不存在时// 才能将父节点的firstEffect指向当前节点的副作用单向链表头if (returnFiber.firstEffect === null) {//让父节点的firstEffect指向当前节点的firstEffectreturnFiber.firstEffect = completedWork.firstEffect;}//当前节点不存在副作用链表才加if (completedWork.lastEffect !== null) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork.firstEffect;}// 将当前节点加到副作用链表中returnFiber.lastEffect = completedWork.lastEffect;}// 2.2 如果当前fiber节点有副作用, 将其添加到子节点的副作用队列之后.const flags = completedWork.flags;if (flags > PerformedWork) {// 忽略PerformedWork// 将当前节点添加到副作用链表尾部if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork;} else {returnFiber.firstEffect = completedWork;}returnFiber.lastEffect = completedWork;}}}const siblingFiber = completedWork.sibling;if (siblingFiber !== null) {// 如果有兄弟节点, 返回之后再次进入beginWork阶段workInProgress = siblingFiber;return;}// 移动指针, 指向下一个节点completedWork = returnFiber;workInProgress = completedWork;} while (completedWork !== null);// 已回溯到根节点, 设置workInProgressRootExitStatus = RootCompletedif (workInProgressRootExitStatus === RootIncomplete) {workInProgressRootExitStatus = RootCompleted;}
}
到这那我们已经就可以走到上面的commitRoot
将内存里的fiber树
输出到react-dom
了。
阶段2.fiber树的输出
在我们的构建阶段结束后,会在performSyncWorkOnRoot
、finishConcurrentRender
中的fiberRoot
传给commitRoot
,去开始commit
提交阶段,此时已经到了fiber
的最终输出阶段,经过了一系列优先级转换,最终会执行commitRootImpl
。
function commitRoot(root) {// 渲染等级const renderPriorityLevel = getCurrentPriorityLevel()// 调度等级runWithPriority(ImmediateSchedulerPriority, commitRootImpl.bind(null, root, renderPriorityLevel))return null
}
commitRootImpl
主要工作就是遍历effect
数组去添加对应的标记,最终交给react-dom
执行,commit
有三个大阶段:
1.提交准备阶段
。
2.提交阶段
,而提交阶段中的三个while
循坏下分别又对应3个子阶段:before mutation
阶段。mutation
阶段。layout
阶段。
3.提交后
。代码较长我们分成3部分解释。
1:准备阶段。
我们大概做了这几个操作:
1.判断是否有未执行的useEffect
,如果有先执行,等待我们上一轮的useEffect
执行完后才继续我们commit
。
2.更新副作用队列,将根节点加到尾部,取出 firstEffect
,也就是第一个需要被处理的 fiber 节点。
3.接着判断如果存在 firstEffect
,会将 firstEffect
指向 nextEffect
,开始三个子阶段。
function commitRootImpl(root, renderPriorityLevel) {// 准备提交阶段// 判断rootWithPendingPassiveEffects,就是说先判断是否有未执行的useEffect,如果有先执行,等待我们上一轮的useEffect执行完后才继续我们commit。do {//flushPassiveEffects()} while (rootWithPendingPassiveEffects !== null)// 忽略代码// 再次更新副作用队列,在前面的函数式组件更新阶段后,`effct list`这条链表只有子节点,没有挂载到根节点上,默认情况下fiber节点的副作用队列是不包括自身的,如果根节点也存在 `effectTag`,那么就需要把根节点拼接到链表的末尾,形成一条完整的 `effect list`,取出第一个需要处理的fiber节点let firstEffectif (finishedWork.flags > PerformedWork) {if (finishedWork.lastEffect !== null) {finishedWork.lastEffect.nextEffect = finishedWorkfirstEffect = finishedWork.firstEffect} else {firstEffect = finishedWork}} else {// There is no effect on the root.firstEffect = finishedWork.firstEffect}// 忽略代码if (firstEffect !== null) {nextEffect = firstEffect// 进入第二阶段// beforeMutation 子阶段:// 执行 commitBeforeMutationEffects// mutation 子阶段:// 执行 commitMutationEffects// layout 子阶段:// 执行 commitLayoutEffects}// 忽略代码
}
2:提交阶段。
这里我们有三个子阶段,对应的是三个函数
commitBeforeMutationEffects
,
commitMutationEffects
,
commitLayoutEffects
let firstEffect = finishedWork.firstEffect
if (firstEffect !== null) {const prevExecutionContext = executionContextexecutionContext |= CommitContext// 阶段1: dom改变之前nextEffect = firstEffectdo {commitBeforeMutationEffects()} while (nextEffect !== null)// 阶段2: dom改变, 界面发生改变nextEffect = firstEffectdo {commitMutationEffects(root, renderPriorityLevel)} while (nextEffect !== null)// 恢复界面状态resetAfterCommit(root.containerInfo)// 切换current指针root.current = finishedWork// 阶段3: layout阶段, 调用生命周期componentDidUpdate和回调函数等nextEffect = firstEffectdo {commitLayoutEffects(root, lanes)} while (nextEffect !== null)nextEffect = nullexecutionContext = prevExecutionContext
}
子阶段1:在dom变化前,commitBeforeMutationEffects
会主要做两件事。1、执行getSnapshowBeforeUpdate
(commitBeforeMutationEffectOnFiber
)生命周期方法。2、调度useEffect
function commitBeforeMutationEffects() {while (nextEffect !== null) {// 忽略代码const effectTag = nextEffect.effectTagif ((effectTag & Snapshot) !== NoEffect) {// 忽略代码commitBeforeMutationEffectOnFiber(current, nextEffect)}// 忽略代码nextEffect = nextEffect.nextEffect}
}// 省略代码
function commitBeforeMutationEffects() {while (nextEffect !== null) {const current = nextEffect.alternateconst flags = nextEffect.flags// 处理Snapshot标记if ((flags & Snapshot) !== NoFlags) {// 这里实际就是处理2个节点,对于ClassComponent类型节点, 调用instance.getSnapshotBeforeUpdate生命周期函数,对于HostRoot类型节点, 调用clearContainer清空了容器节点(即`div#root`这个 dom 节点).commitBeforeMutationEffectOnFiber(current, nextEffect)}// 处理Passive标记if ((flags & Passive) !== NoFlags) {// 处理useEffect// 用于执行useEffect的回调函数,不立即执行,放在scheduleCallBack的回调中,异步根据优先级执行这个回调函数,如果说存在Passive effect,就将rootDoesHavePassiveEffects置为true,并且加入调度,而我们整个commit是一个同步执行操作,所以useEffect会在commit完成后去异步执行。这就跟上面的对上了。if (!rootDoesHavePassiveEffects) {rootDoesHavePassiveEffects = truescheduleCallback(NormalSchedulerPriority, () => {flushPassiveEffects()return null})}}nextEffect = nextEffect.nextEffect}
}
子阶段2:dom 变更, 界面得到更新. 处理副作用队列中带有ContentReset
, Ref
, Placement
, Update
, Deletion
, Hydrating
标记的fiber
节点. 调用栈:
1. 新增
: 函数调用栈 commitPlacement
-> insertOrAppendPlacementNode
或者insertOrAppendPlacementNode
-> appendChild
或者insertBefore
为什么会有或那,因为实际上我们的fiber
树和dom
数并不表现一致,后面单独写一篇文章讲一下。
2. 更新
: 函数调用栈 commitWork
-> commitUpdate
commitWork
,会根据fiber
节点的不同做不同的更新操作
3. 删除: 函数调用栈 commitDeletion
-> unmountHostComponents
-> removeChild
commitDeletion
其实是调用unmountHostComponents
,
对不同的节点类型做销毁操作。 我们最终就会调用react-dom
里面的api,去让页面实现更新
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {while (nextEffect !== null) {const effectTag = nextEffect.effectTag;// 如果有 ContentReset,会重置文本节点if (effectTag & ContentReset) {commitResetTextContent(nextEffect);}// 如果有 Ref,会执行 ref 相关的更新if (flags & Ref) {const current = nextEffect.alternate;if (current !== null) {// 先清空ref, 在commitRoot的第三阶段(dom变更后), 再重新赋值commitDetachRef(current);}}// 处理dom的变化const primaryEffectTag =effectTag & (Placement | Update | Deletion | Hydrating);switch (primaryEffectTag) {// 如果需要插入节点,会执行 commitPlacementcase Placement: {commitPlacement(nextEffect);nextEffect.effectTag &= ~Placement;break;}// 如果需要更新节点,会执行 commitWorkcase Update: {const current = nextEffect.alternate;commitWork(current, nextEffect);break;}// 如果需要删除节点,会执行 commitDeletioncase Deletion: {commitDeletion(root, nextEffect, renderPriorityLevel);break;}// 忽略代码}// 取出下一个 fiber 节点,进入下一次循环nextEffect = nextEffect.nextEffect;}
}
子阶段3:layout
,commitLayoutEffects
核心是执行commitLayoutEffectOnFiber
这个阶段会根据fiber
节点的类型执行不同的处理。
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {// 忽略代码while (nextEffect !== null) {const flags = nextEffect.flags;// 处理标记if (flags & (Update | Callback)) {const current = nextEffect.alternate;commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);}if (flags & Ref) {// 重新设置refcommitAttachRef(nextEffect);}// 忽略代码nextEffect = nextEffect.nextEffect;}// 忽略代码
}
commitLayoutEffectOnFiber
是引入commitLifeCycles
设置的别名,我们先针对于 FunctionComponent
这个case
来分析后面的调用逻辑(因为函数式组件是现在的主流嘛),这里会把 HookLayout
这个 tag
`类型传给 commitHookEffectListMount
方法,也就是说接下来的commitHookEffectListMount
会执行 useLayoutEffect
的回调函数。
接着执行schedulePassiveEffects
,就是把带有Passive
标记的effect
筛选出来(由useEffect
创建), 添加到一个全局数组(pendingPassiveHookEffectsUnmount
和pendingPassiveHookEffectsMount
).。
function commitLifeCycles(finishedRoot: FiberRoot,current: Fiber | null,finishedWork: Fiber,committedLanes: Lanes,
): void {switch (finishedWork.tag) {case FunctionComponent:case ForwardRef:case SimpleMemoComponent:case Block: {{try {startLayoutEffectTimer();// 执行useLayoutEffect回调函数commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);} finally {recordLayoutEffectDuration(finishedWork);}}// finishedWork指的是正在被遍历的有副作用的fiber,放入调度中schedulePassiveEffects(finishedWork);return;}
}
对于FunctionComponent
,commitHookEffectListMount
方法会执行我们的effect.creat
并指向destory
销毁,
接着执行schedulePassiveEffects
方法,在这里会分别注册 useEffect
,
推进 pendingPassiveHookEffectsUnmount
和 pendingPassiveHookEffectsMount
这两个数组中,用于后续flushPassveEffects
执行。
function commitHookEffectListMount(tag: number, finishedWork: Fiber) {const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;if (lastEffect !== null) {const firstEffect = lastEffect.next;let effect = firstEffect;do {if ((effect.tag & tag) === tag) {const create = effect.create;//执行effect回调,调用effect.create()之后, 将返回值赋值到effect.destroy.effect.destroy = create();}effect = effect.next;} while (effect !== firstEffect);}
}
function schedulePassiveEffects(finishedWork: Fiber) {// 1. 获取 fiber.updateQueueconst updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);// 2. 获取 effect环形队列const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;if (lastEffect !== null) {const firstEffect = lastEffect.next;let effect = firstEffect;do {const { next, tag } = effect;// 筛选出由useEffect()创建的effectif ((tag & HookPassive) !== NoHookEffect &&(tag & HookHasEffect) !== NoHookEffect) {// 把effect添加到全局数组, 等待flushPassiveEffectsImpl处理enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);enqueuePendingPassiveHookEffectMount(finishedWork, effect);}effect = next;} while (effect !== firstEffect);}
}export function enqueuePendingPassiveHookEffectUnmount(fiber: Fiber,effect: HookEffect,
): void {// unmount effects 数组pendingPassiveHookEffectsUnmount.push(effect, fiber);
}export function enqueuePendingPassiveHookEffectMount(fiber: Fiber,effect: HookEffect,
): void {// mount effects 数组pendingPassiveHookEffectsMount.push(effect, fiber);
}
到此时第三子阶段就可以告于段落了,紧接着就是`commit`的第三个阶段
3:渲染完成后
渲染后主要是做一些清理
、检测更新
操作,当然我们这里还是以函数式组件为例。 清理
:清理有两个地方,一个是链表拆解的清理,因为gc没法回收,就得手动把链表拆开。
第二个地方,因为我们前面保存了2个数组unmount effect
和mount effects
,useEffect
会留置到 flushPassiveEffects
()监测更新后再去清理。
nextEffect = firstEffect
while (nextEffect !== null) {const nextNextEffect = nextEffect.nextEffectnextEffect.nextEffect = nullif (nextEffect.flags & Deletion) {detachFiberAfterEffects(nextEffect)}nextEffect = nextNextEffect
}
监测更新
:重点是在这个地方,在渲染后的更新我们只说2个必定会调用的监测函数ensureRootIsScheduled
和flushSyncCallbackQueue
,ensureRootIsScheduled
这是用来处理异步任务的(走到我们前文说的流程),
flushSyncCallbackQueue
处理同步任务,如果有就直接调用,再次进入fiber树构造,
function flushSyncCallbackQueueImpl() {if (!isFlushingSyncQueue && syncQueue !== null) {// Prevent re-entrancy.isFlushingSyncQueue = truelet i = 0if (decoupleUpdatePriorityFromScheduler) {const previousLanePriority = getCurrentUpdateLanePriority()try {const isSync = trueconst queue = syncQueuesetCurrentUpdateLanePriority(SyncLanePriority)runWithPriority(ImmediatePriority, () => {for (; i < queue.length; i++) {let callback = queue[i]do {//循坏执行同步任务callback = callback(isSync)} while (callback !== null)}})syncQueue = null} catch (error) {// If something throws, leave the remaining callbacks on the queue.if (syncQueue !== null) {syncQueue = syncQueue.slice(i + 1)}// Resume flushing in the next tickScheduler_scheduleCallback(Scheduler_ImmediatePriority, flushSyncCallbackQueue)throw error} finally {setCurrentUpdateLanePriority(previousLanePriority)isFlushingSyncQueue = false}} else {try {const isSync = trueconst queue = syncQueuerunWithPriority(ImmediatePriority, () => {for (; i < queue.length; i++) {let callback = queue[i]do {callback = callback(isSync)} while (callback !== null)}})syncQueue = null} catch (error) {// If something throws, leave the remaining callbacks on the queue.if (syncQueue !== null) {syncQueue = syncQueue.slice(i + 1)}// Resume flushing in the next tickScheduler_scheduleCallback(Scheduler_ImmediatePriority, flushSyncCallbackQueue)throw error} finally {isFlushingSyncQueue = false}}}
}
到这里就很简单了循环去执行同步任务,再次构建。
1.3 ensureRootIsScheduled
注册调度就非常的简单了
IsScheduled(root: FiberRoot, currentTime: number) {//判断是否注册新的调度const existingCallbackNode = root.callbackNode;const nextLanes = getNextLanes(root,root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,);const newCallbackPriority = returnNextLanesPriority();// Schedule a new callback.let newCallbackNode;if (newCallbackPriority === SyncLanePriority) {// 注册调度走到scheduler去newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root),);} else if (newCallbackPriority === SyncBatchedLanePriority) {// 批处理注册调度走到scheduler去newCallbackNode = scheduleCallback(ImmediateSchedulerPriority,performSyncWorkOnRoot.bind(null, root),);} else {// 并发注册调度走到scheduler去const schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority,);newCallbackNode = scheduleCallback(schedulerPriorityLevel,performConcurrentWorkOnRoot.bind(null, root),);}root.callbackPriority = newCallbackPriority;root.callbackNode = newCallbackNode;
}
说来说去我们就只关心一个事情newCallbackNode = scheduleCallback( ImmediateSchedulerPriority, performSyncWorkOnRoot.bind(null, root), );
他这里把回调函数给到调度中心并绑定节点,等待调度执行之后重复上面1.2的所有操作,我们下一节分析调度是怎么做的。
2.手写
import { arrified, getRoot, getTag, createStateNode } from '../until'
import { commitAllWork } from './commit'
import { scheduleCallback } from '../scheduler'
let first = 1
let subTask = null
let pendingCommit = null
// 构建最外层的fiber对象
function createOutFiber(jsx, root) {const task = {root,props: {children: jsx}}let outFiberif (task.from === 'class_component') {const root = getRoot(task.instance)task.instance.__fiber.partialState = task.partialStateoutFiber = {props: root.props,stateNode: root.stateNode,tag: 'host_root',effects: [],child: null,alternate: root}return outFiber}outFiber = {props: task.props,stateNode: task.root,tag: 'host_root',effects: [],child: null,alternate: task.root.__rootFiberContainer}return outFiber
}function reconcileChildren(fiber, children) {/**
children 可能对象 也可能是数组
将children 转换成数组*/const arrifiedChildren = arrified(children)/**
循环 children 使用的索引*/let index = 0/**
children 数组中元素的个数*/let numberOfElements = arrifiedChildren.length/**
循环过程中的循环项 就是子节点的 virtualDOM 对象*/let element = null/**
子级 fiber 对象*/let newFiber = null/**
上一个兄弟 fiber 对象*/let prevFiber = nulllet alternate = nullif (fiber.alternate && fiber.alternate.child) {alternate = fiber.alternate.child}console.log(arrifiedChildren)while (index < numberOfElements || alternate) {/*** 子级 virtualDOM 对象*/element = arrifiedChildren[index]if (!element && alternate) {/*** 删除操作*/alternate.effectTag = 'delete'fiber.effects.push(alternate)} else if (element && alternate) {/*** 更新*/newFiber = {type: element.type,props: element.props,tag: getTag(element),effects: [],effectTag: 'update',parent: fiber,alternate}if (element.type === alternate.type) {/*** 类型相同*/newFiber.stateNode = alternate.stateNode} else {/*** 类型不同*/newFiber.stateNode = createStateNode(newFiber)}} else if (element && !alternate) {/*** 初始渲染*//*** 子级 fiber 对象*/newFiber = {type: element.type,props: element.props,tag: getTag(element),effects: [],effectTag: 'placement',parent: fiber}/*** 为fiber节点添加DOM对象或组件实例对象*/newFiber.stateNode = createStateNode(newFiber)newFiber.stateNode = createStateNode(newFiber)}if (index === 0) {fiber.child = newFiber} else if (element) {prevFiber.sibling = newFiber}if (alternate && alternate.sibling) {alternate = alternate.sibling} else {alternate = null}// 更新prevFiber = newFiberindex++ //保存构建fiber节点的索引,等待事件后通过索引再次进行构建}
}function workLoopSync() {while (subTask) {subTask = performUnitOfWork(subTask)}if (pendingCommit) {first++commitAllWork(pendingCommit)}
}// 构建子集的fiber对象的任务单元
function performUnitOfWork(fiber) {reconcileChildren(fiber, fiber.props.children)/**
如果子级存在 返回子级
将这个子级当做父级 构建这个父级下的子级*/if (fiber.child) {return fiber.child}/**
如果存在同级 返回同级 构建同级的子级
如果同级不存在 返回到父级 看父级是否有同级*/let currentExecutelyFiber = fiberwhile (currentExecutelyFiber.parent) {currentExecutelyFiber.parent.effects = currentExecutelyFiber.parent.effects.concat(currentExecutelyFiber.effects.concat([currentExecutelyFiber]))if (currentExecutelyFiber.sibling) {return currentExecutelyFiber.sibling}currentExecutelyFiber = currentExecutelyFiber.parent}pendingCommit = currentExecutelyFiberconsole.log(pendingCommit)
}// 源码地址 https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L674-L736
function ensureRootIsScheduled(fiber) {//这里我们可以直接走到注册调度任务,暂时我们分析的是Legacy模式,Concurrent模式实现的performConcurrentWorkOnRoot实现的可中断渲染可以以后实现let newCallbackNode//接下来就可以直接走到调度中心去newCallbackNode = scheduleCallback(workLoopSync)
}// 源码地址 https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L517-L619
function scheduleUpdateOnFiber(fiber) {subTask = fiberif (!first) {//对应暂无render上下文// 对于初次构建来说我们直接进行`fiber构造`.workLoopSync()} else {//对于后续更新以及操作都选择去注册调度任务ensureRootIsScheduled(subTask)}
}
export function render(jsx, root) {const outFiber = createOutFiber(jsx, root)scheduleUpdateOnFiber(outFiber)
}
然后我们就快乐的简单实现了fiberDom的过程~~~
# 总结
比较多工程化和基础的东西,还没完善,有兴趣的可以[github]拉下来试试
欢迎加入群聊,我们一起讨论一些更有趣的技术、商业、闲聊。
相关文章:

React源码阅读-fiber核心构建原理
React源码阅读(2)-fiber核心构建原理 好的,我明白了。您提供的文本主要介绍了 React 源码中 Fiber 核心的构建原理,涵盖了从执行上下文到构建、提交、调度等关键阶段,以及相关的代码实现。 您提出的关联问题也很重要,它们深入探讨…...

视频监控管理平台EasyCVR与V4分析网关对接后告警照片的清理优化方案
一、问题概述 在安防监控、设备运维等场景中,用户将视频监控管理平台EasyCVR与V4网关通过http推送方式协同工作时,硬件盒子上传的告警图片持续累积,导致EasyCVR服务器存储空间耗尽,影响系统正常运行与告警功能使用。 二、解决方…...
基于 BGE 模型与 Flask 的智能问答系统开发实践
基于 BGE 模型与 Flask 的智能问答系统开发实践 一、前言 在人工智能快速发展的今天,智能问答系统成为了提升信息检索效率和用户体验的重要工具。本文将详细介绍如何利用 BGE(Base General Embedding)模型、Faiss 向量检索库以及 Flask 框架…...

机器学习:决策树和剪枝
本文目录: 一、决策树基本知识(一)概念(二)决策树建立过程 二、决策树生成(一)ID3决策树:基于信息增益构建的决策树。(二)C4.5决策树(三ÿ…...

vscode自定义主题语法及流程
vscode c/c 主题 DIY 启用自己的主题(最后步骤) 重启生效 手把手教你制作 在C:\Users\jlh.vscode\extensions下自己创建一个文件夹 里面有两个文件一个文件夹 package.json: {"name":"theme-jlh","displayName":"%displayName%&qu…...

vue中加载Cesium地图(天地图、高德地图)
目录 1、将下载的Cesium包移动至public下 2、首先需要将Cesium.js和widgets.css文件引入到 3、 新建Cesium.js文件,方便在全局使用 4、新建cesium.vue文件,展示三维地图 1、将下载的Cesium包移动至public下 npm install cesium后 2、…...

SpringBoot整合RocketMQ与客户端注意事项
SpringBoot整合RocketMQ 引入依赖(5.3.0比较稳定) <dependencies><dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-spring-boot-starter</artifactId><version>2.3.1</version&…...
Github 2025-06-04 C开源项目日报 Top7
根据Github Trendings的统计,今日(2025-06-04统计)共有7个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量C项目7C++项目1Assembly项目1jq:轻量灵活的命令行JSON处理器 创建周期:4207 天开发语言:C协议类型:OtherStar数量:27698 个Fork数量:1538 …...
大二下期末
一.Numpy(Numerical Python) Numpy库是Python用于科学计算的基础包,也是大量Python数学和科学计算包的基础。不少数据处理和分析包都是在Numpy的基础上开发的,如后面介绍的Pandas包。 Numpy的核心基础是ndarray(N-di…...
LeetCode 热题 100 74. 搜索二维矩阵
LeetCode 热题 100 | 74. 搜索二维矩阵 大家好,今天我们来解决一道经典的算法题——搜索二维矩阵。这道题在 LeetCode 上被标记为中等难度,要求我们在一个满足特定条件的二维矩阵中查找一个目标值。如果目标值在矩阵中,返回 true;…...
解决 VSCode 中无法识别 Node.js 的问题
当 VSCode 无法识别 Node.js 时,通常会出现以下症状: 代码提示缺失require 等 Node.js API 被标记为错误调试功能无法正常工作终端无法运行 Node.js 命令 常见原因及解决方案 1. Node.js 未安装或未正确配置 解决方法: 确保已安…...

Mysql的卸载与安装
确保卸载干净mysql 不然在进行mysal安装时候会出现不一的页面和问题 1、卸载 在应用页面将查询到的mysql相关应用卸载 2、到c盘下将残留的软件包进行数据删除 3、删除programData下的mysql数据 4、检查系统中的mysql是否存在 cmd中执行 sc deleted mysql80 5、删除注册表中的…...
ES101系列09 | 运维、监控与性能优化
本篇文章主要讲解 ElasticSearch 中 DevOps 与性能优化的内容,包括集群部署最佳实践、容量规划、读写性能优化和缓存、熔断器等。 集群部署最佳实践 在生产环境中建议设置单一角色的节点。 Dedicated master eligible nodes:负责集群状态的管理。使用…...
Java常用的判空方法
文章目录 Java常用的判空方法JDK 自带的判空方法1. 使用 或 ! 运算符2. 使用 equals 方法3. Objects.isNull / Objects.nonNull4. Objects.equals4. JDK8 中的 Optional 第三方工具包1. Apache Commons Lang32. Google Guava3. Lombok 注解4. Vavr(函数式风格&…...

Excel处理控件Aspose.Cells教程:使用 C# 在 Excel 中创建组合图表
可视化项目时间线对于有效规划和跟踪至关重要。在本篇教程中,您将学习如何使用 C# 在 Excel 中创建组合图。只需几行代码,即可自动生成动态、美观的组合图。无论您是在构建项目管理工具还是处理内部报告,本指南都将向您展示如何将任务数据转换…...

【多线程初阶】阻塞队列 生产者消费者模型
文章目录 一、阻塞队列二、生产者消费者模型(一)概念(二)生产者消费者的两个重要优势(阻塞队列的运用)1) 解耦合(不一定是两个线程之间,也可以是两个服务器之间)2) 削峰填谷 (三)生产者消费者模型付出的代价 三、标准库中的阻塞队列(一)观察模型的运行效果(二)观察阻塞效果1) 队…...

《100天精通Python——基础篇 2025 第5天:巩固核心知识,选择题实战演练基础语法》
目录 一、踏上Python之旅二、Python输入与输出三、变量与基本数据类型四、运算符五、流程控制 一、踏上Python之旅 1.想要输出 I Love Python,应该使用()函数。 A.printf() B.print() C.println() D.Print() 在Python中想要在屏幕中输出内容,应该使用print()函数…...

机器人夹爪的选型与ROS通讯——机器人抓取系统基础系列(六)
文章目录 前言一、夹爪的选型1.1 任务需求分析1.2 软体夹爪的选型 二、夹爪的ROS通讯2.1 夹爪的通信方式介绍2.2 串口助手测试2.3 ROS通讯节点实现 总结Reference: 前言 本文将介绍夹爪的选型方法和通讯方式。以鞋子这类操作对象为例,将详细阐述了对应的夹爪选型过…...

第二十八章 RTC——实时时钟
第二十八章 RTC——实时时钟 目录 第二十八章 RTC——实时时钟 1 RTC实时时钟简介 2 RTC外设框图剖析 3 UNIX时间戳 4 与RTC控制相关的库函数 4.1 等待时钟同步和操作完成 4.2 使能备份域涉及RTC配置 4.3 设置RTC时钟分频 4.4 设置、获取RTC计数器及闹钟 5 实时时…...

使用 DuckLake 和 DuckDB 构建 S3 数据湖实战指南
本文介绍了由 DuckDB 和 DuckLake 组成的轻量级数据湖方案,旨在解决传统数据湖(如HadoopHive)元数据管理复杂、查询性能低及厂商锁定等问题。该方案为中小规模数据湖场景提供了简单、高性能且无厂商锁定的替代选择。 1. 什么是 DuckLake 和 D…...

大语言模型提示词(LLM Prompt)工程系统性学习指南:从理论基础到实战应用的完整体系
文章目录 前言:为什么提示词工程成为AI时代的核心技能一、提示词的本质探源:认知科学与逻辑学的理论基础1.1 认知科学视角下的提示词本质信息处理理论的深层机制图式理论的实际应用认知负荷理论的优化策略 1.2 逻辑学框架下的提示词架构形式逻辑的三段论…...

如何基于Mihomo Party http端口配置git与bash命令行代理
如何基于Mihomo Party http端口配置git与bash命令行代理 1. 确定Mihomo Party http端口配置 点击内核设置后即可查看 默认7892端口,开启允许局域网连接 2. 配置git代理 配置本机代理可以使用 127.0.0.1 配置局域网内其它机代理需要使用本机的非回环地址 IP&am…...
CMake 为 Debug 版本的库或可执行文件添加 d 后缀
在使用 CMake 构建项目时,我们经常需要区分 Debug 和 Release 构建版本。一个常见的做法是为 Debug 版本的库或可执行文件添加后缀(如 d),例如 libmylibd.so 或 myappd.exe。 本文将介绍几种在 CMake 中实现为 Debug 版本自动添加 d 后缀的方法。 方法一:使用 CMAKE_DEBU…...
Linux 特殊权限位详解:SetUID, SetGID, Sticky Bit
Linux 特殊权限位详解:SetUID, SetGID, Sticky Bit 在Linux权限系统中,除了基本的读、写(w)、执行(x)权限外,还有三个特殊权限位:SetUID、SetGID和Sticky Bit。这些权限位提供了更精细的权限控制机制,尤其在需要临时提升权限或管理共享资源时非常有用。 一、SetUID (s位…...

埃文科技智能数据引擎产品入选《中国网络安全细分领域产品名录》
嘶吼安全产业研究院发布《中国网络安全细分领域产品名录》,埃文科技智能数据引擎产品成功入选数据分级分类产品名录。 在数字化转型加速的今天,网络安全已成为企业生存与发展的核心基石,为了解这一蓬勃发展的产业格局,嘶吼安全产业…...
使用VTK还是OpenGL集成到qt程序里哪个好?
在Qt程序中集成VTK与OpenGL:选择哪个更好? 在Qt程序中实现三维可视化时,开发者常常面临一个选择:是使用VTK(Visualization Toolkit)还是OpenGL(Open Graphics Library)。这两种技术…...
Java-IO流之打印流详解
Java-IO流之打印流详解 一、打印流概述1.1 什么是打印流1.2 打印流的特点1.3 打印流的应用场景 二、PrintStream详解2.1 基本概念2.2 构造函数2.3 核心方法2.4 使用示例 三、PrintWriter详解3.1 基本概念3.2 构造函数3.3 核心方法3.4 使用示例 四、PrintStream与PrintWriter的比…...
高效图像处理:使用 Pillow 进行格式转换与优化
高效图像处理:使用 Pillow 进行格式转换与优化 1. 背景引入 在图像处理应用中,格式转换、裁剪、压缩等操作是常见需求。Python 的 Pillow 库基于 PIL(Python Imaging Library),提供 轻量、强大 的图像处理能力,广泛用于 Web 开发、数据分析、机器学习 等领域。 本文将…...
Github 2025-06-06 Java开源项目日报Top10
根据Github Trendings的统计,今日(2025-06-06统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Java项目10TypeScript项目1Java实现的算法集合:使用Gitpod.io进行编辑和贡献 创建周期:2883 天开发语言:Java协议类型:MIT LicenseStar数量…...
使用 Ansible 在 Windows 服务器上安装 SSL 证书
在本教程中,我将向您展示如何使用 Ansible 在 Windows 服务器上安装 SSL 证书。使用 Ansible 自动化 SSL 证书安装过程可以提高 IT 运营的效率、一致性和协作性。我将介绍以下步骤: 将 SSL 证书文件复制到服务器将 PFX 证书导入指定的存储区获取导入的证…...