构建阶段
在前面的文章中我们讲到了 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
函数,该函数又有如下的调用栈:
commitBeforeMutationEffects
commitBeforeMutationEffects_begin
commitBeforeMutationEffects_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
变为了同步优先级,优先执行。
在文章的最后再贴一直叼图吧!!!
到此为止,本章内容也到这里结束了。
总结
没有总结,你自己总结吧。
就是说,学生党别乱读源码,还不如安安分分写个项目,读这源码对我找工作目前没有任何作用,项目还是用的之前写的垃圾网易云音乐呜呜呜呜呜…..
参考文章
本文正在参加「金石计划」
原文链接:https://juejin.cn/post/7216644653126713405 作者:Moment