API планирования задач с приоритетами
Примечание: Эта функция доступна в Потоках Web Workers.
API планирования задач с приоритетами предоставляет стандартный способ приоритизации всех задач приложения, независимо от того, определены ли они в коде разработчика сайта или в сторонних библиотеках и фреймворках.
Приоритеты задач являются грубыми и основаны на том, блокируют ли задачи взаимодействие пользователя или иным образом влияют на пользовательский опыт, или могут выполняться в фоновом режиме. Разработчики и фреймворки могут реализовывать более точные схемы приоритизации в рамках общих категорий, определенных API.
API основан на обещаниях и поддерживает возможность устанавливать и изменять приоритеты задач, откладывать добавление задач в планировщик, прерывать задачи и отслеживать события изменения приоритета и прерывания.
Концепции и использование
API планирования задач с приоритетами доступен как в окне, так и в рабочих потоках с использованием свойства scheduler глобального объекта.
Основные методы API — scheduler.postTask() и scheduler.yield(). scheduler.postTask() принимает функцию обратного вызова (задачу) и возвращает обещание, которое выполняется со значением возврата функции или отклоняется с ошибкой. scheduler.yield() превращает любую функцию async в задачу, передавая управление главному потоку браузера для выполнения других задач, и выполнение продолжается, когда возвращённое обещание выполняется.
Эти два метода имеют сходную функциональность, но разный уровень управления. scheduler.postTask() более настраиваемый — например, он позволяет явно задавать приоритет задачи и отменять задачу с помощью AbortSignal. scheduler.yield() проще и может быть использован в любой функции await без необходимости предоставления последующей задачи в другой функции.
scheduler.yield()
Чтобы разбить задачи JavaScript с длительным выполнением, чтобы они не блокировали основной поток, вставьте вызов scheduler.yield(), чтобы временно передать управление основному потоку браузера, что создаёт задачу для продолжения выполнения с того места, где она остановилась.
async function slowTask() {
firstHalfOfWork();
await scheduler.yield();
secondHalfOfWork();
}
scheduler.yield() возвращает обещание, с помощью которого можно продолжить выполнение. Это позволяет включить работу, принадлежащую той же функции, без блокировки основного потока при выполнении функции.
scheduler.yield() не принимает аргументов. Задача, которая запускает её продолжение, имеет приоритет по умолчанию user-visible; однако, если scheduler.yield() вызывается внутри функции обратного вызова scheduler.postTask(), она будет наследовать приоритет окружающей задачи.
scheduler.postTask()
Когда scheduler.postTask() вызывается без аргументов, она создаёт задачу с приоритетом по умолчанию user-visible, которую нельзя прервать или изменить её приоритет.
const promise = scheduler.postTask(myTask);
Поскольку метод возвращает обещание, вы можете дождаться его выполнения асинхронно, используя then(), и перехватить ошибки, брошенные функцией обратного вызова задачи (или при прерывании задачи), используя catch. Функция обратного вызова может быть любой функцией (ниже мы демонстрируем стрелочную функцию).
scheduler
.postTask(() => "Task executing")
// Promise resolved: log task result when promise resolves
.then((taskResult) => console.log(`${taskResult}`))
// Promise rejected: log AbortError or errors thrown by task
.catch((error) => console.error(`Error: ${error}`));
Ту же задачу можно ожидать, используя await/async, как показано ниже (обратите внимание, что это выполняется в выражении функции, вызываемом сразу (IIFE)):
(async () => {
try {
const result = await scheduler.postTask(() => "Task executing");
console.log(result);
} catch (error) {
// Log AbortError or error thrown in task function
console.error(`Error: ${error}`);
}
})();
Вы также можете указать объект параметров методу postTask(), если хотите изменить поведение по умолчанию. Параметры:
-
priorityЭто позволяет указать конкретный неизменяемый приоритет. После установки приоритет изменить нельзя. -
signalЭто позволяет указать сигнал, который может быть либоTaskSignal, либоAbortSignal. Сигнал связан с контроллером, который можно использовать для прерывания задачи.TaskSignalтакже может использоваться для установки и изменения приоритета задачи, если задача является изменяемой. -
delayЭто позволяет указать задержку перед добавлением задачи в планировщик в миллисекундах.
Тот же пример, что и выше, с параметром приоритета:
scheduler
.postTask(() => "Task executing", { priority: "user-blocking" })
.then((taskResult) => console.log(`${taskResult}`)) // Log the task result
.catch((error) => console.error(`Error: ${error}`)); // Log any errors
Приоритеты задач
Задачи, запланированные на выполнение, запускаются в порядке приоритета, а затем в порядке их добавления в очередь планировщика.
Существует всего три приоритета, перечисленные ниже (от наивысшего к наименьшему):
user-blocking-
Задачи, которые препятствуют взаимодействию пользователя со страницей. Это включает в себя отрисовку страницы до точки, где она может быть использована, или реакцию на действия пользователя.
user-visible-
Задачи, которые видны пользователю, но не обязательно блокируют действия пользователя. Это может включать отрисовку некритичных частей страницы, таких как некритические изображения или анимации.
Это приоритет по умолчанию для
scheduler.postTask()иscheduler.yield(). background-
Задачи, которые не являются критичными по времени. Это может включать обработку журналов или инициализацию сторонних библиотек, которые не требуются для отрисовки.
Изменяемый и неизменяемый приоритет задачи
Существует много случаев, когда приоритет задачи не нужно изменять, а в других — нужно. Например, загрузка изображения может измениться от задачи background к задаче user-visible при прокрутке карусели в область просмотра.
Приоритет задачи может быть статическим (неизменяемым) или динамическим (изменяемым) в зависимости от аргументов, переданных в Scheduler.postTask().
Приоритет задачи неизменяемый, если значение указано в аргументе options.priority. Указанное значение будет использоваться для приоритета задачи и изменить его нельзя.
Приоритет изменяемый только если TaskSignal передан в аргумент options.signal и options.priority не задан. В этом случае задача будет использовать свой начальный приоритет из signal приоритета, а приоритет можно впоследствии изменить, вызвав TaskController.setPriority() для контроллера, связанного с сигналом.
Если приоритет не задан с options.priority или путём передачи TaskSignal в options.signal, он по умолчанию будет user-visible (и по определению неизменяемым).
Обратите внимание, что задача, которая должна быть прервана, должна установить options.signal либо в TaskSignal, либо в AbortSignal. Однако для задачи с неизменяемым приоритетом, AbortSignal более чётко указывает, что приоритет задачи изменить нельзя с помощью сигнала.
Давайте рассмотрим пример, чтобы продемонстрировать, что мы имеем в виду. Когда у вас несколько задач с примерно одинаковым приоритетом, имеет смысл разбить их на отдельные функции для улучшения поддержки, отладки и многих других причин.
Например:
function main() {
a();
b();
c();
d();
e();
}
Однако такая структура не помогает при блокировке основного потока. Поскольку все пять задач выполняются внутри одной главной функции, браузер выполняет их все как одну задачу.
Для решения этой проблемы мы периодически запускаем функцию, чтобы заставить код передать управление главному потоку. Это означает, что наш код разделен на несколько задач, между выполнением которых браузеру предоставляется возможность обрабатывать задачи высокого приоритета, такие как обновление пользовательского интерфейса. Общий шаблон для этой функции использует setTimeout() для отсрочки выполнения в отдельной задаче:
function yield() {
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
Это можно использовать в схеме планировщика задач, подобной этой, чтобы передавать управление главному потоку после выполнения каждой задачи:
async function main() {
// Create an array of functions to run
const tasks = [a, b, c, d, e];
// Loop over the tasks
while (tasks.length > 0) {
// Shift the first task off the tasks array
const task = tasks.shift();
// Run the task
task();
// Yield to the main thread
await yield();
}
}
Для дальнейшего улучшения можно использовать Scheduler.yield, когда это возможно, чтобы позволить этому коду продолжить выполнение перед другими менее критическими задачами в очереди:
function yield() {
// Use scheduler.yield if it exists:
if ("scheduler" in window && "yield" in scheduler) {
return scheduler.yield();
}
// Fall back to setTimeout:
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
Интерфейсы
Scheduler-
Содержит методы
postTask()иyield()для добавления задач с приоритетами, которые нужно запланировать. Экземпляр этого интерфейса доступен вWindowилиWorkerGlobalScopeглобальных объектах (globalThis.scheduler). TaskController-
Поддерживает как прерывание задачи, так и изменение её приоритета.
TaskSignal-
Объект сигнала, который позволяет прервать задачу и изменить её приоритет, если это необходимо, с помощью объекта
TaskController. TaskPriorityChangeEvent-
Интерфейс для события
prioritychange, которое отправляется при изменении приоритета задачи.
Примечание: Если приоритет задачи никогда не нужно изменять, можно использовать AbortController и связанный с ним AbortSignal вместо TaskController и TaskSignal.
Расширения других интерфейсов
-
Window.schedulerиWorkerGlobalScope.scheduler -
Эти свойства являются точками входа для использования метода
Scheduler.postTask()в окне или в области видимости рабочего потока соответственно.
Примеры
Обратите внимание, что в примерах ниже используется myLog() для записи в текстовую область. Код для области лога и метода обычно скрыт, чтобы не отвлекать от более релевантного кода.
// hidden logger code - simplifies example
let log = document.getElementById("log");
function myLog(text) {
log.textContent += `${text}\n`;
}
Проверка наличия функции
Проверьте, поддерживается ли планирование задач с приоритетами, проверив свойство scheduler в глобальной области видимости.
Приведённый ниже код выводит "Функция: Поддерживается", если API поддерживается в этом браузере.
// Check that feature is supported
if ("scheduler" in globalThis) {
myLog("Feature: Supported");
} else {
myLog("Feature: NOT Supported");
}
Базовое использование
Задачи публикуются с помощью Scheduler.postTask(), указанием функции обратного вызова (задачи) в первом аргументе и необязательным вторым аргументом, который может использоваться для указания приоритета задачи, сигнала и/или задержки. Метод возвращает Promise, который разрешается значением возвращаемым функцией обратного вызова или отклоняется с ошибкой прерывания или ошибкой, выброшенной в функции.
Поскольку он возвращает промис, Scheduler.postTask() может быть присоединён к другим промисам. Ниже мы показываем, как дождаться разрешения промиса с помощью then. Это использует приоритет по умолчанию (user-visible).
// A function that defines a task
function myTask() {
return "Task 1: user-visible";
}
if ("scheduler" in this) {
// Post task with default priority: 'user-visible' (no other options)
// When the task resolves, Promise.then() logs the result.
scheduler.postTask(myTask).then((taskResult) => myLog(`${taskResult}`));
}
Метод также может быть использован с await внутри асинхронной функции. Ниже показано, как можно использовать этот подход, чтобы дождаться завершения user-blocking задачи.
function myTask2() {
return "Task 2: user-blocking";
}
async function runTask2() {
const result = await scheduler.postTask(myTask2, {
priority: "user-blocking",
});
myLog(result); // Logs 'Task 2: user-blocking'.
}
runTask2();
В некоторых случаях вам может не потребоваться ожидание завершения. Для простоты во многих примерах здесь просто регистрируется результат по мере выполнения задачи.
// A function that defines a task
function myTask3() {
myLog("Task 3: user-visible");
}
if ("scheduler" in this) {
// Post task and log result when it runs
scheduler.postTask(myTask3);
}
Лог ниже отображает вывод трёх задач выше. Обратите внимание, что порядок их выполнения зависит от приоритета, а затем от порядка объявления.
Постоянные приоритеты
Приоритеты задач могут быть установлены с помощью параметра priority в необязательном втором аргументе. Приоритеты, установленные таким образом, являются неизменяемыми (не могут быть изменены).
Ниже мы публикуем две группы из трёх задач, каждый член в обратном порядке приоритета. Последняя задача имеет приоритет по умолчанию. При запуске каждая задача просто регистрирует свой ожидаемый порядок (мы не ждём результата, потому что нам это не нужно, чтобы показать порядок выполнения).
if ("scheduler" in this) {
// three tasks, in reverse order of priority
scheduler.postTask(() => myLog("bkg 1"), { priority: "background" });
scheduler.postTask(() => myLog("usr-vis 1"), { priority: "user-visible" });
scheduler.postTask(() => myLog("usr-blk 1"), { priority: "user-blocking" });
// three more tasks, in reverse order of priority
scheduler.postTask(() => myLog("bkg 2"), { priority: "background" });
scheduler.postTask(() => myLog("usr-vis 2"), { priority: "user-visible" });
scheduler.postTask(() => myLog("usr-blk 2"), { priority: "user-blocking" });
// Task with default priority: user-visible
scheduler.postTask(() => myLog("usr-vis 3 (default)"));
}
Вывод ниже показывает, что задачи выполняются в порядке приоритета, а затем в порядке объявления.
Изменение приоритетов задач
Приоритеты задач также могут принимать начальное значение от TaskSignal, переданного в postTask() в необязательном втором аргументе. Если они установлены таким образом, приоритет задачи может быть изменён с помощью контроллера, связанного с сигналом.
Примечание: Установка и изменение приоритетов задач с использованием сигнала работает только тогда, когда аргумент options.priority для postTask() не задан и когда options.signal является TaskSignal (а не AbortSignal).
Код ниже сначала показывает, как создать TaskController, установив начальный приоритет сигнала до user-blocking в конструкторе TaskController().
Затем код использует addEventListener() для добавления обработчика событий к сигналу контроллера (можно использовать свойство TaskSignal.onprioritychange для добавления обработчика событий). Обработчик событий использует previousPriority в событии для получения исходного приоритета и TaskSignal.priority в целевом объекте события для получения нового/текущего приоритета.
Затем задача публикуется, передавая сигнал, и сразу же приоритет изменяется до background вызовом TaskController.setPriority() на контроллере.
if ("scheduler" in this) {
// Create a TaskController, setting its signal priority to 'user-blocking'
const controller = new TaskController({ priority: "user-blocking" });
// Listen for 'prioritychange' events on the controller's signal.
controller.signal.addEventListener("prioritychange", (event) => {
const previousPriority = event.previousPriority;
const newPriority = event.target.priority;
myLog(`Priority changed from ${previousPriority} to ${newPriority}.`);
});
// Post task using the controller's signal.
// The signal priority sets the initial priority of the task
scheduler.postTask(() => myLog("Task 1"), { signal: controller.signal });
// Change the priority to 'background' using the controller
controller.setPriority("background");
}
Вывод ниже демонстрирует, что приоритет был успешно изменён на background с user-blocking. Обратите внимание, что в этом случае приоритет изменяется до выполнения задачи, но его также можно было изменить во время выполнения задачи.
Прерывание задач
Задачи могут быть прерваны с помощью либо TaskController, либо AbortController точно так же. Единственное отличие состоит в том, что необходимо использовать TaskController, если вы также хотите установить приоритет задачи.
Код ниже создаёт контроллер и передаёт его сигнал в задачу. Затем задача немедленно прерывается. Это приводит к отклонению промиса с AbortError, что перехватывается в блоке catch и регистрируется. Обратите внимание, что мы также могли бы прослушать событие abort, сгенерированное по TaskSignal или AbortSignal и записать прерывание там.
if ("scheduler" in this) {
// Declare a TaskController with default priority
const abortTaskController = new TaskController();
// Post task passing the controller's signal
scheduler
.postTask(() => myLog("Task executing"), {
signal: abortTaskController.signal,
})
.then((taskResult) => myLog(`${taskResult}`)) // This won't run!
.catch((error) => myLog(`Error: ${error}`)); // Log the error
// Abort the task
abortTaskController.abort();
}
Лог ниже отображает прерванную задачу.
Задержка задач
Задачи могут быть отложены, указав целое число миллисекунд в параметре options.delay для postTask(). Это фактически добавляет задачу в очередь с приоритетами по таймауту, как это может быть создано с помощью setTimeout(). delay — это минимальное время, прошедшее до того, как задача будет добавлена в планировщик; оно может быть больше.
Код ниже показывает две добавленные задачи (как стрелочные функции) с задержкой.
if ("scheduler" in this) {
// Post task as arrow function with delay of 2 seconds
scheduler
.postTask(() => "Task delayed by 2000ms", { delay: 2000 })
.then((taskResult) => myLog(`${taskResult}`));
scheduler
.postTask(() => "Next task should complete in about 2000ms", { delay: 1 })
.then((taskResult) => myLog(`${taskResult}`));
}
Обновите страницу. Обратите внимание, что вторая строка появляется в логе примерно через 2 секунды.
Спецификации
| Спецификация |
|---|
| Планирование задач с приоритетами # scheduler |
| Раннее обнаружение событий ввода # the-scheduling-interface |
Совместимость с браузерами
| Рабочий стол | Мобильный | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Chrome | Edge | Firefox | Opera | Safari | Chrome Android | Firefox для Android | Opera Android | Safari на iOS | Samsung Internet | WebView Android | |
Prioritized_Task_Scheduling_API |
87 | 87 | Нет | 73 | Нет | 87 | Нет | 62 | Нет | 14.0 | 87 |
isInputPending |
87 | 87 | Нет | 73 | Нет | 87 | Нет | 62 | Нет | 14.0 | 87 |
| Десктоп | Мобильное | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Chrome | Edge | Firefox | Opera | Safari | Chrome Android | Firefox for Android | Opera Android | Safari on IOS | Samsung Internet | WebView Android | |
Prioritized_Task_Scheduling_API |
94 | 94 | 101 | 80 | Нет | 94 | Нет | 66 | Нет | 17.0 | 94 |
postTask |
94 | 94 | 101 | 80 | Нет | 94 | Нет | 66 | Нет | 17.0 | 94 |
yield |
129 | 129 | Нет | 115 | Нет | 129 | Нет | 86 | Нет | Нет | 129 |
api.Scheduler
Таблицы BCD загружаются только в браузере
api.Scheduling
Таблицы BCD загружаются только в браузере
См. также
- Создание более быстрого веб-опыта с планировщиком postTask в блоге Airbnb (2021)
- Оптимизация длительных задач на web.dev (2022)
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API