Task
A task is Scheduler's internal representation of one unit of work. React gives Scheduler a callback and a priority; Scheduler wraps that information in a task object so it can order, delay, execute, continue, or cancel the work later.
The task type in this project is:
export type Task = {
id: number;
callback: Callback | null;
priorityLevel: PriorityLevel;
startTime: number;
expirationTime: number;
sortIndex: number;
};
Each field supports a specific part of Scheduler's behavior:
id: preserves insertion order when two tasks have the same sort value.callback: the function Scheduler will execute.priorityLevel: the priority React assigned to the work.startTime: the earliest time the task is allowed to run.expirationTime: the deadline after which the task is considered expired.sortIndex: the value used by the heap to order the task.
callback
callback is the function Scheduler eventually executes. It is passed into Scheduler through scheduleCallback:
scheduleCallback(NormalPriority, didUserCallbackTimeout => {
// React work happens here.
});
The callback receives one boolean argument:
type Callback = (arg: boolean) => Callback | null | undefined;
That argument is usually called didUserCallbackTimeout. Scheduler computes it by comparing the task's expirationTime with the current time:
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
const continuationCallback = callback(didUserCallbackTimeout);
If didUserCallbackTimeout is true, the task has passed its deadline. React can use that signal to treat the work as more urgent.
The callback can also return another function. That returned function is called a continuation:
scheduleCallback(NormalPriority, didUserCallbackTimeout => {
doPartOfTheWork(didUserCallbackTimeout);
if (hasMoreWork()) {
return didUserCallbackTimeout => {
doTheRemainingWork(didUserCallbackTimeout);
};
}
return null;
});
If the callback returns a function, Scheduler stores that function back on the same task:
currentTask.callback = continuationCallback;
The task stays in the queue and can continue later. If the callback returns null or undefined, Scheduler treats the task as complete and removes it from taskQueue.
priorityLevel
priorityLevel describes how urgent the task is. This project defines the priority values like this:
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
The public Scheduler API in this project exports these usable priorities:
ImmediatePriority: highest urgency; expires immediately.UserBlockingPriority: urgent work related to user interaction.NormalPriority: default priority for ordinary work.LowPriority: work that can wait longer.IdlePriority: work that should run only when nothing more important is pending.
NoPriority exists in the priority module as a sentinel value, but it is not exported from the Scheduler entry point in this implementation. It is not a normal priority you schedule work with here.
Priority is not used directly as the heap sort value. If Scheduler sorted only by priority, two tasks with the same priority would still need a fair order, and Scheduler would lose information about when a task becomes overdue. Instead, priority is converted into a timeout, and that timeout is used to compute expirationTime.
expirationTime
expirationTime is the task's deadline. Scheduler uses it for two things:
- Ordering ready tasks in
taskQueue. - Computing
didUserCallbackTimeoutwhen the task runs.
The formula is:
expirationTime = startTime + timeout;
The timeout depends on the priority:
case ImmediatePriority:
timeout = -1;
break;
case UserBlockingPriority:
timeout = userBlockingPriorityTimeout;
break;
case IdlePriority:
timeout = maxSigned31BitInt;
break;
case LowPriority:
timeout = lowPriorityTimeout;
break;
case NormalPriority:
default:
timeout = normalPriorityTimeout;
break;
In this project, the main timeout constants are:
export const userBlockingPriorityTimeout = 250;
export const normalPriorityTimeout = 5000;
export const lowPriorityTimeout = 10000;
IdlePriority uses maxSigned31BitInt, which is 1073741823, a very large timeout. ImmediatePriority uses -1, so an immediate task is already expired as soon as it is created.
For example, if the current time is 1000 and a user-blocking task is scheduled without delay:
startTime = 1000;
timeout = 250;
expirationTime = 1250;
If that task runs at time 1200, it has not expired:
didUserCallbackTimeout = 1250 <= 1200; // false
If it runs at time 1300, it has expired:
didUserCallbackTimeout = 1250 <= 1300; // true
An immediate task behaves differently:
startTime = 1000;
timeout = -1;
expirationTime = 999;
Because the expiration time is already in the past, didUserCallbackTimeout will be true when the task executes.
The key point is that expirationTime is not a promise that the task will run at that exact time. It is a deadline used for ordering and urgency. Scheduler still runs tasks only when the host environment gives it a chance to run JavaScript.
id
id is an incrementing number:
var taskIdCounter = 1;
Every new task receives the next id:
id: taskIdCounter++;
The heap compares sortIndex first. If two tasks have the same sortIndex, it compares id:
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
This keeps ordering stable. If two tasks have the same deadline or start time, the older task is processed first.
startTime
startTime is the earliest time a task is allowed to run.
For a normal task without a positive delay, startTime is the current time:
startTime = currentTime;
For a delayed task, startTime is computed from the current time plus the delay:
startTime = currentTime + delay;
For example, if the current time is 1000 and the task is scheduled with a 2000 millisecond delay:
startTime = 3000;
That delayed task goes into timerQueue, not taskQueue, because it is not ready yet. When the current time reaches its startTime, Scheduler moves it into taskQueue.
The expiration time is still based on startTime:
expirationTime = startTime + timeout;
That matters because a delayed task should not become expired before it is even eligible to run.
sortIndex
sortIndex is the field the heap uses to order tasks.
For ready tasks in taskQueue, sortIndex is the task's expirationTime:
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
For delayed tasks in timerQueue, sortIndex is the task's startTime:
newTask.sortIndex = startTime;
push(timerQueue, newTask);
When a delayed task becomes ready, Scheduler moves it from timerQueue to taskQueue and changes its sortIndex:
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
So sortIndex does not have one fixed meaning. Its meaning depends on which queue the task is currently in:
| Queue | sortIndex means |
|---|---|
timerQueue | startTime |
taskQueue | expirationTime |
Summary
A task is small, but each field has a precise role:
callbackis the work to run and may return a continuation.priorityLeveldetermines the timeout used to compute the deadline.startTimedecides when the task becomes eligible to run.expirationTimedecides when the task becomes overdue.sortIndexorders the task inside the active queue.idbreaks ties and preserves insertion order.
Understanding this object makes the rest of Scheduler easier to follow: queues store tasks, workLoop executes their callbacks, and Scheduler updates their fields as tasks move from delayed work to ready work.