React源码解读之任务调度流程
「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。
react 版本:v17.0.3
无论是执行setState(class组件)进行更新还是执行useState(function组件)进行更新,都会创建更新任务(update)并添加到fiber的更新队列上 (class组件的更新队列为updateQueue,function组件的更新队列为baseQueue)。更新任务创建好并关联到了fiber之后,接下来就进入react render 阶段的核心之一 -- reconciler 阶段。
reconciler 流程
reconciler 的过程可以分为四个阶段:
-
任务输入:scheduleUpdateOnFiber 是处理更新任务开始的地方
-
调度任务注册:与调度中心(Scheduler)交互,注册调度任务(scheduler task),等待回调
-
执行任务回调:执行渲染任务,在内存中构造出fiber树,同时与渲染器(react-dom)交互,在内存中创建出与fiber对应的DOM节点
-
输出DOM节点:与渲染器(react-dom)交互,渲染DOM节点
reconciler的运作流程是一个固定的流程,它的四个阶段可以用下图表示:
任务输入
scheduleUpdateOnFiber
无论是首次渲染,还是后续更新操作,都会进入到react-reconciler对外暴露的updateContainer函数,然后在该函数中调用scheduleUpdateOnFiber,因此scheduleUpdateOnFiber函数是任务调度的入口,下面是该函数的关键代码:
// react-reconciler/src/ReactFiberWorkLoop.new.js
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
): FiberRoot | null {
// 检查是否有循环更新
// 避免例如在类组件 render 函数中调用了 setState 这种死循环的情况
checkForNestedUpdates();
// ...
// 标记当前 current树的lane优先级及其子节点的lanes优先级,会自底向上更新 child.fiberLanes
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
addFiberToLanesMap(root, fiber, lane);
}
}
// Mark that the root has a pending update.
// 标记 root 有更新,将 update 的 lane 插入到 root.pendingLanes 中
markRootUpdated(root, lane, eventTime);
// ...
// TODO: Consolidate with `isInterleavedUpdate` check
if (root === workInProgressRoot) {
// 在渲染过程中接收到一个更新,在根节点上标记一个交错更新,
if (
deferRenderPhaseUpdateToNextBatch ||
(executionContext & RenderContext) === NoContext
) {
workInProgressRootUpdatedLanes = mergeLanes(
workInProgressRootUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// 由于有高优先级的新的 update,将当前的更新标记为暂停,并切换到新的 update
markRootSuspended(root, workInProgressRootRenderLanes);
}
}
// 执行可中断更新
ensureRootIsScheduled(root, eventTime);
// ...
return root;
}
1、在 scheduleUpdateOnFiber 函数中,首先会调用 checkForNestedUpdates 方法,检查嵌套的更新数量,如果嵌套数量大于50 (NESTED_UPDATE_LIMIT = 50)层时,被认为是循环更新 (无限循环) 。此时会抛出异常,避免了例如在类组件render函数中调用了 setState 这种死循环的情况。
// 检查是否有循环更新
// 避免例如在类组件 render 函数中调用了 setState 这种死循环的情况
checkForNestedUpdates();
2、然后调用 markUpdateLaneFromFiberToRoot 方法,更新当前fiber节点lanes优先级及其父路径上父节点的child lanes优先级,即自底向上更新childLanes 。lanes的更新很简单,就是将当前任务的 lane 与 之前的 lane 进行二进制 或运算 叠加。
// 标记当前 current树的lane优先级及其子节点的lanes优先级,会自底向上更新 child.fiberLanes
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
3、接着将update的lane通过二进制运算插入到 root.pendingLanes,标记root节点有一个待处理的更新。
// Mark that the root has a pending update.
// 标记 root 有更新,将 update 的 lane 插入到 root.pendingLanes 中
markRootUpdated(root, lane, eventTime);
4、最后调用ensureRootIsScheduled,传入 root 节点和创建update的时间,开始执行可中断更新。
// 执行可中断更新
ensureRootIsScheduled(root, eventTime);
注册调度任务
安排调度任务 -- ensureRootIsScheduled
通过scheduleUpdateOnFiber函数输入任务后,会进入ensureRootIsScheduled函数。该函数的作用是为root安排调度任务。每个更新任务 (update) 都会经过 ensureRootIsScheduled 的处理。下面给出该函数的关键代码。
// 使用这个函数为 root 安排任务,每个 root 只有一个任务
// 每次更新时都会调用此函数,并在退出任务之前调用此函数。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// ...
// Schedule a new callback.
// 重新安排一个新的渲染任务
let newCallbackNode;
// 如果新渲染任务的优先级是同步优先级
// if 逻辑处理的是同步任务,同步任务不需经过 Scheduler
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
// 同步任务不经过 Scheduler,任务执行入口是 performSyncWorkOnRoot 函数
if (root.tag === LegacyRoot) {
// LegacyRoot 启动模式
// ...
// 启动模式为 legacy模式
// 把这个渲染任务加入 syncQueue 队列中
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// 启动模式为非 legacy模式
// 把这个渲染任务加入 syncQueue 队列中
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
// ...
} else {
// else 逻辑处理的是并发任务,并发任务需要经过 Scheduler
//...
// 根据调度优先级,调度并发任务
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
// ...
}
在 ensureRootIsScheduled 函数中,会根据任务的类型,去安排何种类型的调度任务。
-
如果任务类型是同步任务,则不需要经过调度,经过scheduleLegacySyncCallback或scheduleSyncCallback包装,如果当前的 js 主线程空闲 (没有正在执行的react任务),则执行performSyncWorkOnRoot开始执行同步任务。
// 同步任务不经过 Scheduler,任务执行入口是 performSyncWorkOnRoot 函数 if (root.tag === LegacyRoot) { // LegacyRoot 启动模式
// ...
// 启动模式为 legacy模式 // 把这个渲染任务加入 syncQueue 队列中 scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root)); } else { // 启动模式为非 legacy模式 // 把这个渲染任务加入 syncQueue 队列中 scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); }
-
如果任务类型是并发任务,则需要经过调度,会通过scheduleCallback回调函数注册调度任务。
// 根据调度优先级,调度并发任务 newCallbackNode = scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root), );
创建调度任务 -- unstable_scheduleCallback
在调度器Scheduler中,首先会通过unstable_scheduleCallback函数创建调度任务(schedule task),然后根据任务是否超时,分别将任务插入到超时队列timerQueue 和调度任务队列taskQueue中。将任务插入调度任务队列taskQueue之后,会通过requestHostCallback函数去调度任务。
// packages/scheduler/src/forks/Scheduler.js
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 这个 currentTime 获取的是 performance.now() 时间戳
var currentTime = getCurrentTime();
// 任务开始的时间
var startTime;
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
// 根据调度优先级设置相应的超时时间
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
// 过期时间
var expirationTime = startTime + timeout;
// 属于 Scheduler 自己的 task
var newTask = {
id: taskIdCounter++,
callback, // 这里的 callback 是 performConcurrentWorkOnRoot 函数
priorityLevel, // 调度优先级
startTime, // 任务开始时间
expirationTime, // 任务过期时间
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
// Scheduler调度任务时传入了 delay time,startTime 是大于 currentTime 的,
// 表示这个任务将会延迟执行
if (startTime > currentTime) {
// 当前任务已超时,插入超时队列
// This is a delayed task.
newTask.sortIndex = startTime;
// timerQueue 是一个二叉堆结构,以最小堆的形式存储 task
// 向二叉堆中添加节点
push(timerQueue, newTask);
// peek 查看堆的顶点, 也就是优先级最高的`task`
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
// 这个任务是最早延迟执行的
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
// 取消现有的定时器
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Schedule a timeout.
// 安排一个定时器
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// 任务未超时,插入调度任务队列
newTask.sortIndex = expirationTime;
// taskQueue 是一个二叉堆结构,以最小堆的形式存储 task
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
// 符合更新调度执行的标志
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
// requestHostCallback 调度任务
requestHostCallback(flushWork);
}
}
return newTask;
}
值得注意的是,超时队列timerQueue 和调度任务队列taskQueue都是一个二叉堆结构,使用最小堆存储 Scheduler 的 task,使得在任务调度的过程中可以在O(1)的时间获取到优先级最高的task,提高了任务调度的效率。
实现调度任务 -- performWorkUntilDeadline
在创建调度任务时,如果任务没有超时,则会被插入到调度队列taskQueue中,然后会通过requestHostCallback函数去调度任务。requestHostCallback中的参数callback就是通过workLoop获取到的优先级最高的任务。
// 通过 postMessage,通知 scheduler 已经开始了帧调度
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
在实现调度任务时,react 通过 new MessageChannel();
创建了消息通道,当发现 js 线程空闲时,通过 postMessage 通知 Scheduler 开始调度。然后 react 接收到调度开始的通知时,就通过 performWorkUntilDeadline 函数去执行任务,从而实现了帧空闲时间的任务调度。
// 获取当前设备每帧的时长
function forceFrameRate(fps) {
// ...
if (fps > 0) {
frameInterval = Math.floor(1000 / fps);
} else {
// reset the framerate
frameInterval = frameYieldMs;
}
}
// 帧结束前执行任务
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// Keep track of the start time so we can measure how long the main thread
// has been blocked.
startTime = currentTime;
const hasTimeRemaining = true;
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
//
// Intentionally not using a try-catch, since that makes some debugging
// techniques harder. Instead, if `scheduledHostCallback` errors, then
// `hasMoreWork` will remain true, and we'll continue the work loop.
let hasMoreWork = true;
try {
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
// 如果还有调度任务就执行
if (hasMoreWork) {
// If there's more work, schedule the next message event at the end
// of the preceding one.
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
} else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker environments.
// We prefer MessageChannel because of the 4ms setTimeout clamping.
// 通过 MessageChannel 创建消息通道,实现任务调度通知
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
// We should only fallback here in non-browser environments.
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
// 通过 postMessage,通知 scheduler 已经开始了帧调度
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
执行任务回调
执行任务回调,实际上就是执行 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot 。如果是同步任务,则调用performSyncWorkOnRoot函数执行同步任务,如果是并发任务,则任务需要经过调度,调用performConcurrentWorkOnRoot函数,执行并发任务(调度任务)。
执行同步渲染任务 -- performSyncWorkOnRoot
任务类型为同步任务,并且当前的js线程空闲(没有正在执行的react任务),通过performSyncWorkOnRoot(root)方法执行同步渲染任务。
// react-reconciler/src/ReactFiberWorkLoop.new.js
// 这个函数是不经过Scheduler调度的同步任务的入口点
function performSyncWorkOnRoot(root) {
// ...
// 从根节点开始进行同步渲染任务
let exitStatus = renderRootSync(root, lanes);
// ...
// We now have a consistent tree. Because this is a sync render, we
// will commit it even if something suspended.
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
// 提交fiber树
commitRoot(root);
// Before exiting, make sure there's a callback scheduled for the next
// pending level.
// 为 root 节点调度同步渲染任务
ensureRootIsScheduled(root, now());
return null;
}
在 performSyncWorkOnRoot 函数中,主要做了三件事:
-
调用 renderRootSync 从根节点开始执行同步渲染任务
-
调用 commitRoot 提交fiber树,执行commit流程
-
继续调用 ensureRootIsScheduled,为根节点安排同步渲染任务
执行并发任务(调度任务) -- performConcurrentWorkOnRoot
任务类型不是同步任务时,通过 performConcurrentWorkOnRoot (root, didTimeout)
执行并发渲染任务,去执行可中断的更新。
// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
// 并发任务的入口,即需要通过调度
function performConcurrentWorkOnRoot(root, didTimeout) {
// ...
// 时间切片
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
// 独立于 includesBlockingLane 的检查,因为在渲染已经开始后,车道可能会过期
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
// 退出状态
// 默认情况下并发更新总是使用时间切片
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes) // 并发渲染
: renderRootSync(root, lanes); // 同步渲染
// ...
// 退出前再次检测, 是否还有其他更新, 是否需要发起新调度
ensureRootIsScheduled(root, now());
// 下一个要渲染的任务,递归调用 performConcurrentWorkOnRoot自身 继续执行调度
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
// 渲染被阻断, 返回一个新的performConcurrentWorkOnRoot函数, 等待下一次调用
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}
在调用 performConcurrentWorkOnRoot 执行并发任务(调度任务)时,会根据 shouldTimeSlice 来决定是执行同步渲染任务还是执行并发渲染任务。
在react中,只有并发渲染任务才会使用 time slice(时间切片),因此进入renderRootConcurrent执行可中断更新。
// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// ...
// 循环执行并发渲染任务
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// ...
}
执行可中断更新
在执行并发渲染任务时,performConcurrentWorkOnRoot 会执行以下过程:
performConcurrentWorkOnRoot => renderRootConcurrent => workLoopConcurrent
。workLoopConcurrent 在每次对 workInProgress 执行 performUnitOfWork 之前,都会先判断 shouldYield() 的值。若为true,则中断执行,若为false,则继续执行。shouldYield() 停顿机制,实现了时间切片和可中断渲染。
// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
// shouldYield() 停顿机制,这个机制实现了 时间切片和可中断渲染
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
执行同步更新 (不可中断)
在执行同步渲染任务时,则会进入 renderRootSync 函数,循环调用 workLoopSync 进行同步更新。
// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function renderRootSync(root: FiberRoot, lanes: Lanes) {
// ...
// 循环执行同步渲染任务
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// ...
}
在 workLoopSync 中,只要 workInProgress (workInProgress fiber 树中新创建的 fiber 节点) 不为 null,就会一直循环,执行 performUnitOfWork,进行更新。
// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
performUnitOfWork
无论是执行同步渲染任务(不需经过调度),还是执行并发渲染任务(需经过调度),都会进入 performUnitOfWork 函数中。
// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
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) {
// If this doesn't spawn new work, complete the current work.
// 没有下一个任务了,完成当前的渲染任务
completeUnitOfWork(unitOfWork);
} else {
// workInProgress 是一个全局变量,如果 workInProgress 不为 null,
// 在 workLoopSync 和 workLoopConcurrent 中循环执行任务,直到所有的任务执行完
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
在 performUnitOfWork 函数中,会以 fiber 作为单元,进行reconciler过程。执行 beginWork,完成当前fiber上的更新任务,然后返回下一个fiber,更新 workInProgress,从而响应了上面的 workLoop循环 (workLoopSync 和 workLoopConcurrent),直到所有的更新任务执行完。
beginWork
在 performUnitOfWork 中调用的 beginWork,是根据当前的执行环境,封装调用了 ReactFiberBeginWork.new.js 中的 beginWork 函数。如果是 __DEV__ 环境,则再次封装 originalBeginWork 函数,如果是非 __DEV__ 环境,则直接调用 originalBeginWork 函数。
// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
import { beginWork as originalBeginWork } from './ReactFiberBeginWork.new';
let beginWork;
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
const dummyFiber = null;
beginWork = (current, unitOfWork, lanes) => {
// ...
try {
return originalBeginWork(current, unitOfWork, lanes);
} catch (originalError) {
// ...
}
};
} else {
beginWork = originalBeginWork;
}
在 originalBeginWork 函数中 (ReactFiberBeginWork.new.js 中的 beginWork 函数),会根据 workInProgress 的 tag 属性,执行不同类型的React元素的更新函数。
// packages/react-reconciler/src/ReactFiberBeginWork.new.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...
// 根据 workInProgress 的 tag 属性,
// 执行不同类型React元素的更新函数
switch (workInProgress.tag) {
// ...
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,
renderLanes,
);
}
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,
renderLanes,
);
}
// ...
}
// ...
}
无论是何种类型的React元素,其更新函数最终都会调用 reconcileChildren 函数。以 updateFunctionComponent 为例,首先执行 renderWithHooks 获取children,然后调用 reconcileChildren执行协调过程。
// packages/react-reconciler/src/ReactFiberBeginWork.new.js
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {
// ...
if (__DEV__) {
// ...
} else {
// 执行 renderWithHooks,获取children
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
}
// ...
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
// 调用 reconcileChildren,执行协调过程
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
reconcileChildren 做的事情是React的另一核心之一 -- diff 过程,会在下一篇文章中详细解析。
completeUnitOfWork
执行 beginWork,完成当前fiber上的更新任务后,如果返回的下一个 fiber 为 null,表示当前任务的fiber树已经遍历完了,此时 workInProgress 也变为了null,就进入 completeUnitOfWork 执行渲染任务。
// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
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) {
setCurrentDebugFiberInDEV(completedWork);
let next;
if (
!enableProfilerTimer ||
(completedWork.mode & ProfileMode) === NoMode
) {
// 完成当前的渲染任务,返回下一个任务
next = completeWork(current, completedWork, subtreeRenderLanes);
} else {
startProfilerTimer(completedWork);
// 完成当前的渲染任务,返回下一个任务
next = completeWork(current, completedWork, subtreeRenderLanes);
// Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
}
resetCurrentDebugFiberInDEV();
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
// workInProgress 是一个全局变量,如果 workInProgress 不为 null,
// 在 workLoopSync 和 workLoopConcurrent 中循环执行任务,直到所有的任务执行完
workInProgress = next;
return;
}
} else {
// 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.
// fiber节点没有完成渲染,从堆栈中弹出
const next = unwindWork(completedWork, subtreeRenderLanes);
// ...
}
// 当前的fiber已完成渲染任务,移动到它的兄弟节点,继续执行兄弟节点上的任务
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
// 当前的fiber已完成渲染任务,回到它的父节点,继续执行父节点上的任务
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
// ...
}
completeUnitOfWork 做的事情,就是从当前的fiber节点开始,到其兄弟节点,再到其父节点,逐层往上完成fiber树上的渲染任务。completeUnitOfWork 结束后,后面就到了commit阶段,即将fiber树输出反映到DOM上。
输出DOM节点
在输出阶段,commitRoot 只是设置了更新的优先级,主要的实现逻辑是在 commitRootImpl 函数中。其主要做的事情,就是处理副作用队列,将最新的fiber树结构反映到DOM上。
// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function commitRootImpl(root, renderPriorityLevel) {
// do while 循环,执行所有的副作用
do {
// flushPassiveEffects 在最后会调用 flushSyncUpdateQueue
// 循环执行 flushPassiveEffects,直到没有挂载阶段的副作用
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// ...
if (subtreeHasEffects || rootHasEffect) {
// 有副作用,处理 fiber树上的副作用
// ...
// 第一个阶段是 before mutation ,在这个阶段可以读取改变之前的 host tree 的state
// 这个阶段是 生命周期函数getSnapshotBeforeUpdate 调用的地方
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
// ...
// The next phase is the mutation phase, where we mutate the host tree.
// 下一个阶段是 mutation phase,可以在这个阶段 改变 host tree
commitMutationEffects(root, finishedWork, lanes);
// ...
// 下一个阶段是 layout phase(布局阶段)
// 提交 layout 阶段的副作用
commitLayoutEffects(finishedWork, root, lanes);
// ...
// Tell Scheduler to yield at the end of the frame, so the browser has an
// opportunity to paint.
// 告诉调度器在帧结束时让出,这样浏览器就有机会进行绘制。
requestPaint();
// 重置执行栈环境
executionContext = prevExecutionContext;
// Reset the priority to the previous non-sync value.
// 将优先级重置为之前的 非同步优先级
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
} else {
// 没有副作用
// ...
}
// ...
// 在退出 commitRoot 之前总是调用 ensureRootIsScheduled(),确保 root 节点上的任何额外任务都被调度
ensureRootIsScheduled(root, now());
// ...
// If layout work was scheduled, flush it now.
// 布局工作已安排,立即刷新它
flushSyncCallbacks();
// ...
}
commitRootImpl 的核心处理逻辑可分为3个阶段:
- 阶段一:commitBeforeMutationEffects
这个阶段主要是处理副作用队列中带有Snapshot、Passive标记的fiber节点,在这个阶段可以读取改变之前的 host tree 的state,这个阶段也是 生命周期函数 getSnapshotBeforeUpdate 调用的地方。
- 阶段二:commitMutationEffects
这个阶段可以改变host tree,重置current树,主要是处理副作用队列中带有Placement、Update、Deletion、Hydrating标记的fiber节点。
- 阶段三:commitLayoutEffects
这个阶段是布局阶段,主要处理副作用队列中带有Update | Callback标记的fiber节点,然后进行布局。
总结
本文介绍了 reconciler 的运作流程,将其分为了 4 个阶段,并深入分析了各个阶段的运作。
-
第一个阶段是任务输入阶段,scheduleUpdateOnFiber函数是任务输入的入口。
-
第二个阶段是调度任务注册阶段,通过与调度中心(Scheduler)交互,通过unstable_scheduleCallback函数注册调度任务。
-
第三个阶段是执行任务回调阶段,主要是通过performSyncWorkOnRoot执行同步渲染任务,通过performConcurrentWorkOnRoot执行并发渲染任务。
-
第四个阶段是输出DOM节点阶段,在内存中构建好的fiber树,与渲染器(react-dom)交互,渲染DOM节点。
这4个阶段,基本覆盖了react-reconciler包的核心逻辑。
转载自:https://juejin.cn/post/7057144903054131213