Skip to content

Commit 83102b6

Browse files
committed
[Scheduler] Canceling an already running task
Canceling an already running task is currently a no-op. It does not cause the task to yield execution, and if the task does yield with a continuation, then the continuation will run. This change addresses both parts: canceling the current task causes `shouldYield` to return `true`, and if the canceled task returns a continuation, the continuation is ignored. This fixes the regression test introduced in the preceding commit. There's likely a better way to model a restart of an in-progress render, but this approach is conceptually similar to how a regular high priority interruption already works.
1 parent 8e2dd67 commit 83102b6

File tree

4 files changed

+51
-10
lines changed

4 files changed

+51
-10
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2046,8 +2046,16 @@ function updateDehydratedSuspenseComponent(
20462046
// at even higher pri.
20472047
let attemptHydrationAtExpirationTime = renderExpirationTime + 1;
20482048
suspenseState.retryTime = attemptHydrationAtExpirationTime;
2049+
// TODO: This happens to abort the current render and switch to a
2050+
// hydration pass, because the priority of the new task is slightly
2051+
// higher priority, which causes the work loop to cancel the current
2052+
// Scheduler task via `Scheduler.cancelCallback`. But we should probably
2053+
// model this entirely within React instead of relying on Scheduler's
2054+
// semantics for canceling in-progress tasks. I've chosen this approach
2055+
// for now since it's a fairly non-invasive change and it conceptually
2056+
// matches how other types of interuptions (e.g. due to input events)
2057+
// already work.
20492058
scheduleWork(current, attemptHydrationAtExpirationTime);
2050-
// TODO: Early abort this render.
20512059
} else {
20522060
// We have already tried to ping at a higher priority than we're rendering with
20532061
// so if we got here, we must have failed to hydrate at those levels. We must

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,9 @@ function scheduleCallbackForRoot(
515515
// New callback has higher priority than the existing one.
516516
const existingCallbackNode = root.callbackNode;
517517
if (existingCallbackNode !== null) {
518+
// If this happens during render, this task represents the currently
519+
// running task. Canceling has the effect of interrupting the render and
520+
// starting over at the higher priority.
518521
cancelCallback(existingCallbackNode);
519522
}
520523
root.callbackExpirationTime = expirationTime;

packages/scheduler/src/Scheduler.js

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ var currentPriorityLevel = NormalPriority;
7575
// This is set while performing work, to prevent re-entrancy.
7676
var isPerformingWork = false;
7777

78+
var didCancelCurrentTask = false;
79+
7880
var isHostCallbackScheduled = false;
7981
var isHostTimeoutScheduled = false;
8082

@@ -184,16 +186,20 @@ function workLoop(hasTimeRemaining, initialTime) {
184186
markTaskRun(currentTask, currentTime);
185187
const continuationCallback = callback(didUserCallbackTimeout);
186188
currentTime = getCurrentTime();
187-
if (typeof continuationCallback === 'function') {
188-
currentTask.callback = continuationCallback;
189-
markTaskYield(currentTask, currentTime);
189+
if (didCancelCurrentTask) {
190+
didCancelCurrentTask = false;
190191
} else {
191-
if (enableProfiling) {
192-
markTaskCompleted(currentTask, currentTime);
193-
currentTask.isQueued = false;
194-
}
195-
if (currentTask === peek(taskQueue)) {
196-
pop(taskQueue);
192+
if (typeof continuationCallback === 'function') {
193+
currentTask.callback = continuationCallback;
194+
markTaskYield(currentTask, currentTime);
195+
} else {
196+
if (enableProfiling) {
197+
markTaskCompleted(currentTask, currentTime);
198+
currentTask.isQueued = false;
199+
}
200+
if (currentTask === peek(taskQueue)) {
201+
pop(taskQueue);
202+
}
197203
}
198204
}
199205
advanceTimers(currentTime);
@@ -389,6 +395,9 @@ function unstable_cancelCallback(task) {
389395
// remove from the queue because you can't remove arbitrary nodes from an
390396
// array based heap, only the first one.)
391397
task.callback = null;
398+
if (task === currentTask) {
399+
didCancelCurrentTask = true;
400+
}
392401
}
393402

394403
function unstable_getCurrentPriorityLevel() {
@@ -406,6 +415,7 @@ function unstable_shouldYield() {
406415
firstTask.callback !== null &&
407416
firstTask.startTime <= currentTime &&
408417
firstTask.expirationTime < currentTask.expirationTime) ||
418+
didCancelCurrentTask ||
409419
shouldYieldToHost()
410420
);
411421
}

packages/scheduler/src/__tests__/Scheduler-test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,26 @@ describe('Scheduler', () => {
288288
expect(Scheduler).toFlushWithoutYielding();
289289
});
290290

291+
it('cancelling the currently running task', () => {
292+
const task = scheduleCallback(NormalPriority, () => {
293+
Scheduler.unstable_yieldValue('Start');
294+
for (let i = 0; !shouldYield(); i++) {
295+
if (i === 5) {
296+
// Canceling the current task will cause `shouldYield` to return
297+
// `true`. Otherwise this would infinite loop.
298+
Scheduler.unstable_yieldValue('Cancel');
299+
cancelCallback(task);
300+
}
301+
}
302+
Scheduler.unstable_yieldValue('Finish');
303+
// The continuation should be ignored, since the task was
304+
// already canceled.
305+
return () => Scheduler.unstable_yieldValue('Continuation');
306+
});
307+
308+
expect(Scheduler).toFlushAndYield(['Start', 'Cancel', 'Finish']);
309+
});
310+
291311
it('top-level immediate callbacks fire in a subsequent task', () => {
292312
scheduleCallback(ImmediatePriority, () =>
293313
Scheduler.unstable_yieldValue('A'),

0 commit comments

Comments
 (0)