Skip to content

Commit

Permalink
Extract DOM-specific logic into separate module
Browse files Browse the repository at this point in the history
- ScheduleDOM contains all DOM-specific scheduling. It's essentially a
requestIdleCallback polyfill.
- Schedule is host agnostic.

There's no concept of a "host config" as yet because we currently target
the DOM. However, splitting the logic out this way still has
architectural benefits. ScheduleDOM is now almost identical to what it
looked like back when this module was part of React. It only needs to
handle a single callback; all queuing and ordering is handled
by Schedule.

Practically, it also makes testing easier, because we can use a Jest
mock of ScheduleDOM to test non-DOM-specific features, a la our React
Noop tests.
  • Loading branch information
acdlite committed Sep 6, 2018
1 parent 8679f4d commit 76d0ecf
Show file tree
Hide file tree
Showing 5 changed files with 502 additions and 325 deletions.
2 changes: 1 addition & 1 deletion packages/react-reconciler/src/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ import {
import {Dispatcher} from './ReactFiberDispatcher';

export type Deadline = {
timeRemaining: () => number,
timeRemaining(): number,
didTimeout: boolean,
};

Expand Down
216 changes: 215 additions & 1 deletion packages/schedule/src/Schedule.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,218 @@
* @flow
*/

export * from './ScheduleDOM';
import {
requestWork,
cancelWork,
getCurrentTime,
getTimeRemainingInFrame,
} from './ScheduleDOM';

type Deadline = {
timeRemaining(): number,
didTimeout: boolean,
};

type Options = {
timeout?: number,
};

opaque type CallbackNode = {|
callback: Deadline => mixed,
timesOutAt: number,
next: CallbackNode,
previous: CallbackNode,
|};

// TODO: Currently there's only a single priority level, Deferred. Need to add
// additional priorities (serial, offscreen).
type Priority = 0;

const Deferred = 0;

const DEFERRED_TIMEOUT = 5000;

// Callbacks are stored as a circular, doubly linked list.
let firstCallbackNode: CallbackNode | null = null;

let priorityContext: Priority = Deferred;
let isPerformingWork: boolean = false;

let isHostCallbackScheduled: boolean = false;

const deadlineObject: Deadline = {
timeRemaining: getTimeRemainingInFrame,
didTimeout: false,
};

function ensureHostCallbackIsScheduled(highestPriorityNode) {
if (isPerformingWork) {
// Don't schedule work yet; wait until the next time we yield.
return;
}
const timesOutAt = highestPriorityNode.timesOutAt;
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true;
} else {
// Cancel the existing work.
cancelWork();
}
// Schedule work using the highest priority callback's timeout.
requestWork(flushWork, timesOutAt);
}

function computeAbsoluteTimeoutForPriority(currentTime, priority) {
if (priority === Deferred) {
return currentTime + DEFERRED_TIMEOUT;
}
throw new Error('Not yet implemented.');
}

function flushCallback(node) {
// This is already true; only assigning to appease Flow.
firstCallbackNode = node;

// Remove the node from the list before calling the callback. That way the
// list is in a consistent state even if the callback throws.
const next = firstCallbackNode.next;
if (firstCallbackNode === next) {
// This is the last callback in the list.
firstCallbackNode = null;
} else {
const previous = firstCallbackNode.previous;
firstCallbackNode = previous.next = next;
next.previous = previous;
}

(node: any).next = (node: any).previous = null;

// Now it's safe to call the callback.
const callback = node.callback;
callback(deadlineObject);
}

function flushWork(didTimeout) {
isPerformingWork = true;
deadlineObject.didTimeout = didTimeout;
try {
if (firstCallbackNode !== null) {
if (didTimeout) {
// Flush all the timed out callbacks without yielding.
do {
flushCallback(firstCallbackNode);
} while (
firstCallbackNode !== null &&
firstCallbackNode.timesOutAt <= getCurrentTime()
);
} else {
// Keep flushing callbacks until we run out of time in the frame.
while (firstCallbackNode !== null && getTimeRemainingInFrame() > 0) {
flushCallback(firstCallbackNode);
}
}
}
} finally {
isPerformingWork = false;
if (firstCallbackNode !== null) {
// There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled(firstCallbackNode);
} else {
isHostCallbackScheduled = false;
}
}
}

export function unstable_scheduleWork(
callback: Deadline => mixed,
options?: Options,
): CallbackNode {
const currentTime = getCurrentTime();

let timesOutAt;
if (options !== undefined && options !== null) {
const timeoutOption = options.timeout;
if (timeoutOption !== null && timeoutOption !== undefined) {
// If an explicit timeout is provided, it takes precedence over the
// priority context.
timesOutAt = currentTime + timeoutOption;
} else {
// Compute an absolute timeout using the current priority context.
timesOutAt = computeAbsoluteTimeoutForPriority(
currentTime,
priorityContext,
);
}
} else {
timesOutAt = computeAbsoluteTimeoutForPriority(
currentTime,
priorityContext,
);
}

const newNode: CallbackNode = {
callback,
timesOutAt,
next: (null: any),
previous: (null: any),
};

// Insert the new callback into the list, sorted by its timeout.
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled(firstCallbackNode);
} else {
let next = null;
let node = firstCallbackNode;
do {
if (node.timesOutAt > timesOutAt) {
// This callback is lower priority than the new one.
next = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);

if (next === null) {
// No lower priority callback was found, which means the new callback is
// the lowest priority callback in the list.
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
// The new callback is the highest priority callback in the list.
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled(firstCallbackNode);
}

const previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}

return newNode;
}

export function unstable_cancelScheduledWork(callbackNode: CallbackNode): void {
const next = callbackNode.next;
if (next === null) {
// Already cancelled.
return;
}

if (next === callbackNode) {
// This is the only scheduled callback. Clear the list.
firstCallbackNode = null;
} else {
// Remove the callback from its position in the list.
if (callbackNode === firstCallbackNode) {
firstCallbackNode = next;
}
const previous = callbackNode.previous;
previous.next = next;
next.previous = previous;
}

callbackNode.next = callbackNode.previous = (null: any);
}

export const unstable_now = getCurrentTime;

0 comments on commit 76d0ecf

Please sign in to comment.