接着一篇说道整个App以及内部子组件都已经渲染成了Fiber树,那么接下来就会看看如何将Fiber树渲染为真实的DOM节点。
function completeUnitOfWork(workInProgress) {
// Attempt to complete the current unit of work, then move to the
// next sibling. If there are no more siblings, return to the
// parent fiber.
while (true) {
// 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$$1 = workInProgress.alternate;
{
setCurrentFiber(workInProgress);
}
var returnFiber = workInProgress.return;
var siblingFiber = workInProgress.sibling;
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
// Don't replay if it fails during completion phase.
mayReplayFailedUnitOfWork = false;
}
// This fiber completed.
// Remember we're completing this unit so we can find a boundary if it fails.
nextUnitOfWork = workInProgress;
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
startProfilerTimer(workInProgress);
}
nextUnitOfWork = completeWork(current$$1, workInProgress, nextRenderExpirationTime);
if (workInProgress.mode & ProfileMode) {
// Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
}
} else {
nextUnitOfWork = completeWork(current$$1, workInProgress, nextRenderExpirationTime);
}
if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
// We're out of completion phase so replaying is fine now.
mayReplayFailedUnitOfWork = true;
}
stopWorkTimer(workInProgress);
resetChildExpirationTime(workInProgress, nextRenderExpirationTime);
{
resetCurrentFiber();
}
if (nextUnitOfWork !== null) {
// Completing this fiber spawned new work. Work on that next.
return nextUnitOfWork;
}
if (returnFiber !== null &&
// Do not append effects to parents if a sibling failed to complete
(returnFiber.effectTag & Incomplete) === NoEffect) {
// Append all the effects of the subtree and this fiber onto the effect
// list of the parent. The completion order of the children affects the
// side-effect order.
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = workInProgress.firstEffect;
}
if (workInProgress.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
}
returnFiber.lastEffect = workInProgress.lastEffect;
}
// If this fiber had side-effects, we append it AFTER the children's
// side-effects. We can perform certain side-effects earlier if
// needed, by doing multiple passes over the effect list. We don't want
// to schedule our own side-effect on our own list because if end up
// reusing children we'll schedule this effect onto itself since we're
// at the end.
var effectTag = workInProgress.effectTag;
// Skip both NoWork and PerformedWork tags when creating the effect list.
// PerformedWork effect is read by React DevTools but shouldn't be committed.
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress;
} else {
returnFiber.firstEffect = workInProgress;
}
returnFiber.lastEffect = workInProgress;
}
}
if (true && ReactFiberInstrumentation_1.debugTool) {
ReactFiberInstrumentation_1.debugTool.onCompleteWork(workInProgress);
}
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber. Complete the returnFiber.
workInProgress = returnFiber;
continue;
} else {
// We've reached the root.
return null;
}
} else {
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// Record the render duration for the fiber that errored.
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
// Include the time spent working on failed children before continuing.
var actualDuration = workInProgress.actualDuration;
var child = workInProgress.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
workInProgress.actualDuration = actualDuration;
}
// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
var next = unwindWork(workInProgress, nextRenderExpirationTime);
// Because this fiber did not complete, don't reset its expiration time.
if (workInProgress.effectTag & DidCapture) {
// Restarting an error boundary
stopFailedWorkTimer(workInProgress);
} else {
stopWorkTimer(workInProgress);
}
{
resetCurrentFiber();
}
if (next !== null) {
stopWorkTimer(workInProgress);
if (true && ReactFiberInstrumentation_1.debugTool) {
ReactFiberInstrumentation_1.debugTool.onCompleteWork(workInProgress);
}
// If completing this work spawned new work, do that next. We'll come
// back here again.
// Since we're restarting, remove anything that is not a host effect
// from the effect tag.
next.effectTag &= HostEffectMask;
return next;
}
if (returnFiber !== null) {
// Mark the parent fiber as incomplete and clear its effect list.
returnFiber.firstEffect = returnFiber.lastEffect = null;
returnFiber.effectTag |= Incomplete;
}
if (true && ReactFiberInstrumentation_1.debugTool) {
ReactFiberInstrumentation_1.debugTool.onCompleteWork(workInProgress);
}
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber. Complete the returnFiber.
workInProgress = returnFiber;
continue;
} else {
return null;
}
}
}
// Without this explicit null return Flow complains of invalid return type
// TODO Remove the above while(true) loop
// eslint-disable-next-line no-unreachable
return null;
}
在完成整个workLoop循环后,得出的Fiber树,之后会进入completeUnitOfWork这个函数当中,在completeUnitOfWork中会对会执行一个叫completeWork的函数,因为我们当前传入到completeUnitOfWork中的workInProgress是原生DOM的Fiber对象。
completeUnitOfWork其实也是一个递归的过程,还记的传入到completeUnitOfWork的workInProgress其实是最子级的Fiber吗?所以在completeUnitOfWork中会对workInProgress一顿操作后,将Fiber.return赋值到workInProgress上。直到为null。
所以在执行completeWork中会根据workInProgress.tag进行不同的操作,我们先看看对于原生DOM的Fiber对象会做怎样的操作。
function completeWork(current, workInProgress, renderExpirationTime) {
var newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
break;
case LazyComponent:
break;
case SimpleMemoComponent:
case FunctionComponent:
break;
case ClassComponent:
{
var Component = workInProgress.type;
if (isContextProvider(Component)) {
popContext(workInProgress);
}
break;
}
case HostRoot:
{
popHostContainer(workInProgress);
popTopLevelContextObject(workInProgress);
var fiberRoot = workInProgress.stateNode;
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
if (current === null || current.child === null) {
// If we hydrated, pop so that we can delete any remaining children
// that weren't hydrated.
popHydrationState(workInProgress);
// This resets the hacky state to fix isMounted before committing.
// TODO: Delete this when we delete isMounted and findDOMNode.
workInProgress.effectTag &= ~Placement;
}
updateHostContainer(workInProgress);
break;
}
case HostComponent:
{
popHostContext(workInProgress);
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
if (current.ref !== workInProgress.ref) {
markRef$1(workInProgress);
}
} else {
if (!newProps) {
!(workInProgress.stateNode !== null) ? invariant(false, 'We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue.') : void 0;
// This can happen when we abort work.
break;
}
var currentHostContext = getHostContext();
// TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on we want to add then top->down or
// bottom->up. Top->down is faster in IE11.
var wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {
// If changes to the hydrated node needs to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
} else {
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
// Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance, currentHostContext)) {
markUpdate(workInProgress);
}
workInProgress.stateNode = instance;
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef$1(workInProgress);
}
}
break;
}
case HostText:
{
var newText = newProps;
if (current && workInProgress.stateNode != null) {
var oldText = current.memoizedProps;
// If we have an alternate, that means this is an update and we need
// to schedule a side-effect to do the updates.
updateHostText$1(current, workInProgress, oldText, newText);
} else {
if (typeof newText !== 'string') {
!(workInProgress.stateNode !== null) ? invariant(false, 'We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue.') : void 0;
// This can happen when we abort work.
}
var _rootContainerInstance = getRootHostContainer();
var _currentHostContext = getHostContext();
var _wasHydrated = popHydrationState(workInProgress);
if (_wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(newText, _rootContainerInstance, _currentHostContext, workInProgress);
}
}
break;
}
case ForwardRef:
break;
case SuspenseComponent:
{
var nextState = workInProgress.memoizedState;
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
// Something suspended. Re-render with the fallback children.
workInProgress.expirationTime = renderExpirationTime;
// Do not reset the effect list.
return workInProgress;
}
var nextDidTimeout = nextState !== null;
var prevDidTimeout = current !== null && current.memoizedState !== null;
if (current !== null && !nextDidTimeout && prevDidTimeout) {
// We just switched from the fallback to the normal children. Delete
// the fallback.
// TODO: Would it be better to store the fallback fragment on
var currentFallbackChild = current.child.sibling;
if (currentFallbackChild !== null) {
// Deletions go at the beginning of the return fiber's effect list
var first = workInProgress.firstEffect;
if (first !== null) {
workInProgress.firstEffect = currentFallbackChild;
currentFallbackChild.nextEffect = first;
} else {
workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChild;
currentFallbackChild.nextEffect = null;
}
currentFallbackChild.effectTag = Deletion;
}
}
// The children either timed out after previously being visible, or
// were restored after previously being hidden. Schedule an effect
// to update their visiblity.
if (
//
nextDidTimeout !== prevDidTimeout ||
// Outside concurrent mode, the primary children commit in an
// inconsistent state, even if they are hidden. So if they are hidden,
// we need to schedule an effect to re-hide them, just in case.
(workInProgress.effectTag & ConcurrentMode) === NoContext && nextDidTimeout) {
workInProgress.effectTag |= Update;
}
break;
}
case Fragment:
break;
case Mode:
break;
case Profiler:
break;
case HostPortal:
popHostContainer(workInProgress);
updateHostContainer(workInProgress);
break;
case ContextProvider:
// Pop provider fiber
popProvider(workInProgress);
break;
case ContextConsumer:
break;
case MemoComponent:
break;
case IncompleteClassComponent:
{
// Same as class component case. I put it down here so that the tags are
// sequential to ensure this switch is compiled to a jump table.
var _Component = workInProgress.type;
if (isContextProvider(_Component)) {
popContext(workInProgress);
}
break;
}
default:
invariant(false, 'Unknown unit of work tag. This error is likely caused by a bug in React. Please file an issue.');
}
return null;
}
我们会看到大量的判断,对不同不同类型的Fiber进行不同的处理,因为当前传入的Fiber是原生DOM对象,所以会case == HostComponent。最终会执行一个createInstance的函数,该函数最终返回出来的是一个原生DOM对象。
最终会赋值到Fiber中的stateNode中。那么我们回到completeUnitOfWork这个函数中。
在completeUnitOfWork中一开始会对传入的workInProgress.return存在returnFiber变量中。
因为当前的returnFiber是H1这个函数的Fiber,所以会继续循环completeUnitOfWork,将returnFiber赋值到workInProgress中。
这一次的Fiber是一个H1函数,所以进入到completeWork函数中的时候会和原本的原生DOM不一样,进入的是ClassComponent的判断中。
再一次进入递归,这一次是H1的Fiber的return。是一个App函数。中间的过程其实和H1函数Fiber是一样的。其实从这里我们就可以知道,无论你的组件中包含多少个组件都是会以这种方式一层一层执行的。
如果我们一个组件当中存在一个兄弟组件,那么递归的方式将会以一种深度优先的方式,先将一个组件的所有子组件都完成构建后,才会返回出来进行下一个兄弟组件的构建。
现在我们的递归已经来到的最后一次,就是根Fiber的构建。这个时候根Fiber的return为null了。那么就说明这应该就是最后一次递归了。
这个时候进入的completeWork中执行的将会是HostRoot。
发现最后执行的updateHostContainer是一个空函数,是不是以后会有文章呢?
因为根的Fiber的siblingFiber和returnFiber都是为null,所以结束这个递归了。然后return了一个null回到performUnitOfWork函数中,在performUnitOfWork中有以下的代码,将completeUnitOfWork返回的值复制给next。
这个时候next为null,然后completeUnitOfWork最后返回next到workLoop函数中。
这个时候nextUnitOfWork就等于了null,所以workLoop的循环也接受了。
这里经过了几篇文章的阅读,如果对于代码有点遗忘的话,可能需要反复阅读一下代码,或者看看之前的文章重温一下。
进过一层一层的返回,回到renderRoot函数中。还记得在renderRoot是如何进入workLoop的吗?
然后结束了do...while之后最后去执行onComplete函数。
因为之前一直说的各种递归,各种修改属性,都是对引用类型的修改,所以在renderRoot中的rootWorkInProgress就是最终的根Fiber以及所有子级Fiber的Fiber树。
function onComplete(root, finishedWork, expirationTime) {
root.pendingCommitExpirationTime = expirationTime;
root.finishedWork = finishedWork;
}
对root进行一些赋值。之后renderRoot也执行完毕了,返回到上一级performWorkOnRoot函数中。温故而知新一下,现在经过onComplete函数的赋值后,root.finishedWork就是根Fiber了,可以进入到completeRoot函数中了。
function completeRoot(root, finishedWork, expirationTime) {
// Check if there's a batch that matches this expiration time.
var firstBatch = root.firstBatch;
if (firstBatch !== null && firstBatch._expirationTime >= expirationTime) {
if (completedBatches === null) {
completedBatches = [firstBatch];
} else {
completedBatches.push(firstBatch);
}
if (firstBatch._defer) {
// This root is blocked from committing by a batch. Unschedule it until
// we receive another update.
root.finishedWork = finishedWork;
root.expirationTime = NoWork;
return;
}
}
// Commit the root.
root.finishedWork = null;
// Check if this is a nested update (a sync update scheduled during the
// commit phase).
if (root === lastCommittedRootDuringThisBatch) {
// If the next root is the same as the previous root, this is a nested
// update. To prevent an infinite loop, increment the nested update count.
nestedUpdateCount++;
} else {
// Reset whenever we switch roots.
lastCommittedRootDuringThisBatch = root;
nestedUpdateCount = 0;
}
commitRoot(root, finishedWork);
}
发现React很喜欢全局变量呀。就是一些赋值之后就执行commitRoot了。开始渲染真实的DOM了。
变量名:finishedWork == 根Fiber
首先会将finishedWork.firstEffect赋值到firstEffect上,接着将firstEffect赋值到全局的nextEffect上。nextEffect是什么东西?
nextEffect现在是App的Fiber对象。之后会进入一个叫commitAllHostEffects的函数中。
在commitAllHostEffects函数中会调用commitPlacement函数,而commitPlacement函数中最终会调用appendChildToContainer函数将原生DOM插入到页面中渲染出来。
到这里其实大概就是第一次渲染的源码阅读过程,当然中间其实没有去说生命周期的调用。在下一篇文章中,将会总结出第一篇到第五的总结内容。敬请期待!