引用一段话,作为我对Fiber架构的理解
You can think of a fiber as a data structure that represents some work to do or, in other words, a unit of work. Fiber’s architecture also provides a convenient way to track, schedule, pause and abort the work.
Fiber架构是通过数据结构来代表任务的集合,可以基于这个架构来实现跟踪,调度,暂停,取消任务;
后面理解Fiber 架构的过程都是基于下面的示例代码
import React, { useEffect, useState, useRef, useCallback } from 'react';
export default function Test() {
let name = 1;
const logRef = useRef(null);
const logRef2 = useRef(null);
const logRef3 = useRef(null);
const [a, setA] = useState('a');
const [b, setB] = useState('b');
const [c, setC] = useState('c');
const handler = useCallback(() => {
setA(a + a);
setB(b + b);
setC(c + c);
}, [a, b, c]);
return (
<section>
<header>{a}</header>
<div>{b}</div>
<b>{c}</b>
<p onClick={handler}>Click Me</p>
</section>
);
}
React Fiber分成两个阶段
Render Phase
Fiber建构中有两个树:current和workInProgress
任务开始是从Root开始:
function performSyncWorkOnRoot(root) {
...
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
}
在第一次render后,React会创建一个当前状态的树,这个树我们就是current树;
当React在遍历current树时,会为根据current上节点的alternate属性,
为每个Fiber Node创建workInProgress 节点,这样就构成了workInProgress tree;
workInProgress = createWorkInProgress(root.current, null);
function createWorkInProgress(current, pendingProps) {
var workInProgress = current.alternate;
if (workInProgress === null) {
// ...
} else {
workInProgress.pendingProps = pendingProps; // We already have an alternate.
// Reset the effect tag.
workInProgress.effectTag = NoEffect; // The effect list is no longer valid.
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
}
// ...
workInProgress.childExpirationTime = current.childExpirationTime;
workInProgress.expirationTime = current.expirationTime;
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.
return workInProgress;
}
做好上面的准备后,开始深度遍历Node开始计算render过程了。
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
}
这里的workInProgress就是上面createWorkInProgress中生成的,当前指向root.current.alternate;
后面开始从workInProgress出发,执行任务单元;
function performUnitOfWork(unitOfWork) {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
var current = unitOfWork.alternate;
startWorkTimer(unitOfWork);
setCurrentFiber(unitOfWork);
var next;
if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork$1(current, unitOfWork, renderExpirationTime$1);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork$1(current, unitOfWork, renderExpirationTime$1);
}
resetCurrentFiber();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(unitOfWork);
}
ReactCurrentOwner$2.current = null;
return next;
}
beginWork里面会根据当前Fiber Node的tag来判断如何执行; 比如当前tag=3代表是Root Node;
switch (workInProgress.tag) {
case HostRoot: // 3
pushHostRootContext(workInProgress);
resetHydrationState();
break;
case HostComponent:
}
beginWork执行完成后,会返回下个要执行的任务单元:child 节点;
然后返回到performUnitOfWork阶段,继续执行下一个循环;
根据上面的代码,这里传入的unitOfWork为Root的child节点,
重复上面的操作:current指向unitOfWork.alternate;
在这个Demo里指向的是最外层的APP container,tag=5(HostComponet);
依次通过child属性进行遍历
Root -> App Div -> Test FC
Current = child.alternate
同样到底,当我们遍历到section节点时,props中的children值会发生变化(父组件重新创建,会导致子组件都会重新render);
当render到header节点时,我们看下beginWork的执行情况
function beginWork(current, workInProgress, renderExpirationTime) {
var updateExpirationTime = workInProgress.expirationTime;
// ...
if (current !== null) {
var oldProps = current.memoizedProps;
var newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasContextChanged() || ( // Force a re-render if the implementation changed due to hot reload:
workInProgress.type !== current.type )) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
} else if (updateExpirationTime < renderExpirationTime) {
didReceiveUpdate = false;
// ...
这里会涉及到Props的比较,比如这里setState之后:
后面开始进入updateComponet
function updateHostComponent(current, workInProgress, renderExpirationTime) {
pushHostContext(workInProgress);
// ...
reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime);
return workInProgress.child;
}
最终还是进入reconcileChildren方法
// reconcileChildren function
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderExpirationTime);
// 隔离
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, expirationTime) {
// ...
var resultingFirstChild = null;
var previousNewFiber = null;
var oldFiber = currentFirstChild;
var lastPlacedIndex = 0;
var newIdx = 0;
var nextOldFiber = null;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], expirationTime);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
比如这里会创建一个纯文本, updateSlot最终会调用下面的方式,借用createWorkInProgress方案实现新建一个Fiber Node
function useFiber(fiber, pendingProps) {
// We currently set sibling to null and index to 0 here because it is easy
// to forget to do before returning it. E.g. for the single child case.
var clone = createWorkInProgress(fiber, pendingProps);
clone.index = 0;
clone.sibling = null;
return clone;
}
最终会通过useFiber来实现新建一个fiber Node,作为nextNewFiber; 最终返回的resultingFirstChild就是header的第一个子节点,文本节点status; 并且 workInProgress.child指向当前节点,并且把这个节点作为performUnitOfWork的参数开始执行,最终会返回一个null;
现在把执行完header,p,span,b等Component之后,我们继续执行performSyncWorkOnRoot
root.finishedWork = root.current.alternate;
root.finishedExpirationTime = expirationTime;
finishSyncRender(root);
finishedWork就代表了当前经过第一调制阶段后的结果:在这之前我们叫workInProgress; 更新之后,这个树会变成下个render阶段的current;也就是说当前finishedWork上面的状态已经是最新的状态,真正需要render的diff是这里的effect list;
最终我们从root节点开始,
root.child
--firstEffect--> header
--nextEffect--> div
--nextEffect--> b
--nextEffect--> p(useCallback会导致函数重新生成)
root.child
--lastEffect--> p
这里Effect的顺序因为是深度遍历的,如果你把上面代码改成
<header>{a}{b}</header
你会发现,firstEffect是指向的 FiberNode a , 然后nextEffect指向的是Fiber Node b 两个的tag类型都是hostText;
结果生成后,开始进入commit阶段
Commit
function finishSyncRender(root) {
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
commitRoot(root);
}
function runWithPriority$1(reactPriorityLevel, fn) {
var priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
return Scheduler_runWithPriority(priorityLevel, fn);
}
根据优先级来执行任务
firstEffect = finishedWork.firstEffect;
拿到第一个effect入口
function invokeGuardedCallback(name, func, context, a, b, c, d, e, f) {
hasError = false;
caughtError = null;
invokeGuardedCallbackImpl$1.apply(reporter, arguments);
}
事件绑定:
var evtType = "react-" + (name ? name : 'invokeguardedcallback');
// evtType: react-invokeguardedcallback
fakeNode.addEventListener(evtType, callCallback, false);
evt.initEvent(evtType, false, false);
fakeNode.dispatchEvent(evt);
这里callCallback会执行下面函数
function commitMutationEffects(root, renderPriorityLevel) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
var primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
// ...
case Update:
{
var _current3 = nextEffect.alternate;
// current3: a; nextEffect: aa
commitWork(_current3, nextEffect);
break;
}
// ...
recordEffect();
resetCurrentFiber();
nextEffect = nextEffect.nextEffect;
}
然后coomitWork
case HostText:
{
if (!(finishedWork.stateNode !== null)) {
{
throw Error( "This should have a text node initialized. This error is likely caused by a bug in React. Please file an issue." );
}
}
// stateNode:Text Dom Node
var textInstance = finishedWork.stateNode;
var newText = finishedWork.memoizedProps; // For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
var oldText = current !== null ? current.memoizedProps : newText;
commitTextUpdate(textInstance, oldText, newText);
return;
}
最终执行了
function commitTextUpdate(textInstance, oldText, newText) {
textInstance.nodeValue = newText;
}
这里汇总了一些基本的DOM 的操作API
然后再获取下一个nextEffect
nextEffect = nextEffect.nextEffect;
另外单独看下FC的render过程:
case FunctionComponent:
{
var _Component = workInProgress.type;
var unresolvedProps = workInProgress.pendingProps;
var resolvedProps = workInProgress.elementType === _Component ? unresolvedProps : resolveDefaultProps(_Component, unresolvedProps);
return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderExpirationTime);
}
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderExpirationTime) {
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.expirationTime = NoWork; // The following should have already
//...
var children = Component(props, secondArg); // Check if there was a render phase update
if (workInProgress.expirationTime === renderExpirationTime) {
// Keep rendering in a loop for as long as render phase updates continue to
// be scheduled. Use a counter to prevent infinite loops.
var numberOfReRenders = 0;
do {
workInProgress.expirationTime = NoWork;
if (!(numberOfReRenders < RE_RENDER_LIMIT)) {
{
throw Error( "Too many re-renders. React limits the number of renders to prevent an infinite loop." );
}
}
numberOfReRenders += 1;
{
// Even when hot reloading, allow dependencies to stabilize
// after first render to prevent infinite render phase updates.
ignorePreviousDependencies = false;
} // Start over from the beginning of the list
currentHook = null;
workInProgressHook = null;
workInProgress.updateQueue = null;
{
// Also validate hook order for cascading updates.
hookTypesUpdateIndexDev = -1;
}
ReactCurrentDispatcher.current = HooksDispatcherOnRerenderInDEV ;
children = Component(props, secondArg);
} while (workInProgress.expirationTime === renderExpirationTime);
} // We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrancy.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
{
workInProgress._debugHookTypes = hookTypesDev;
} // This check uses currentHook so that it works the same in DEV and prod bundles.
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
var didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;
renderExpirationTime = NoWork;
currentlyRenderingFiber$1 = null;
currentHook = null;
workInProgressHook = null;
{
currentHookNameInDev = null;
hookTypesDev = null;
hookTypesUpdateIndexDev = -1;
}
didScheduleRenderPhaseUpdate = false;
if (!!didRenderTooFewHooks) {
{
throw Error( "Rendered fewer hooks than expected. This may be caused by an accidental early return statement." );
}
}
return children;
}
Componet(props, secondArg)会返回一个Object对象,里面有描述FC render之后的描述信息;
并且会返回nextChild,比如这里就是section; 最终会进入render child的阶段
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, expirationTime)
{
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, expirationTime));
case REACT_PORTAL_TYPE:
return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, expirationTime));
}
}
最终还是会返回一个Fiber Node,并把workInProgress.child指向这个Node
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderExpirationTime);
最上面updateFunctionComponent对FC的执行,也就返回这个child Node;
并把开始下一轮的循环执行:performUnitOfWork
while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
依次深度遍历执行,直到遍历完所有Fiber Node,生成workInProgress Tree,并且计算完effect-list;