前言:
在React源码解析之renderRoot概览中,提到了renderRoot()
会调用workLoop()/workLoopSync()进行循环单元的更新:
function renderRoot(){
xxx
xxx
if (workInProgress !== null) {
//执行每个节点的更新
if (isSync) {
workLoopSync();
} else {
//判断是否需要继续调用performUnitOfWork
workLoop();
}
}
xxx
xxx
}
本文就来讲解workLoop
及其内部的方法逻辑
注意:
本文涉及到 fiber 对象的部分属性,请参考:React源码解析之RootFiber
一、workLoop
作用:
循环执行performUnitOfWork
并赋值给workInProgress
,直到workInProgress
值为空,则中止循环
源码:
//同步的 workLoop,说明是不可以被中断的
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
}
//异步的 workLoop,说明是可以被中断的
//判断是否需要继续调用performUnitOfWork
function workLoop() {
// Perform work until Scheduler asks us to yield
/*nextUnitOfWork =》workInProgress*/
//未到达根节点时
//有workInProgress.child的时候,一直循环,直到所有节点更新完毕
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
解析:
workInProgress 是一个 fiber 对象,它值不为 null 意味着该 fiber 对象上仍然有更新要执行
二、performUnitOfWork
作用:
调用beginWork
,从父至子,进行组件(节点)更新;
调用completeUnitOfWork
,从子至父,根据 effectTag,对节点进行一些处理
源码:
//从上至下遍历、操作节点,至底层后,再从下至上,根据effectTag 操作节点
//unitOfWork 即 workInProgress,是一个 fiber 对象
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
// 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.
//current <=> workInProgress
//获取当前节点
const current = unitOfWork.alternate;
//在unitOfWork上做个标记,不看
startWorkTimer(unitOfWork);
//dev 环境,不看
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
//进行节点操作,并创建子节点
//current: workInProgress.alternate
//unitOfWork: workInProgress
//workInProgress.child
//判断fiber有无更新,有更新则进行相应的组件更新,无更新则复制节点
next = beginWork(current, unitOfWork, renderExpirationTime);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, renderExpirationTime);
}
//不看
resetCurrentDebugFiberInDEV();
//将待更新的 props 替换成正在用的 props
unitOfWork.memoizedProps = unitOfWork.pendingProps;
//说明已经更新到了最底层的叶子节点,并且叶子节点的兄弟节点也已经遍历完
if (next === null) {
// If this doesn't spawn new work, complete the current work.
//当从上到下遍历完成后,completeUnitOfWork 会从下到上根据effact tag进行一些处理
next = completeUnitOfWork(unitOfWork);
}
ReactCurrentOwner.current = null;
return next;
}
解析:
(1) 获取当前节点current
(2) 执行beginWork(current, unitOfWork, renderExpirationTime)
进行组件的更新,返回的值赋给next
(3) 将unitOfWork
上待更新的 props 替换成正在用的 props
(4) 如果next
为 null,则执行completeUnitOfWork
,从下到上遍历,并根据 effectTag 进行一些处理
(5) 最终返回next
(next 为 fiber 对象)
看下beginWork()
。completeUnitOfWork()
会在后面文章中解析。
三、beginWork
注意:
switch...case... 那超长的两段直接跳过
作用:
判断fiber
有无更新,有更新则进行相应的组件更新,无更新则复制节点
源码:
//判断fiber有无更新,有更新则进行相应的组件更新,无更新则复制节点
//current: workInProgress.alternate
function beginWork(
current: Fiber | null,
//workInProgress创建的子节点也是workInProgress
workInProgress: Fiber,
//标记 该次渲染中,优先级最高的点
renderExpirationTime: ExpirationTime,
): Fiber | null {
//只有当调用 react.domRender的时候,rootFiber的expirationTime才有值,rootFiber 才会更新
//获取 fiber 对象上更新的过期时间
const updateExpirationTime = workInProgress.expirationTime;
//判断是不是第一次渲染
//如果不是第一次渲染
if (current !== null) {
//上一次渲染完成后的props,即 oldProps
const oldProps = current.memoizedProps;
//新的变动带来的props,即newProps
const newProps = workInProgress.pendingProps;
if (
//前后 props 是否不相等
oldProps !== newProps ||
//是否有老版本的 context 使用,并且发生了变化
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
//开发环境永远是 false
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// 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).
//判断接收到了更新 update
didReceiveUpdate = true;
}
//有更新,但是优先级不高,在本次渲染过程中不需要执行,设为 false
else if (updateExpirationTime < renderExpirationTime) {
didReceiveUpdate = false;
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.
//根据workInProgress的tag,进行相应组件的更新
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
resetHydrationState();
break;
case HostComponent:
pushHostContext(workInProgress);
if (
workInProgress.mode & ConcurrentMode &&
renderExpirationTime !== Never &&
shouldDeprioritizeSubtree(workInProgress.type, newProps)
) {
if (enableSchedulerTracing) {
markSpawnedWork(Never);
}
// Schedule this fiber to re-render at offscreen priority. Then bailout.
workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
return null;
}
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
case HostPortal:
pushHostContainer(
workInProgress,
workInProgress.stateNode.containerInfo,
);
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
pushProvider(workInProgress, newValue);
break;
}
case Profiler:
if (enableProfilerTimer) {
workInProgress.effectTag |= Update;
}
break;
case SuspenseComponent: {
const state: SuspenseState | null = workInProgress.memoizedState;
const didTimeout = state !== null;
if (didTimeout) {
// If this boundary is currently timed out, we need to decide
// whether to retry the primary children, or to skip over it and
// go straight to the fallback. Check the priority of the primary
// child fragment.
const primaryChildFragment: Fiber = (workInProgress.child: any);
const primaryChildExpirationTime =
primaryChildFragment.childExpirationTime;
if (
primaryChildExpirationTime !== NoWork &&
primaryChildExpirationTime >= renderExpirationTime
) {
// The primary children have pending work. Use the normal path
// to attempt to render the primary children again.
return updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
);
} else {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
// The primary children do not have pending work with sufficient
// priority. Bailout.
const child = bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
if (child !== null) {
// The fallback children have pending work. Skip over the
// primary children and work on the fallback.
return child.sibling;
} else {
return null;
}
}
} else {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
}
break;
}
case DehydratedSuspenseComponent: {
if (enableSuspenseServerRenderer) {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
// We know that this component will suspend again because if it has
// been unsuspended it has committed as a regular Suspense component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.effectTag |= DidCapture;
}
break;
}
case SuspenseListComponent: {
const didSuspendBefore =
(current.effectTag & DidCapture) !== NoEffect;
const childExpirationTime = workInProgress.childExpirationTime;
if (childExpirationTime < renderExpirationTime) {
// If none of the children had any work, that means that none of
// them got retried so they'll still be blocked in the same way
// as before. We can fast bail out.
pushSuspenseContext(workInProgress, suspenseStackCursor.current);
if (didSuspendBefore) {
workInProgress.effectTag |= DidCapture;
}
return null;
}
if (didSuspendBefore) {
// If something was in fallback state last time, and we have all the
// same children then we're still in progressive loading state.
// Something might get unblocked by state updates or retries in the
// tree which will affect the tail. So we need to use the normal
// path to compute the correct tail.
return updateSuspenseListComponent(
current,
workInProgress,
renderExpirationTime,
);
}
// If nothing suspended before and we're rendering the same children,
// then the tail doesn't matter. Anything new that suspends will work
// in the "together" mode, so we can continue from the state we had.
let renderState = workInProgress.memoizedState;
if (renderState !== null) {
// Reset to the "together" mode in case we've started a different
// update in the past but didn't complete it.
renderState.rendering = null;
renderState.tail = null;
}
pushSuspenseContext(workInProgress, suspenseStackCursor.current);
break;
}
case EventComponent:
if (enableFlareAPI) {
pushHostContextForEventComponent(workInProgress);
}
break;
}
//跳过该节点及所有子节点的更新
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
} else {
didReceiveUpdate = false;
}
// Before entering the begin phase, clear the expiration time.
workInProgress.expirationTime = NoWork;
//如果节点是有更新的
//根据节点类型进行组件的更新
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderExpirationTime,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
);
}
//FunctionComponent的更新
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,
renderExpirationTime,
);
}
//ClassComponent的更新
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,
renderExpirationTime,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
);
case HostPortal:
return updatePortalComponent(
current,
workInProgress,
renderExpirationTime,
);
case ForwardRef: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
return updateForwardRef(
current,
workInProgress,
type,
resolvedProps,
renderExpirationTime,
);
}
case Fragment:
return updateFragment(current, workInProgress, renderExpirationTime);
case Mode:
return updateMode(current, workInProgress, renderExpirationTime);
case Profiler:
return updateProfiler(current, workInProgress, renderExpirationTime);
case ContextProvider:
return updateContextProvider(
current,
workInProgress,
renderExpirationTime,
);
case ContextConsumer:
return updateContextConsumer(
current,
workInProgress,
renderExpirationTime,
);
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
const outerPropTypes = type.propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
resolvedProps, // Resolved for outer only
'prop',
getComponentName(type),
getCurrentFiberStackInDev,
);
}
}
}
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
updateExpirationTime,
renderExpirationTime,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
updateExpirationTime,
renderExpirationTime,
);
}
case IncompleteClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
case DehydratedSuspenseComponent: {
if (enableSuspenseServerRenderer) {
return updateDehydratedSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
);
}
break;
}
case SuspenseListComponent: {
return updateSuspenseListComponent(
current,
workInProgress,
renderExpirationTime,
);
}
case EventComponent: {
if (enableFlareAPI) {
return updateEventComponent(
current,
workInProgress,
renderExpirationTime,
);
}
break;
}
}
invariant(
false,
'Unknown unit of work tag. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
}
解析:
(1) fiber 对象只要有更新,就会计算出一个 expirationTime
(2) memoizedProps
和pendingProps
是fiber
对象的属性,具体请看:React源码解析之RootFiber
(3) switch...case
那段太长了,可跳过,后面会讲FunctionComponent
情况时,组件是如何更新的
(4) 注意updateExpirationTime < renderExpirationTime
的情况,它的意思是当前 fiber 的更新优先级不高,在本次渲染的过程中不会执行它的更新,所以会执行bailoutOnAlreadyFinishedWork
,来跳过该节点及所有子节点的更新,不再往下执行组件的更新
(5) 最后根据workInProgress.tag
的不同情况,来进行组件的更新
四、bailoutOnAlreadyFinishedWork
作用:
跳过该节点及该节点上所有子节点的更新
源码:
//跳过该节点及所有子节点的更新
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
//不看
cancelWorkTimer(workInProgress);
if (current !== null) {
// Reuse previous dependencies
workInProgress.dependencies = current.dependencies;
}
if (enableProfilerTimer) {
// Don't update "base" render times for bailouts.
stopProfilerTimerIfRunning(workInProgress);
}
// Check if the children have any pending work.
//expirationTime 表示该节点是否有更新,如果该节点有更新,可能会影响子节点的更新
//如果expirationTime和childExpirationTime都没有,则子树是不需要更新的
//由于子孙节点造成的更新
const childExpirationTime = workInProgress.childExpirationTime;
//如果子树不需要更新,则返回 null
//childExpirationTime的一个好处就是快捷地知道子树有没有更新,从而跳过没有更新的子树
//如果childExpirationTime为空,react 还需要遍历子树来判断是否更新
if (childExpirationTime < renderExpirationTime) {
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
//跳过整个子树的更新渲染,这是一个非常大的优化
return null;
}
//调和子节点
else {
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
//该节点不需要更新,子节点也不需要更新,所以只要复制子节点过来即可
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}
解析:
通常判断子节点的更新是要遍历子树来获取信息的,但 React 非常聪明地在子节点产生更新的时候,设置上 childExpirationTime,并最终在父节点上设置一个优先级最高的 childExpirationTime,这样的话,如果childExpirationTime
优先级小于renderExpirationTime
的话,则跳过子树的遍历及更新渲染。
这是一个非常大的优化。
五、completeUnitOfWork
completeUnitOfWork
跟beginWork
的结构是类似的,但我会放在后面的文章去讲
六、workLoop流程图
七、GitHub
https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberWorkLoop.js
(完)