Skip to content

Commit fd1f95b

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 fd1f95b

File tree

2 files changed

+261
-40
lines changed

2 files changed

+261
-40
lines changed

packages/schedule/src/Schedule.js

Lines changed: 90 additions & 32 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);
@@ -136,6 +192,8 @@ function unstable_scheduleWork(callback, options) {
136192
priorityContext,
137193
);
138194
}
195+
} else if (currentlyFlushingTime !== -1) {
196+
timesOutAt = currentlyFlushingTime;
139197
} else {
140198
timesOutAt = computeAbsoluteTimeoutForPriority(
141199
currentTime,
@@ -280,7 +338,7 @@ if (hasNativePerformanceNow) {
280338

281339
let requestCallback;
282340
let cancelCallback;
283-
let getTimeRemaining;
341+
let getFrameDeadline;
284342

285343
if (typeof window === 'undefined') {
286344
// If this accidentally gets imported in a non-browser environment, fallback
@@ -292,13 +350,13 @@ if (typeof window === 'undefined') {
292350
cancelCallback = () => {
293351
clearTimeout(timeoutID);
294352
};
295-
getTimeRemaining = () => 0;
353+
getFrameDeadline = () => 0;
296354
} else if (window._sched) {
297355
// Dynamic injection, only for testing purposes.
298356
const impl = window._sched;
299357
requestCallback = impl[0];
300358
cancelCallback = impl[1];
301-
getTimeRemaining = impl[2];
359+
getFrameDeadline = impl[2];
302360
} else {
303361
if (typeof console !== 'undefined') {
304362
if (typeof localRequestAnimationFrame !== 'function') {
@@ -332,7 +390,7 @@ if (typeof window === 'undefined') {
332390
let previousFrameTime = 33;
333391
let activeFrameTime = 33;
334392

335-
getTimeRemaining = () => frameDeadline;
393+
getFrameDeadline = () => frameDeadline;
336394

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

0 commit comments

Comments
 (0)