Reactの内部構造を見ていくよ(スケジュールフェーズ編) scheduleUpdateOnFiber以降
レンダーフェーズ以降
scheduleUpdateOnFiber関数が呼び出されたらどうなる?
scheduleUpdateOnFiber関数内部では、ensureRootIsScheduled関数が呼び出されるっぽい
scheduleUpdateOnFiber関数
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
) {
if (__DEV__) {
if (isRunningInsertionEffect) {
console.error('useInsertionEffect must not schedule updates.');
}
}
if (__DEV__) {
if (isFlushingPassiveEffects) {
didScheduleUpdateDuringPassiveEffects = true;
}
}
// Check if the work loop is currently suspended and waiting for data to
// finish loading.
if (
// Suspended render phase
(root === workInProgressRoot &&
workInProgressSuspendedReason === SuspendedOnData) ||
// Suspended commit phase
root.cancelPendingCommit !== null
) {
// The incoming update might unblock the current render. Interrupt the
// current attempt and restart from the top.
prepareFreshStack(root, NoLanes);
const didAttemptEntireTree = false;
markRootSuspended(
root,
workInProgressRootRenderLanes,
workInProgressDeferredLane,
didAttemptEntireTree,
);
}
// Mark that the root has a pending update.
markRootUpdated(root, lane);
if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
// This update was dispatched during the render phase. This is a mistake
// if the update originates from user space (with the exception of local
// hook updates, which are handled differently and don't reach this
// function), but there are some internal React features that use this as
// an implementation detail, like selective hydration.
warnAboutRenderPhaseUpdatesInDEV(fiber);
// Track lanes that were updated during the render phase
workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(
workInProgressRootRenderPhaseUpdatedLanes,
lane,
);
} else {
// This is a normal update, scheduled from outside the render phase. For
// example, during an input event.
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
addFiberToLanesMap(root, fiber, lane);
}
}
warnIfUpdatesNotWrappedWithActDEV(fiber);
if (enableTransitionTracing) {
const transition = ReactSharedInternals.T;
if (transition !== null && transition.name != null) {
if (transition.startTime === -1) {
transition.startTime = now();
}
// $FlowFixMe[prop-missing]: The BatchConfigTransition and Transition types are incompatible but was previously untyped and thus uncaught
// $FlowFixMe[incompatible-call]: "
addTransitionToLanesMap(root, transition, lane);
}
}
if (root === workInProgressRoot) {
// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root.
if ((executionContext & RenderContext) === NoContext) {
workInProgressRootInterleavedUpdatedLanes = mergeLanes(
workInProgressRootInterleavedUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
const didAttemptEntireTree = false;
markRootSuspended(
root,
workInProgressRootRenderLanes,
workInProgressDeferredLane,
didAttemptEntireTree,
);
}
}
ensureRootIsScheduled(root);
if (
lane === SyncLane &&
executionContext === NoContext &&
!disableLegacyMode &&
(fiber.mode & ConcurrentMode) === NoMode
) {
if (__DEV__ && ReactSharedInternals.isBatchingLegacy) {
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
} else {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncWorkOnLegacyRootsOnly();
}
}
}
}
markRootUpdated関数
function markRootSuspended(
root: FiberRoot,
suspendedLanes: Lanes,
spawnedLane: Lane,
didAttemptEntireTree: boolean,
) {
// When suspending, we should always exclude lanes that were pinged or (more
// rarely, since we try to avoid it) updated during the render phase.
suspendedLanes = removeLanes(suspendedLanes, workInProgressRootPingedLanes);
suspendedLanes = removeLanes(
suspendedLanes,
workInProgressRootInterleavedUpdatedLanes,
);
_markRootSuspended(root, suspendedLanes, spawnedLane, didAttemptEntireTree);
}
_markRootSuspendedの本来の関数の方
export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
root.pendingLanes |= updateLane;
// If there are any suspended transitions, it's possible this new update
// could unblock them. Clear the suspended lanes so that we can try rendering
// them again.
//
// TODO: We really only need to unsuspend only lanes that are in the
// `subtreeLanes` of the updated fiber, or the update lanes of the return
// path. This would exclude suspended updates in an unrelated sibling tree,
// since there's no way for this update to unblock it.
//
// We don't do this if the incoming update is idle, because we never process
// idle updates until after all the regular updates have finished; there's no
// way it could unblock a transition.
if (updateLane !== IdleLane) {
root.suspendedLanes = NoLanes;
root.pingedLanes = NoLanes;
root.warmLanes = NoLanes;
}
}
rootのpendingLaneをビット演算でorしている
これはマージしているということ?
簡単にレーンの特徴をお伝えします。演算子を使うことでレーン同士をマージしたり、対象のレーンが含まれているか確認することができます。複数のレーンを持つビットマスクを作ることも可能です。
const SyncLane = 0b0000000000000000000000000000001;
const InputContinuousLane = 0b0000000000000000000000000000100;
// `|`でレーンをマージ
// `SyncLane`と`InputContinuousLane`を持つビットマスクになる
const merged = SyncLane | InputContinuousLane;
console.log(merged); // 0b0000000000000000000000000000101
// `&`でビットマスクが対象のレーンを含むか確認できる
console.log((merged & InputContinuousLane) != 0); // true
このコードで何をしたいのかよくわからんマン
次はensureRootIsScheduled関数について調べる
ensureRootIsScheduled関数
export function ensureRootIsScheduled(root: FiberRoot): void {
// This function is called whenever a root receives an update. It does two
// things 1) it ensures the root is in the root schedule, and 2) it ensures
// there's a pending microtask to process the root schedule.
//
// Most of the actual scheduling logic does not happen until
// `scheduleTaskForRootDuringMicrotask` runs.
// Add the root to the schedule
if (root === lastScheduledRoot || root.next !== null) {
// Fast path. This root is already scheduled.
} else {
if (lastScheduledRoot === null) {
firstScheduledRoot = lastScheduledRoot = root;
} else {
lastScheduledRoot.next = root;
lastScheduledRoot = root;
}
}
// Any time a root received an update, we set this to true until the next time
// we process the schedule. If it's false, then we can quickly exit flushSync
// without consulting the schedule.
mightHavePendingSyncWork = true;
// At the end of the current event, go through each of the roots and ensure
// there's a task scheduled for each one at the correct priority.
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
// We're inside an `act` scope.
if (!didScheduleMicrotask_act) {
didScheduleMicrotask_act = true;
scheduleImmediateTask(processRootScheduleInMicrotask);
}
} else {
if (!didScheduleMicrotask) {
didScheduleMicrotask = true;
scheduleImmediateTask(processRootScheduleInMicrotask);
}
}
if (!enableDeferRootSchedulingToMicrotask) {
// While this flag is disabled, we schedule the render task immediately
// instead of waiting a microtask.
// TODO: We need to land enableDeferRootSchedulingToMicrotask ASAP to
// unblock additional features we have planned.
scheduleTaskForRootDuringMicrotask(root, now());
}
if (
__DEV__ &&
!disableLegacyMode &&
ReactSharedInternals.isBatchingLegacy &&
root.tag === LegacyRoot
) {
// Special `act` case: Record whenever a legacy update is scheduled.
ReactSharedInternals.didScheduleLegacyUpdate = true;
}
}
これは何を行っている?
多分scheduleTaskForRootDuringMicrotask関数が一番重要、みたいな気が
function scheduleTaskForRootDuringMicrotask(
root: FiberRoot,
currentTime: number,
): Lane {
// This function is always called inside a microtask, or at the very end of a
// rendering task right before we yield to the main thread. It should never be
// called synchronously.
//
// TODO: Unless enableDeferRootSchedulingToMicrotask is off. We need to land
// that ASAP to unblock additional features we have planned.
//
// This function also never performs React work synchronously; it should
// only schedule work to be performed later, in a separate task or microtask.
// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
markStarvedLanesAsExpired(root, currentTime);
// Determine the next lanes to work on, and their priority.
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
const existingCallbackNode = root.callbackNode;
if (
// Check if there's nothing to work on
nextLanes === NoLanes ||
// If this root is currently suspended and waiting for data to resolve, don't
// schedule a task to render it. We'll either wait for a ping, or wait to
// receive an update.
//
// Suspended render phase
(root === workInProgressRoot && isWorkLoopSuspendedOnData()) ||
// Suspended commit phase
root.cancelPendingCommit !== null
) {
// Fast path: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return NoLane;
}
// Schedule a new callback in the host environment.
if (
includesSyncLane(nextLanes) &&
// If we're prerendering, then we should use the concurrent work loop
// even if the lanes are synchronous, so that prerendering never blocks
// the main thread.
!(enableSiblingPrerendering && checkIfRootIsPrerendering(root, nextLanes))
) {
// Synchronous work is always flushed at the end of the microtask, so we
// don't need to schedule an additional task.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
} else {
// We use the highest priority lane to represent the priority of the callback.
const existingCallbackPriority = root.callbackPriority;
const newCallbackPriority = getHighestPriorityLane(nextLanes);
if (
newCallbackPriority === existingCallbackPriority &&
// Special case related to `act`. If the currently scheduled task is a
// Scheduler task, rather than an `act` task, cancel it and re-schedule
// on the `act` queue.
!(
__DEV__ &&
ReactSharedInternals.actQueue !== null &&
existingCallbackNode !== fakeActCallbackNode
)
) {
// The priority hasn't changed. We can reuse the existing task.
return newCallbackPriority;
} else {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
// Scheduler does have an "ImmediatePriority", but now that we use
// microtasks for sync work we no longer use that. Any sync work that
// reaches this path is meant to be time sliced.
case DiscreteEventPriority:
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
const newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performWorkOnRootViaSchedulerTask.bind(null, root),
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
return newCallbackPriority;
}
}
優先度をむっちゃ確認しながら実行する感じのものっぽい?気がする
マクロタスクとかマイクロタスクとかの話はこの記事でまとめてある内容に沿って読んでいけそう
というか、scheduleTaskForRootDuringMicrotaskって書いてあるがマイクロタスクなのか・・・?
本当に
scheduleCallback関数のなかみを見てみます
function scheduleCallback(
priorityLevel: PriorityLevel,
callback: RenderTaskFn,
) {
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
// Special case: We're inside an `act` scope (a testing utility).
// Instead of scheduling work in the host environment, add it to a
// fake internal queue that's managed by the `act` implementation.
ReactSharedInternals.actQueue.push(callback);
return fakeActCallbackNode;
} else {
return Scheduler_scheduleCallback(priorityLevel, callback);
}
}
Scheduler_scheduleCallback関数の中身はこれ
export const scheduleCallback = Scheduler.unstable_scheduleCallback;
たらい回しやないか・・・
と思ったが、これでunstable_scheduleCallbackにたどり着いた
unstable_scheduleCallbackとは?
react18記事で言及がされている
ユーザ操作のイベントが発生するとunstable_scheduleCallback関数が呼ばれます。
関数内でタスクがスタックに積まれた後、requestHostCallback(flushWork);でflushWork関数をマクロタスクとして追加します。
unstable_scheduleCallback関数
function unstable_scheduleCallback(
priorityLevel: PriorityLevel,
callback: Callback,
options?: {delay: number},
): Task {
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:
// Times out immediately
timeout = -1;
break;
case UserBlockingPriority:
// Eventually times out
timeout = userBlockingPriorityTimeout;
break;
case IdlePriority:
// Never times out
timeout = maxSigned31BitInt;
break;
case LowPriority:
// Eventually times out
timeout = lowPriorityTimeout;
break;
case NormalPriority:
default:
// Eventually times out
timeout = normalPriorityTimeout;
break;
}
var expirationTime = startTime + timeout;
var newTask: Task = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
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;
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();
}
}
return newTask;
}
requestHostCallback関数でflushWork関数を引数に渡しているみたいだけども、存在しない・・・
なんで?
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback();
}
しょうがないのでrequestHostCallback関数を解剖
function requestHostCallback() {
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
schedulePerformWorkUntilDeadline関数
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
// Node.js and old IE.
// There's a few reasons for why we prefer setImmediate.
//
// Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
// (Even though this is a DOM fork of the Scheduler, you could get here
// with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
// https://github.com/facebook/react/issues/20756
//
// But also, it runs earlier which is the semantic we want.
// If other browsers ever implement it, it's better to use it.
// Although both of these would be inferior to native scheduling.
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker environments.
// We prefer MessageChannel because of the 4ms setTimeout clamping.
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 = () => {
// $FlowFixMe[not-a-function] nullable value
localSetTimeout(performWorkUntilDeadline, 0);
};
}
performWorkUntilDeadline関数
const performWorkUntilDeadline = () => {
if (isMessageLoopRunning) {
const currentTime = getCurrentTime();
// Keep track of the start time so we can measure how long the main thread
// has been blocked.
startTime = currentTime;
// 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 `flushWork` errors, then `hasMoreWork` will
// remain true, and we'll continue the work loop.
let hasMoreWork = true;
try {
hasMoreWork = flushWork(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;
}
}
}
};
あった!!
たらい回しにさせすぎじゃろ・・・
localSetImmediate(performWorkUntilDeadline);
もしくはlocalSetTimeout(performWorkUntilDeadline, 0);
で、コールバック関数を引数に与えている
つまり、ここで実際にタスクを積んでいることになるのでは・・・?
flushWork関数
function flushWork(initialTime: number) {
if (enableProfiling) {
markSchedulerUnsuspended(initialTime);
}
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
if (enableProfiling) {
try {
return workLoop(initialTime);
} catch (error) {
if (currentTask !== null) {
const currentTime = getCurrentTime();
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskErrored(currentTask, currentTime);
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.isQueued = false;
}
throw error;
}
} else {
// No catch in prod code path.
return workLoop(initialTime);
}
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
if (enableProfiling) {
const currentTime = getCurrentTime();
markSchedulerSuspended(currentTime);
}
}
}
workLoop関数を呼び出している
workLoop関数
function workLoop(initialTime: number) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
const callback = currentTask.callback;
if (typeof callback === 'function') {
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.callback = null;
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentPriorityLevel = currentTask.priorityLevel;
// $FlowFixMe[incompatible-use] found when upgrading Flow
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
if (enableProfiling) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskRun(currentTask, currentTime);
}
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// If a continuation is returned, immediately yield to the main thread
// regardless of how much time is left in the current time slice.
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.callback = continuationCallback;
if (enableProfiling) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskYield(currentTask, currentTime);
}
advanceTimers(currentTime);
return true;
} else {
if (enableProfiling) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskCompleted(currentTask, currentTime);
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
advanceTimers(currentTime);
}
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
workLoop関数の内部実装についてはこっちの記事を参考にしたほうがいい?
優先度付きキューの実装はここ
おそらく
1.タスクを作成
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
- 優先度付きキューにタスクを突っ込む
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
(省略)
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
(省略)
- 現在のタスク: taskQueue
- 未来のタスク: timerQueue
https://tnoyan.hatenablog.com/entry/2021/12/12/035727
ここ参考
workLoop関数
workLoop関数内では、バイナリヒープからタスクを取り出し、タスクのcallback関数を実行します。
つまり
3. 優先度付きキューからタスクを取り出してタスクにあるコールバック関数を実行
ここでタスクに入った処理が実行されるということか
// $FlowFixMe[incompatible-use] found when upgrading Flow
const callback = currentTask.callback;
if (typeof callback === 'function') {
(省略)
const continuationCallback = callback(didUserCallbackTimeout);
...
なんか色々条件分岐があるのは期限切れタスクかどうかの条件分岐のような気がする
しかし気になるのは、callbackの中身が関数の場合はpopされないこと
全体像の引用
優先度付きキューとタスクの消化の話はまた後で!
callback関数について再注目するために、
もう一度scheduleTaskForRootDuringMicrotask関数に戻って考える
コード再掲
const newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performWorkOnRootViaSchedulerTask.bind(null, root),
);
performWorkOnRootViaSchedulerTask関数
function performWorkOnRootViaSchedulerTask(
root: FiberRoot,
didTimeout: boolean,
): RenderTaskFn | null {
// This is the entry point for concurrent tasks scheduled via Scheduler (and
// postTask, in the future).
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
resetNestedUpdateFlag();
}
// Flush any pending passive effects before deciding which lanes to work on,
// in case they schedule additional work.
const originalCallbackNode = root.callbackNode;
const didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects) {
// Something in the passive effect phase may have canceled the current task.
// Check if the task node for this root was changed.
if (root.callbackNode !== originalCallbackNode) {
// The current task was canceled. Exit. We don't need to call
// `ensureRootIsScheduled` because the check above implies either that
// there's a new task, or that there's no remaining work on this root.
return null;
} else {
// Current task was not canceled. Continue.
}
}
// Determine the next lanes to work on, using the fields stored on the root.
// TODO: We already called getNextLanes when we scheduled the callback; we
// should be able to avoid calling it again by stashing the result on the
// root object. However, because we always schedule the callback during
// a microtask (scheduleTaskForRootDuringMicrotask), it's possible that
// an update was scheduled earlier during this same browser task (and
// therefore before the microtasks have run). That's because Scheduler batches
// together multiple callbacks into a single browser macrotask, without
// yielding to microtasks in between. We should probably change this to align
// with the postTask behavior (and literally use postTask when
// it's available).
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
const lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (lanes === NoLanes) {
// No more work on this root.
return null;
}
// Enter the work loop.
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
const forceSync = !disableSchedulerTimeoutInWorkLoop && didTimeout;
performWorkOnRoot(root, lanes, forceSync);
// The work loop yielded, but there may or may not be work left at the current
// priority. Need to determine whether we need to schedule a continuation.
// Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask;
// however, since most of the logic for determining if we need a continuation
// versus a new task is the same, we cheat a bit and call it here. This is
// only safe to do because we know we're at the end of the browser task.
// So although it's not an actual microtask, it might as well be.
scheduleTaskForRootDuringMicrotask(root, now());
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performWorkOnRootViaSchedulerTask.bind(null, root);
}
return null;
}
これってscheduleTaskForRootDuringMicrotask関数と似たようなことをしてそう
なんだこれ、再帰的に呼び出してないか?
これは関数の呼び出しではないかも
prototype.bindメソッドか・・・!
あーなるほど・・・
引数をバインドして無名関数を返しているのか
じゃあこれ自体がコールバック関数だ
えーわからん
多分performWorkOnRootViaSchedulerTask関数はレンダーフェーズを開始するようなコールバック関数を返却するはずだが・・・
performWorkOnRoot関数を呼び出しているのでこれに注目
export function performWorkOnRoot(
root: FiberRoot,
lanes: Lanes,
forceSync: boolean,
): void {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
// We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
const shouldTimeSlice =
(!forceSync &&
!includesBlockingLane(lanes) &&
!includesExpiredLane(root, lanes)) ||
// If we're prerendering, then we should use the concurrent work loop
// even if the lanes are synchronous, so that prerendering never blocks
// the main thread.
// TODO: We should consider doing this whenever a sync lane is suspended,
// even for regular pings.
(enableSiblingPrerendering && checkIfRootIsPrerendering(root, lanes));
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes, true);
do {
let renderWasConcurrent = shouldTimeSlice;
if (exitStatus === RootInProgress) {
// Render phase is still in progress.
if (
enableSiblingPrerendering &&
workInProgressRootIsPrerendering &&
!shouldTimeSlice
) {
// We're in prerendering mode, but time slicing is not enabled. This
// happens when something suspends during a synchronous update. Exit the
// the work loop. When we resume, we'll use the concurrent work loop so
// that prerendering is non-blocking.
//
// Mark the root as suspended. Usually we do this at the end of the
// render phase, but we do it here so that we resume in
// prerendering mode.
// TODO: Consider always calling markRootSuspended immediately.
// Needs to be *after* we attach a ping listener, though.
const didAttemptEntireTree = false;
markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
}
break;
} else if (exitStatus === RootDidNotComplete) {
// The render unwound without completing the tree. This happens in special
// cases where need to exit the current render without producing a
// consistent tree or committing.
const didAttemptEntireTree = !workInProgressRootDidSkipSuspendedSiblings;
markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
} else {
// The render completed.
// Check if this render may have yielded to a concurrent event, and if so,
// confirm that any newly rendered stores are consistent.
// TODO: It's possible that even a concurrent render may never have yielded
// to the main thread, if it was fast enough, or if it expired. We could
// skip the consistency check in that case, too.
const finishedWork: Fiber = (root.current.alternate: any);
if (
renderWasConcurrent &&
!isRenderConsistentWithExternalStores(finishedWork)
) {
// A store was mutated in an interleaved event. Render again,
// synchronously, to block further mutations.
exitStatus = renderRootSync(root, lanes, false);
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
renderWasConcurrent = false;
// Need to check the exit status again.
continue;
}
// Check if something threw
if (
(disableLegacyMode || root.tag !== LegacyRoot) &&
exitStatus === RootErrored
) {
const lanesThatJustErrored = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
lanesThatJustErrored,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(
root,
lanesThatJustErrored,
errorRetryLanes,
);
renderWasConcurrent = false;
// Need to check the exit status again.
if (exitStatus !== RootErrored) {
// The root did not error this time. Restart the exit algorithm
// from the beginning.
// TODO: Refactor the exit algorithm to be less confusing. Maybe
// more branches + recursion instead of a loop. I think the only
// thing that causes it to be a loop is the RootDidNotComplete
// check. If that's true, then we don't need a loop/recursion
// at all.
continue;
} else {
// The root errored yet again. Proceed to commit the tree.
}
}
}
if (exitStatus === RootFatalErrored) {
prepareFreshStack(root, NoLanes);
// Since this is a fatal error, we're going to pretend we attempted
// the entire tree, to avoid scheduling a prerender.
const didAttemptEntireTree = true;
markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
break;
}
// We now have a consistent tree. The next step is either to commit it,
// or, if something suspended, wait to commit it after a timeout.
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
}
break;
} while (true);
ensureRootIsScheduled(root);
}
以下の処理が気になる
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes, true);
まずはrenderRootSyncから取り組む?
(初回マウント時はこれになる)再レンダリング時の処理調査についてはこっちをベースにrenderRootConcurrentを読み解く
(が、これ以降はレンダーフェーズ編に分離)
ここからはスケジュールの優先度キューやタスク消化の詳細について分析
shouldYield関数など中心に
そういえば、localSetImmediate関数についての解剖
const localSetImmediate =
typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom
これはNode.jsか古いIEでの実行の場合である
localSetTimeout関数についての解剖
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
これがsetTimeoutのラッパーとなっている気がする