Skip to content

Commit 3b40cc0

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 3b40cc0

File tree

3 files changed

+42
-9
lines changed

3 files changed

+42
-9
lines changed

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)