构建阶段
在前面的文章中我们讲到了 Schedule 调度,在文章中讲到了 scheduleUpdateOnFiber(...) 函数是 React 应用处理更新的入口,无论是首次渲染,还是后续更新操作,都会进入到该函数,而首次渲染是经过 updateContainer(...) 函数然后在该函数体中调用 scheduleUpdateOnFiber(...) 函数,而在该函数中又调用了 ensureRootIsScheduled(...) 函数,如果是同步任务,会调用 performSyncWorkOnRoot(...) 函数,如果并发模式下会调用 performConcurrentWorkOnRoot() 函数。
首先我们看看 performSyncWorkOnRoot(...) 函数,该函数源码看起来很多,初次构建中真正的用到的却不是很多。
该函数是 reconciler 阶段的所有的执行入口,首次渲染将进入 renderRootSync,该函数具体代码如下所示:
function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
// 如果fiberRoot变动, 或者update.lane变动, 都会刷新栈帧, 丢弃上一次渲染进度
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
movePendingFibersToMemoized(root, lanes);
}
}
workInProgressTransitions = getTransitionsForLanes(root, lanes);
prepareFreshStack(root, lanes);
}
if (enableSchedulingProfiler) {
markRenderStarted(lanes);
}
do {
try {
// 以同步的方式开始构建 fiber 树
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);
if (enableSchedulingProfiler) {
markRenderStopped();
}
// 重置全局变量, 表明render结束
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}
在该函数中主要创建了 HostRootFiber.alternate,重置全局变量 workInProgress 和 workInProgressRoot 等,并且会在该函数调用 workLoopSync(...) 函数,从此进入循环阶段。
循环构造
workLoopSync
代码逻辑来到了 workLoopSync() 函数里,该函数执行一个 while 循环,如果 workInProgress 不为空,会循环调用 performUnitOfWork(workInProgress),该函数的具体代码如下所示:
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
performUnitOfWork
最终无论是同步执行任务,还是可中断地执行任务,都会进入 performUnitOfWork 函数中,该函数会以 fiber 作为单元,进行协调过程,每次 beginWork 执行后都会更新 workingProgress,直到 fiber 树遍历完成之后,workingProgress 此时设置为 null,执行 completeUnitOfWork(...) 函数,表明 fiber 树构建完成,performUnitOfWork 该函数的具体实现如下:
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 如果没有子节点,表示当前的 fiber 已经完成了
completeUnitOfWork(unitOfWork);
} else {
// 如果有子节点,就让子节点称为下一个工作单元
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
探寻阶段 beginWork
- 根据
ReactElement对象创建所有的fiber节点,最终构造出fiber属性结构,并设置return和sibling指针,例如这个玩意:
- 设置
fiber.flags,该值是一个二进制形式变量,用来标记fiber节点的增、删、改状态,等待completeWork阶段处理; - 设置
fiber.stateNode局部状态。
在 beginWork 函数中有很多 updateXXX 函数,例如 updateHostRoot,updateClassComponent等,虽然case较多,但是主要逻辑可以概括为三个步骤:
-
获取
fiber.pendingProps,pendingProps是函数初始化时设置的props,和fiber.updateQueue等输入状态,计算fiber.memoizedState(memoizedProps是函数执行结束时props的值)作为输出状态; -
获取夏季
ReactElement对象:-
Class类型的 fiber 阶段:
- 构建
React.Component实例; - 把新实例挂载到
fiber.stateNode上; - 执行
render之前的生命周期函数; - 执行
render方法,获取夏季reactElement; - 根据实际情况,设置
fiebr.flags;
- 构建
-
function 类型的
fiber节点:- 执行function,获取下级
reactElement; - 根据实际情况,设置
fiber.flags;
- 执行function,获取下级
-
HostComponent 类型(如:
div, span, button等)的fiber节点:pendingProps.children作为下级reactElement;- 如果下级节点是文本节点,则设计下级节点为null,准备进入
completeUnitOfWork阶段; - 根据实际情况,设置
fiber.flags;
-
-
根据
ReactElement对象,调用reconcileChildren生成fiber子节点;
回溯阶段 completeWork
当 beginWork 已经创建完成 fiber 节点时,会将该 fiber 节点赋值给 next,如果为 null,说明没有子节点了,表示当前的 fiber 节点已经完成了,会调用 completeUnitOfWork(...)。
completeUnitOfWork
该函数是在 beginWork 执行到第一个叶子节点的时候开始执行的,从子到父,构建其余节点的 fiber 树。
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
// 获取备份节点
// 初始化渲染非根 fiber 对象没有备份节点,所以 current 为 null
const current = completedWork.alternate;
// 父级 fiber 树,非根 fiber 对象都有父级
const returnFiber = completedWork.return;
// 检查是否存在异常
if ((completedWork.flags & Incomplete) === NoFlags) {
setCurrentDebugFiberInDEV(completedWork);
let next;
if (
!enableProfilerTimer ||
(completedWork.mode & ProfileMode) === NoMode
) {
// 创建节点真实 DOM 对象并将其添加到 stateNode 属性中
next = completeWork(current, completedWork, subtreeRenderLanes);
} else {
startProfilerTimer(completedWork);
next = completeWork(current, completedWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
}
resetCurrentDebugFiberInDEV();
if (next !== null) {
workInProgress = next;
return;
}
} else {
const next = unwindWork(current, completedWork, subtreeRenderLanes);
if (next !== null) {
next.flags &= HostEffectMask;
workInProgress = next;
return;
}
if (
enableProfilerTimer &&
(completedWork.mode & ProfileMode) !== NoMode
) {
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
let actualDuration = completedWork.actualDuration;
let child = completedWork.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
completedWork.actualDuration = actualDuration;
}
if (returnFiber !== null) {
returnFiber.flags |= Incomplete;
returnFiber.subtreeFlags = NoFlags;
returnFiber.deletions = null;
} else {
// We've unwound all the way to the root.
workInProgressRootExitStatus = RootDidNotComplete;
workInProgress = null;
return;
}
}
// 获取下一个同级 fiber 对象
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// 如果下一个同级 fiber 树存在
workInProgress = siblingFiber;
return;
}
// Otherwise, return to the parent
completedWork = returnFiber;
// 否则退回父级
workInProgress = completedWork;
} while (completedWork !== null);
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}
接下来分析 fiber 处理函数 completeWork(...),该函数的核心是根据 fiber.tag 做不同的处理,核心是 HostComponent 的实现:
- 调用
createInstance函数,创建了一个DOM元素; - 调用
appendAllChildren函数,将所有的子级追加到instance父级中; - 为
fiber树添加stateNode属性; - 设置
DOM对象的属性,绑定事件等; - 设置
fiber.flags,判断当前fiber是否有副作用,如果存在需要将当前fiber阶段加入到父节点的effects队列中,等待commit阶段处理。
举个例子,在我们的项目有如下的代码:
import React, { Component } from "react";
class App extends Component {
constructor() {
super();
}
render() {
return (
<div>
<h2>count的值为 1</h2>
<div>
<h3>hello</h3>
</div>
</div>
);
}
}
export default App;
该 DOM 树的构造方式正是深度遍历,遍历到最终的子节点,当子节点为空时,寻找它的兄弟节点,继续前面的方法,最终生成一个完整的 DOM 树,具体请看调用 createInstance(...) 函数后返回的 instance 在浏览器输出:
对比更新
在前面的内容中有讲到,无论是 首次渲染 还是 对比更新,最后都会调用 scheduleUpdateOnFiber(...) 函数,并且会在该函数中调用 ensureRootIsScheduled(...) 函数,该函数又调用 performConcurrentWorkOnRoot(...) 函数,如果当前是首次更新,会调用 renderRootSync(...) 函数,但如果是对比更新,会调用 renderRootConcurrent(...) 函数。
如果要发起主动更新,有三种常见方式:
Class组件中调用setState;Function组件中调用hook对象暴露出来的dispatchAction;- 在
container节点上重复调用render;
举个例子,当我们调用 setState 的时候,会调用 enqueueSetState(...) 函数,该函数里面会调用 enqueueUpdate(...) 函数将任务推入更新队列,最后会在该函数的结尾返回enqueueConcurrentClassUpdate(...) 函数的调用结果,在该函数中,主要做的作用就又是调用 markUpdateLaneFromFiberToRoot(...) 函数并返回结果,这个函数很关键,接下来我们看看它到底都干了些啥?
构建阶段
markUpdateLaneFromFiberToRoot
该函数,只在 对比更新 阶段才发挥出它的作用,它找出了 fiber 树中受到本次 update 更新的影响的所有节点,并设置这些节点的 fiber.lanes 或 fiber.childLanes 以备 fiber 树构造阶段使用。
function markUpdateLaneFromFiberToRoot(
sourceFiber: Fiber, // sourceFiber表示被更新的节点
lane: Lane,// lane表示update优先级
): FiberRoot | null {
console.log(sourceFiber);
// 将 update 优先级设置到 sourceFiber.lanes
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
let alternate = sourceFiber.alternate;
if (alternate !== null) {
// 同时设置sourceFiber.alternate的优先级
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
// 当前的fiber
let node = sourceFiber;
// 父fiber
let parent = sourceFiber.return;
// 从 sourceFiber 开始, 向上遍历所有节点, 直到HostRoot. 设置沿途所有节点(包括alternate)的childLanes
while (parent !== null) {
// fiber 的childLanes 合并
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
}
node = parent;
parent = parent.return;
}
if (node.tag === HostRoot) {
const root: FiberRoot = node.stateNode;// 最终返回一个新的 fiberRoot
return root;
} else {
return null;
}
}
还是之前的例子,当我们调用 setState 发起更新的时候,会执行到该函数,通过打印 sourceFiber,发现控制台下会有以下输出:
在红色框那个地方标记着本次更新的节点。
这个时候我们不妨把关注点放回到 performConcurrentWorkOnRoot(...) 函数中,如果不是首次渲染,会触发 renderRootConcurrent(...) 函数,这个函数做的事情真的很简单,只不过是加了一些 do...while循环 把任务交给小弟 workLoopConcurrent 去做。
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
探寻阶段 beginWork
在前面的 workLoopConcurrent 函数中会循环调用 performUnitOfWork(...) 函数,该函数又调用了 beginWork,这前面不是讲了吗? 是的,有讲过,但是这次和上一次的不同。
当 首次渲染 时,current 为 null 或者 current.memoizedProps 的值为 null。
在 beginWork 函数中,根据 workInProgress.tag 的不同,返回不同的值,但是注意看到有很 updateXXX 函数,其中很多个函数都会调用 bailoutOnAlreadyFinishedWork(...) 函数。
bailoutOnAlreadyFinishedWork
该函数的主要的作用是做性能优化的,那么什么时候会触发该函数呢?
举个例子,当 workInProgress.tag === FunctionComponent 是会调用 updateFunctionComponent 函数,该函数的核心逻辑如下:
- 处理
context,然后执行renderWithHooks函数; - 如果
current不为null并且didReceiveUpdate为false,则执行bailoutHooks(...)函数和bailoutOnAlreadyFinishedWork(...)函数,并返回后者; - 执行
reconcileChildren函数; - 返回
workInProgress.child;
到这里.我们来聊聊 bailout 逻辑,在 React 中 bailout 用于判断子树节点是否完全复用,如果可以复用,则会略过 fiber 树构造。
与初次创建不同,在对比更新过程中,如果是老节点,那么 current!==null 成立,需要进行对比,然后决定是否复用老节点及其子树。
那么我们看看 bailoutOnAlreadyFinishedWork() 函数,该函数的主要作用是作对比更新:
- 如果同时满足
!includesSomeLane(renderLanes, workInProgress.childLanes),表明该fiber节点及其子树都无需更新,可直接进入回溯阶段; - 如果不满足
!includesSomeLane(renderLanes, workInProgress.childLanes),意味着子节点需要更新,clone并返回子节点;
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
workInProgress.dependencies = current.dependencies;
}
if (enableProfilerTimer) {
stopProfilerTimerIfRunning(workInProgress);
}
markSkippedUpdateLanes(workInProgress.lanes);
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
if (enableLazyContextPropagation && current !== null) {
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
}
} else {
// 渲染优先级不包括 workInProgress.childLanes, 表明子节点也无需更新. 返回null, 直接进入回溯阶段.
return null;
}
}
// 本fiber虽然不用更新, 但是子节点需要更新. clone并返回子节点
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
cloneChildFibers(...) 函数内部调用 createWorkInProgress,在构造 fiber 节点时会优先复用 workInProgress.alternate (不开辟新的内存空间), 否则才会创建新的fiber对象。
回溯阶段 completeWork
completeUnitOfWork(unitOfWork) 函数在 初次创建 和 对比更新 逻辑一致,都是处理 beginWork 阶段已经创建出来的 fiber 节点,最后创建或更新 DOM 对象,并上移副作用队列。
fiber 树渲染
fiber 树的渲染阶段整个渲染逻辑都在 commitRoot 函数中,具体代码如下:
function commitRoot(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
) {
const previousUpdateLanePriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
ReactCurrentBatchConfig.transition = null;
setCurrentUpdatePriority(DiscreteEventPriority);
commitRootImpl(
root,
recoverableErrors,
transitions,
previousUpdateLanePriority,
);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
}
return null;
}
这个 commitRoot(...) 函数只是一个桥梁,最终的实现还是需要通过 commitRootImpl(...) 函数。
所以 commit 阶段又分为三个子阶段,它们分别是:
before mutation阶段,执行DOM操作前;mutation阶段,执行DOM操作;layout阶段,执行DOM后;
进入 commit 和阶段,先清空之前未执行的 useEffect。
before mutation 阶段
这个阶段的入口自函数的 commitBeforeMutationEffects 函数,该函数又有如下的调用栈:
commitBeforeMutationEffectscommitBeforeMutationEffects_begincommitBeforeMutationEffects_complete
在这里就贴出第一个函数的代码,具体自行查阅源码:
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
) {
focusedInstanceHandle = prepareForCommit(root.containerInfo);
nextEffect = firstChild;
commitBeforeMutationEffects_begin();
const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
commitBeforeMutationEffects_begin 中会向下遍历 child 指针,为每个遍历到的节点指向 ensureCorrectReturnPointer(...) 函数,以确立当前父节点的指针,若不存在子节点则调用 commitBeforeMutationEffects_complete(...) 函数。
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
setCurrentDebugFiberInDEV(fiber);
try {
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
在该函数中主要的作用是调用 commitBeforeMutationEffectsOnFiber(...) 函数以调用 getSnapshotBeforeUpdate 生命周期钩子。
因此我们在这里可以得知 before mutation 阶段的主要职责就是执行class组件的 getsnapshotBeforeUpdate 生命周期。
mutation 阶段
这个阶段就是 DOM 操作阶段,通过调用 commitMutationEffects(...) 函数,这个函数根据不同的 finishedWork.tag 进入不同的入口,对于 HostRoot,首先会有一些公共逻辑会先执行:
- 调用
recursivelyTraverseMutationEffects函数深度遍历执行删除操作; - 调用
commitReconciliationEffects函数执行插入操作;
layout 阶段
layout 阶段的入口函数是 commitLayoutEffects,它的实现如下代码所示:
export function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes,
): void {
inProgressLanes = committedLanes;
inProgressRoot = root;
nextEffect = finishedWork;
commitLayoutEffects_begin(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
和 before mutation 阶段一样,先深度优先递归,找最后一个 LayoutMask 标记的 fiber。然后从下往上调用 complete 逻辑,确保逻辑是从底部到顶部,从子到父。
complete 的函数是 commitLayoutMountEffects_complete,它的具体实现如下代码所示:
function commitLayoutMountEffects_complete(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & LayoutMask) !== NoFlags) {
const current = fiber.alternate;
setCurrentDebugFiberInDEV(fiber);
try {
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
}
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
它的核心工作就是调用 commitLayoutEffectOnFiber,它会根据组件类型不同执行不同逻辑:
- 如果是函数组件,会调用
useLayoutEffect的回调函数,调度useEffect的 销毁 与 回调 函数; - 如果是类组件的情况:
- 如果是首次渲染,调用
instance.componentDidMount方法; - 如果是更新,提取
preProps等参数传入到componentDidUpdate里调用;
- 如果是首次渲染,调用
最后会取出 updateQueue 里的 effect,一次调用 effect.callback 函数,这个 callback 其实就是 setState 方法的第二个参数。
在上面的事情处理完成之后,调用 commitAttachRef 获取 DOM 实例,更新 ref。该函数的具体实现如下代码所示:
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
// 获取DOM实例
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
// Moved outside to ensure DCE works with this flag
if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
instanceToUse = instance;
}
if (typeof ref === "function") {
let retVal;
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// 如果ref是函数形式,调用回调函数
retVal = ref(instanceToUse);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
retVal = ref(instanceToUse);
}
} else {
// 如果ref是ref实例形式,赋值ref.current
ref.current = instanceToUse;
}
}
}
useEffect
现在还差 useEffect 没调用了,它不在同步的 commit 阶段中执行,它是异步的,被 schuduler 异步调度执行。
举个例子,在我们的项目中如下定义:
import React, { useEffect, useLayoutEffect, useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(1);
return () => {
console.log(5);
};
}, [count]);
useLayoutEffect(() => {
console.log(2);
}, [count]);
setTimeout(() => {
console.log(6);
}, 0);
console.log(3);
new Promise((resolve) => {
resolve(4);
}).then((res) => {
console.log(res);
});
return <div onClick={() => setCount(count + 1)}>{count}</div>;
};
export default App;
当我们运行项目时首次输出会有以下这玩意:
你会发现 settimeout 函数都会被优先执行了,useEffect 再最后再执行,当我们通过 onLlick 事件调用 useState 时,又会有如下输出:
在这里我们应该知道一点,useEffect 的销毁函数会交给下一次渲染中执行,所以当我们点击时会首先执行这个(忽略2和3),由于离散事件优先级最高,需要和用户优先进行交互,这个时候 useEffect 变为了同步优先级,优先执行。
在文章的最后再贴一直叼图吧!!!
到此为止,本章内容也到这里结束了。
总结
没有总结,你自己总结吧。
就是说,学生党别乱读源码,还不如安安分分写个项目,读这源码对我找工作目前没有任何作用,项目还是用的之前写的垃圾网易云音乐呜呜呜呜呜.....
参考文章
本文正在参加「金石计划」