背景概述
React 原理分析(一) —— React 设计思想中说到说到,为了实现 React Diff 阶段的异步可中断 。Fiber
将 React
的一次更新流程拆成了两个阶段:
- Render 阶段: 异步可中断的 diff 新老 DOM,找到差异后并不及时立刻更新。而是对该 Fiber 节点打上一个
tag
(Update/Placement/Delete)。 - Commit 阶段: 遍历存在 tag 的 Fiber ,根据
tag
的类型执行对应的 DOM 更新。
本文重点讲解更新流程中 render
阶段的全流程 ,在 React
中,render
流程中可以被拆分为 beginWork
和 completeWork
两个阶段。这里分别介绍一下两个流程的工作。
流程概览
调度时机
如React 原理分析(一) —— React 设计思想 所说,React 每一次 state 更新 都会生成一个 Update 加入任务队列中 。 最终 Schedule 会根据任务队列对应优先级行任务调度。示意流程如下:
React
中每一个 Update
都会经历 Render
和 Commit
的流程。Render
阶段主要完成fiber
节点的创建/变化并打上对应的tag
。 Render
会从 rootFiber
节点开始进行深度优先遍历, 深度优先的过程中 递阶段 执行BeginWork
函数, 归阶段 执行CompleteWork
函数。
export default function App() {
return (
<div className="App">
<div>
hello
<span> world </span>
</div>
</div>
);
}
复制代码
上方 App
组件生成的对应的 Fiber 节点如下 :
对应 Fiber 的 BeginWork/CompleteWork
调度顺序如下:
- Root 节点的 BeginWork 阶段
- App 节点的 BeginWork 阶段
- div 节点的 BeginWork 阶段
- div 节点的 CompleteWork 阶段
- span 节点的 BeginWork 阶段
- span 节点的 CompleteWork 阶段 (React 内部的优化,如果只有一个 Text 结点跳过 BeginWork/CompleteWork)
- div 节点的 CompleteWork 阶段
- App 节点的 CompleteWork 阶段
- Root 节点的 CompleteWork 阶段
BeginWork
在整个更新流程中,React
从 FiberRoot(Root)
开始深度遍历。对每一个Fiber
节点执行一次beginWork
。最终会返回一个Child Fiber
。beginWork
总体概括如下:
React
根据来对workInProgress
节点和对应的current
节点进行Diff
并打上对应的 Flag
。
BeginWork 函数主流程
BeginWork
方法执行于 performUnitOfWork
函数中。具体逻辑如下:
// 根据双缓存机制, current 为当前页面渲染的 Fiber 结点,在 mount 阶段时为 null。workInProgress 为本次需要渲染在 浏览器上的 Dom 结点
// renderLanes 为本次更新的优先级, 优先级相关内容在后期会讲述到。在这里只需要知道就好了
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// 获取本次 Update 的优先级
const updateLanes = workInProgress.lanes;
// 根据 current 判断是 mount 还是 update
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasLegacyContextChanged()) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
// Fiber Update 优先级 与 本次调度 Update 优先级不符合
didReceiveUpdate = false;
...
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
} else {
didReceiveUpdate = false;
}
workInProgress.lanes = NoLanes;
// 根据 Fiber 类型,走向不同的分支
switch (workInProgress.tag) {
// 一个函数组件,第一次 mount 时会走到 IndeterminateComponent(匿名组件逻辑中),
// 执行完后,tag 会根据是否存在render函数判断是否为 Class 组件和 Function 组件
case IndeterminateComponent: { // 类型未知的组件
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case FunctionComponent: { // 函数 组件
...
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent: { // Class 组件
...
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot: // FiberRoot 节点
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent: // DOM节点对应的 Fiber
return updateHostComponent(current, workInProgress, renderLanes);
case HostText: // Text 节点
return updateHostText(current, workInProgress);
}
}
复制代码
根据 current
是否存在判断是 Mount
阶段还是 Update
阶段
根据React的双缓存机制可知 current Fiber
会储存当前页面渲染的 Fiber
节点。 如果该节点为空。则表示当前页面没有对应的 DOM节点
, 为 Mount 阶段, 否则为 Update 阶段。
判断节点是否需要复用
React 内部根据 didReceiveUpdate
判断节点是否需要复用。 如果该变量为 false
。 则直接复用之前 Fiber 的 DOM节点
。
根据 tag 走不同的挂载流程
beginWork
最终会根据不同的 tag 走向不同的分支逻辑, 这些函数的主要逻辑大同小异,主要逻辑都是创建对应的 Child Fiber
,并为当前节点/子节点打上 Placement/Deletion/Update
标记该Fiber
需要 放置/删除/更新。
挂载 函数组件 流程
这里以 IndeterminateComponent
类型为例。当首次挂载函数组件时,函数组件的tag
为IndeterminateComponent
。 在 mountIndeterminateComponent
中,会执行函数组件的逻辑,生成一个 Fiber
及其对应的DOM
结构。
function mountIndeterminateComponent(
_current,
workInProgress,
Component,
renderLanes,
) {
// 如果是 mount 阶段,将当前 Fiber 的 alternate 清空。
// 给当前 Fiber 打上 Placement 的标记。
if (_current !== null) {
_current.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.flags |= Placement;
}
const props = workInProgress.pendingProps;
let value;
// 执行 函数组件, 获取对应的 DOM 结构
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes,
)
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
// 将 tag 置成 FunctionComponent
workInProgress.tag = FunctionComponent;
// Diff 算法,为 Child Fiber 打上 Placement/Update/Delete 的Flag
reconcileChildren(null, workInProgress, value, renderLanes);
// 返回下一个需要 beginWork 的 Fiber 节点, 如果为 null 表示当前
// Fiber 没有子节点可以继续 beginWork 了, 则从该节点开始 beginWork
return workInProgress.child;
}
复制代码
其余的 updateFunctionComponent
/ updateClassComponent
逻辑类似,不再解释。
CompleteWork
BeginWork
结束时,会返回当前 Fiber
节点的 child
节点。如果 child
节点为 null
时,说明当前节点不能再向下遍历了,这时便会执行当前节点的 completeUnitOfWork
逻辑。 completeWork
函数的主要作用如下:
- 针对
type
为HostRoot
,HostComponent
,HostText
等存在真实DOM
的Fiber
节点。将其DOM
依次从子节点插入父节点的DOM
中, 并赋值stateNode
属性。 - 将存在
Placement / Deletion / Update
等tag
的串成一个链表, 方便commit
阶段对Fiber
进行处理。
function performUnitOfWork(unitOfWork: Fiber): void {
// 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.
const current = unitOfWork.alternate;
let next;
next = beginWork(current, unitOfWork, subtreeRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 如果是 Fiber 树的叶子结点, 则执行 completeWork
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
复制代码
DOM 结构的创建与插入
completeWork
会将字节点的 DOM
依次插入父节点的 DOM
中,并保存在 stateNode
属性中。并在 commit
阶段将其渲染在浏览器上。代码如下:
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// Update 阶段, 更新 Fiber
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// 创建 当前 Fiber 对应的 DOM 节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将 子节点对应的 DOM 节点加入当前 Fiber 中
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
return null;
}
// 将 workInPrgress 的节点挂载在 parent 对应的 DOM 节点上
const appendAllChildren = function(
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node !== null) {
// 如果当前 Fiber 是 HostComponent 或者 HostText 节点,插入 parent 节点中
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
// 如果是一个 函数组件
appendInitialChild(parent, node.stateNode.instance);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.child !== null) {
// 递归
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
// 处理兄弟节点
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
};
复制代码
如下面 App 组件,其对应的 Fiber 的 Fiber 中挂载的 DOM 如下。
export default function App() {
return (
<div className="App">
<div>
hello
<span> world </span>
</div>
</div>
);
}
复制代码
EffectList 的建立
在beginWork
中,React
给所有需要变动的 Fiber
打上了对应的flag
。这些flag
会在commit
阶段中被消费。为了更好的对Fiber
中的flag
进行消费。 React
将其Child Fiber
的flag
挂在当前Fiber
的 subtreeFlags
。 subtreeFlags
表示,如果当前Fiber
的子节点有存在的flag
,subtreeFlags
就会存在对应的值。
function bubbleProperties(completedWork: Fiber) {
let subtreeFlags = NoFlags;
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;
child.return = completedWork;
child = child.sibling;
}
completedWork.subtreeFlags |= subtreeFlags;
}
复制代码
经过这样的处理后,在commit
阶段只需要通过Fiber
上的 subtreeFlags
属性便可知道,当前Fiber
下是否是有Child Fiber
存在flags
。提高了commit
阶段的效率。
render 渲染 Error 组件
Error 组件处理流程
在上面提到, React
的组件会在beginWork
阶段执行。
从下方源码中可以看到,在 dev
阶段,当 originalBeginWork
执行失败时会走到catch
重置属性,并重新执行一次beginWork
函数。
beginWork = (current, unitOfWork, lanes) => {
// 复制当前 Fiber , 如果 beginWork 出错了。 则用复制的 Fiber 进行 reset
const originalWorkInProgressCopy = assignFiberPropertiesInDEV(
dummyFiber,
unitOfWork,
);
try {
return originalBeginWork(current, unitOfWork, lanes);
} catch (originalError) {
// Keep this code in sync with handleError; any changes here must have
// corresponding changes there
resetContextDependencies();
resetHooksAfterThrow();
// Don't reset current debug fiber, since we're about to work on the
// same fiber again.
// Unwind the failed stack frame
unwindInterruptedWork(unitOfWork, workInProgressRootRenderLanes);
// Restore the original properties of the fiber.
assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
// Run beginWork again.
invokeGuardedCallback(
null,
originalBeginWork,
null,
current,
unitOfWork,
lanes,
);
// We always throw the original error in case the second render pass is not idempotent.
// This can happen if a memoized function or CommonJS module doesn't throw after first invocation.
throw originalError;
}
};
复制代码
如果再一次执行 Fiber
的 beginWork
阶段失败,则会被外层的try ... Catch
捕获住并被handleError
函数进行处理。handleError
最终会走到
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
function handleError(root, thrownValue): void {
do {
let erroredWork = workInProgress;
try {
// Reset module-level state that was set during the render phase.
resetContextDependencies();
resetHooksAfterThrow();
resetCurrentDebugFiberInDEV();
// TODO: I found and added this missing line while investigating a
// separate issue. Write a regression test using string refs.
ReactCurrentOwner.current = null;
// 对 root Fiber 的特殊处理, 因为 ErrorBoundary 只能对子组件进行处理
if (erroredWork === null || erroredWork.return === null) {
// Expected to be working on a non-root fiber. This is a fatal error
// because there's no ancestor that can handle it; the root is
// supposed to capture all errors that weren't caught by an error
// boundary.
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
workInProgress = null;
return;
}
// 最终会走到 throwException
throwException(
root,
erroredWork.return,
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
completeUnitOfWork(erroredWork);
} catch (yetAnotherThrownValue) {
// 如果抛出了其他异常,则一直循环。直到处理完成
thrownValue = yetAnotherThrownValue;
if (workInProgress === erroredWork && erroredWork !== null) {
// If this boundary has already errored, then we had trouble processing
// the error. Bubble it to the next boundary.
erroredWork = erroredWork.return;
workInProgress = erroredWork;
} else {
erroredWork = workInProgress;
}
continue;
}
// Return to the normal work loop.
return;
} while (true);
}
复制代码
throwException
会为这个出错的Fiber
打上Imcomplete
的effect flags
。表明该Fiber
的render
阶段未完成。- 将
workInProgressRootExitStatus
置为RootErrored
在所有Fiber
的 - 同时会向上查找最近存在
getDerivedStateFromError
或componentDidCatch
的Class Component
(下文称ErrorBoundary
组件)作为错误边界。找到后打上ShouldCapture
的flag
表明该组件需要进行错误处理。 - 基于
ErrorBoundary
组件创建一个update
,getDerivedStateFromError
为update
的payload
,componentDidCatch
为callback
,最终入队ErrorBoundary
组件的updateQueue
中。 - 跳出该节点的
beginWork
阶段, 进入completeWork
阶段。
function throwException(
root: FiberRoot,
returnFiber: Fiber,
sourceFiber: Fiber,
value: mixed,
rootRenderLanes: Lanes,
) {
// The source fiber did not complete.
sourceFiber.flags |= Incomplete;
if (
value !== null &&
typeof value === 'object' &&
typeof value.then === 'function'
) {
const wakeable: Wakeable = (value: any);
resetSuspendedComponent(sourceFiber, rootRenderLanes);
renderDidError();
value = createCapturedValue(value, sourceFiber);
let workInProgress = returnFiber;
do {
switch (workInProgress.tag) {
case HostRoot: {
const errorInfo = value;
workInProgress.flags |= ShouldCapture;
const lane = pickArbitraryLane(rootRenderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
enqueueCapturedUpdate(workInProgress, update);
return;
}
case ClassComponent:
// Capture and retry
const errorInfo = value;
const ctor = workInProgress.type;
const instance = workInProgress.stateNode;
if (
(workInProgress.flags & DidCapture) === NoFlags &&
(typeof ctor.getDerivedStateFromError === 'function' ||
(instance !== null &&
typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance)))
) {
workInProgress.flags |= ShouldCapture;
const lane = pickArbitraryLane(rootRenderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
// Schedule the error boundary to re-render using updated state
const update = createClassErrorUpdate(
workInProgress,
errorInfo,
lane,
);
enqueueCapturedUpdate(workInProgress, update);
return;
}
break;
default:
break;
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
}
复制代码
最后就是 Fiber
的 completeWork
阶段了,需要关注的是此时 Fiber
的 flag
为 Incomplete
。这里会向上找到最近的 ErrorBoundary
组件并从该节点开始从新执行新的 beginWork
逻辑来产生 fallback
组件。
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// Check if the work completed or if something threw.
if ((completedWork.flags & Incomplete) === NoFlags) {
} else {
// 如果 completedWork 为能处理错误的组件,则 next 为该组件,否则为 null
const next = unwindWork(completedWork, subtreeRenderLanes);
// 因为该组件可以处理 Error ,将其设置为 workInProgress Fiber
if (next !== null) {
next.flags &= HostEffectMask;
workInProgress = next;
return;
}
// 将其父节点设置为 Incomplete ,直到找到可以处理 Error 的组件。
if (returnFiber !== null) {
// Mark the parent fiber as incomplete and clear its subtree flags.
returnFiber.flags |= Incomplete;
returnFiber.subtreeFlags = NoFlags;
returnFiber.deletions = null;
}
}
// 如果存在兄弟节点,则将兄弟节点设置为 workInProgress Fiber
// 否则将父节点设置为下一个 workInProgress 的组件
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// Otherwise, return to the parent
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
}
复制代码
流程梳理
以下面这个 Fiber
结构为例, ErrorBoundary
组件可以处理 Error
,其中App
组件会抛出一个错误。整个渲染流程如下
- 从
Root Fiber
开始向下深度优先进行beiginWork
的处理 - 处理到
App Fiber
时抛出一个Error
- 重新在
App Fiber
进行beginWork
处理,再次抛出Error
- 为
App Fiber
打上Incomplete
的flag
, 找到最近的可以处理Error
的组件ErrorBoudary
,打上shouldCaptrue
的flag
。并为该Fiber
添加上一个payload
为getDerivedStateFromError
的update
。 App
组件执行completeWork
, 向上找到ErrorBoundary
组件,ErrorBoundary
组件处理updateQueue
获取新的state
。根据state
更新到fallback UI
。- 重新为
fallback UI
执行beginWork
,按正常流程进行render
阶段处理 - 进行
commit
阶段,浏览器渲染fallback UI
和对应的DOM