Skip to content

Commit 9da68b3

Browse files
committed
Nested callbacks and continuations
A "nested callback" is one that is scheduled during the execution of a parent callback. It should default to the timeout of the parent. A "continuation" is a callback that is scheduled when yielding execution. It's scheduled by returning a callback from the parent. It should have the same timeout as the parent, and because it's a continuation, it should be inserted into the queue before callbacks of equal priority (not after, as callbacks are usually scheduled). Example of a continuation: function performWork(deadline) { while (tasks.length > 0) { const task = tasks.shift(); doTask(task); if ( tasks.length > 0 && !deadline.didTimeout && deadline.timeRemaining() <= 0 ) { // Ran out of time. Yield and // continue later. return performWork; } } } scheduleWork(performWork);
1 parent 400f2a9 commit 9da68b3

File tree

2 files changed

+269
-53
lines changed

2 files changed

+269
-53
lines changed

packages/schedule/src/Schedule.js

Lines changed: 98 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,36 @@ const DEFERRED_TIMEOUT = 5000;
1414
let firstCallbackNode = null;
1515

1616
let priorityContext = Deferred;
17-
let isPerformingWork = false;
17+
let currentlyFlushingTime = -1;
1818

1919
let isHostCallbackScheduled = false;
2020

2121
let timeRemaining;
2222
if (hasNativePerformanceNow) {
2323
timeRemaining = () => {
24+
if (
25+
firstCallbackNode !== null &&
26+
firstCallbackNode.timesOutAt < currentlyFlushingTime
27+
) {
28+
// A higher priority callback was scheduled. Yield so we can switch to
29+
// working on that.
30+
return 0;
31+
}
2432
// We assume that if we have a performance timer that the rAF callback
2533
// gets a performance timer value. Not sure if this is always true.
26-
const remaining = getTimeRemaining() - performance.now();
34+
const remaining = getFrameDeadline() - performance.now();
2735
return remaining > 0 ? remaining : 0;
2836
};
2937
} else {
38+
// Same thing, but with Date.now()
3039
timeRemaining = () => {
31-
// Fallback to Date.now()
32-
const remaining = getTimeRemaining() - Date.now();
40+
if (
41+
firstCallbackNode !== null &&
42+
firstCallbackNode.timesOutAt < currentlyFlushingTime
43+
) {
44+
return 0;
45+
}
46+
const remaining = getFrameDeadline() - Date.now();
3347
return remaining > 0 ? remaining : 0;
3448
};
3549
}
@@ -40,7 +54,7 @@ const deadlineObject = {
4054
};
4155

4256
function ensureHostCallbackIsScheduled(highestPriorityNode) {
43-
if (isPerformingWork) {
57+
if (currentlyFlushingTime !== -1) {
4458
// Don't schedule work yet; wait until the next time we yield.
4559
return;
4660
}
@@ -62,54 +76,96 @@ function computeAbsoluteTimeoutForPriority(currentTime, priority) {
6276
throw new Error('Not yet implemented.');
6377
}
6478

65-
function flushCallback(node) {
66-
// This is already true; only assigning to appease Flow.
67-
firstCallbackNode = node;
79+
function flushFirstCallback() {
80+
const flushedNode = firstCallbackNode;
6881

69-
// Remove the node from the list before calling the callback. That way the
82+
// Remove the flushedNode from the list before calling the callback. That way the
7083
// list is in a consistent state even if the callback throws.
71-
const next = firstCallbackNode.next;
84+
let next = firstCallbackNode.next;
7285
if (firstCallbackNode === next) {
7386
// This is the last callback in the list.
7487
firstCallbackNode = null;
88+
next = null;
7589
} else {
7690
const previous = firstCallbackNode.previous;
7791
firstCallbackNode = previous.next = next;
7892
next.previous = previous;
7993
}
8094

81-
node.next = node.previous = null;
95+
flushedNode.next = flushedNode.previous = null;
8296

8397
// Now it's safe to call the callback.
84-
const callback = node.callback;
85-
callback(deadlineObject);
98+
currentlyFlushingTime = flushedNode.timesOutAt;
99+
const callback = flushedNode.callback;
100+
const continuationCallback = callback(deadlineObject);
101+
102+
if (typeof continuationCallback === 'function') {
103+
const timesOutAt = flushedNode.timesOutAt;
104+
const continuationNode: CallbackNode = {
105+
callback: continuationCallback,
106+
timesOutAt,
107+
next: null,
108+
previous: null,
109+
};
110+
111+
// Insert the new callback into the list, sorted by its timeout.
112+
if (firstCallbackNode === null) {
113+
// This is the first callback in the list.
114+
firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
115+
} else {
116+
let nextAfterContinuation = null;
117+
let node = firstCallbackNode;
118+
do {
119+
if (node.timesOutAt >= timesOutAt) {
120+
// This callback is equal or lower priority than the new one.
121+
nextAfterContinuation = node;
122+
break;
123+
}
124+
node = node.next;
125+
} while (node !== firstCallbackNode);
126+
127+
if (nextAfterContinuation === null) {
128+
// No equal or lower priority callback was found, which means the new
129+
// callback is the lowest priority callback in the list.
130+
nextAfterContinuation = firstCallbackNode;
131+
} else if (nextAfterContinuation === firstCallbackNode) {
132+
// The new callback is the highest priority callback in the list.
133+
firstCallbackNode = continuationNode;
134+
ensureHostCallbackIsScheduled(firstCallbackNode);
135+
}
136+
137+
const previous = nextAfterContinuation.previous;
138+
previous.next = nextAfterContinuation.previous = continuationNode;
139+
continuationNode.next = nextAfterContinuation;
140+
continuationNode.previous = previous;
141+
}
142+
}
86143
}
87144

88145
function flushWork(didTimeout) {
89-
isPerformingWork = true;
90146
deadlineObject.didTimeout = didTimeout;
91147
try {
92-
if (firstCallbackNode !== null) {
93-
if (didTimeout) {
94-
// Flush all the timed out callbacks without yielding.
148+
if (didTimeout) {
149+
// Flush all the timed out callbacks without yielding.
150+
while (
151+
firstCallbackNode !== null &&
152+
firstCallbackNode.timesOutAt <= getCurrentTime()
153+
) {
154+
flushFirstCallback();
155+
}
156+
} else {
157+
// Keep flushing callbacks until we run out of time in the frame.
158+
if (firstCallbackNode !== null) {
95159
do {
96-
flushCallback(firstCallbackNode);
160+
flushFirstCallback();
97161
} while (
98162
firstCallbackNode !== null &&
99-
firstCallbackNode.timesOutAt <= getCurrentTime()
163+
getFrameDeadline() - getCurrentTime() > 0
100164
);
101-
} else {
102-
// Keep flushing callbacks until we run out of time in the frame.
103-
while (
104-
firstCallbackNode !== null &&
105-
getTimeRemaining() - getCurrentTime() > 0
106-
) {
107-
flushCallback(firstCallbackNode);
108-
}
109165
}
110166
}
111167
} finally {
112-
isPerformingWork = false;
168+
currentlyFlushingTime = -1;
113169
if (firstCallbackNode !== null) {
114170
// There's still work remaining. Request another callback.
115171
ensureHostCallbackIsScheduled(firstCallbackNode);
@@ -123,19 +179,16 @@ function unstable_scheduleWork(callback, options) {
123179
const currentTime = getCurrentTime();
124180

125181
let timesOutAt;
126-
if (options !== undefined && options !== null) {
127-
const timeoutOption = options.timeout;
128-
if (timeoutOption !== null && timeoutOption !== undefined) {
129-
// If an explicit timeout is provided, it takes precedence over the
130-
// priority context.
131-
timesOutAt = currentTime + timeoutOption;
132-
} else {
133-
// Compute an absolute timeout using the current priority context.
134-
timesOutAt = computeAbsoluteTimeoutForPriority(
135-
currentTime,
136-
priorityContext,
137-
);
138-
}
182+
if (
183+
options !== undefined &&
184+
options !== null &&
185+
options.timeout !== null &&
186+
options.timeout !== undefined
187+
) {
188+
// Check for an explicit timeout.
189+
timesOutAt = currentTime + options.timeout;
190+
} else if (currentlyFlushingTime !== -1) {
191+
timesOutAt = currentlyFlushingTime;
139192
} else {
140193
timesOutAt = computeAbsoluteTimeoutForPriority(
141194
currentTime,
@@ -280,7 +333,7 @@ if (hasNativePerformanceNow) {
280333

281334
let requestCallback;
282335
let cancelCallback;
283-
let getTimeRemaining;
336+
let getFrameDeadline;
284337

285338
if (typeof window === 'undefined') {
286339
// If this accidentally gets imported in a non-browser environment, fallback
@@ -292,13 +345,13 @@ if (typeof window === 'undefined') {
292345
cancelCallback = () => {
293346
clearTimeout(timeoutID);
294347
};
295-
getTimeRemaining = () => 0;
348+
getFrameDeadline = () => 0;
296349
} else if (window._sched) {
297350
// Dynamic injection, only for testing purposes.
298351
const impl = window._sched;
299352
requestCallback = impl[0];
300353
cancelCallback = impl[1];
301-
getTimeRemaining = impl[2];
354+
getFrameDeadline = impl[2];
302355
} else {
303356
if (typeof console !== 'undefined') {
304357
if (typeof localRequestAnimationFrame !== 'function') {
@@ -332,7 +385,7 @@ if (typeof window === 'undefined') {
332385
let previousFrameTime = 33;
333386
let activeFrameTime = 33;
334387

335-
getTimeRemaining = () => frameDeadline;
388+
getFrameDeadline = () => frameDeadline;
336389

337390
// We use the postMessage trick to defer idle work until after the repaint.
338391
const messageKey =

0 commit comments

Comments
 (0)