API задач в фоновом режиме
Ограниченная доступность
Эта функция не относится к Baseline, так как не работает в некоторых из наиболее широко используемых браузеров.
API кооперативного планирования задач в фоновом режиме (также известный как API задач в фоновом режиме или API requestIdleCallback()) предоставляет возможность добавлять задачи в очередь для автоматического выполнения агентом пользователя, когда он определяет наличие свободного времени.
Примечание: Этот API не доступен в Web Workers.
Основные понятия и использование
Основной поток веб-браузера сосредоточен вокруг его цикла событий. Этот код отображает все ожидающие обновления для Document, отображаемого в данный момент, выполняет любой необходимый JavaScript-код страницы, принимает события от устройств ввода и направляет эти события элементам, которые должны их получить. Кроме того, цикл событий обрабатывает взаимодействия с операционной системой, обновления пользовательского интерфейса браузера и т. д. Это очень загруженный участок кода, и ваш основной JavaScript-код может выполняться прямо в этом потоке вместе со всем этим. Безусловно, большинство, если не все, код, способный вносить изменения в DOM, выполняется в основном потоке, так как изменения пользовательского интерфейса обычно доступны только в основном потоке.
Поскольку обработка событий и обновление экрана являются одними из наиболее очевидных способов, которыми пользователи замечают проблемы с производительностью, важно, чтобы ваш код был добросовестным гражданином веб-среды и помогал предотвращать задержки в выполнении цикла событий. Раньше это можно было делать только путем написания максимально эффективного кода и перекладывания как можно большей работы на потоки. Window.requestIdleCallback() позволяет активно участвовать в обеспечении плавного функционирования цикла событий браузера, позволяя браузеру указывать вашему коду, сколько времени можно безопасно использовать, не вызывая задержки в системе. Если вы остаетесь в пределах заданного предела, вы можете значительно улучшить пользовательский опыт.
Максимальное использование обратных вызовов при бездействии
Поскольку обратные вызовы при бездействии предназначены для того, чтобы ваш код мог сотрудничать с циклом событий, чтобы гарантировать максимальное использование ресурсов системы без перегрузки, что может привести к задержкам или другим проблемам с производительностью, следует тщательно подходить к их использованию.
-
Используйте обратные вызовы при бездействии для задач, которые не имеют высокого приоритета. Поскольку вы не знаете, сколько обратных вызовов установлено и насколько загружена система пользователя, вы не знаете, как часто будет выполняться ваш обратный вызов (если вы не задали
timeout). Нет гарантии, что каждый проход цикла событий (или даже каждый цикл обновления экрана) будет включать выполнение любых обратных вызовов при бездействии; если цикл событий использует все доступное время, вы не повезло (опять же, если вы не использовалиtimeout). -
Обратные вызовы при бездействии должны по возможности не превышать отведенное время. Хотя браузер, ваш код и веб-среда в целом будут работать нормально, если вы превысите заданный лимит времени (даже если вы значительно его превысите), это ограничение предназначено для того, чтобы оставить системе достаточно времени для завершения текущего прохода цикла событий и перехода к следующему, не вызывая задержки другого кода или эффектов анимации. В настоящее время
timeRemaining()имеет верхний предел в 50 миллисекунд, но на самом деле у вас часто будет меньше времени, поскольку цикл событий может уже использовать это время на сложных сайтах, с расширениями браузера, требующими процессорного времени и так далее. -
Избегайте внесения изменений в DOM внутри обратного вызова при бездействии. К моменту выполнения вашего обратного вызова текущий кадр уже завершил отрисовку, и все обновления макета и вычисления завершены. Если вы внесете изменения, влияющие на макет, вы можете вызвать ситуацию, в которой браузеру придется остановить работу и выполнить перерасчеты, которые в противном случае были бы не нужны. Если вашему обратному вызову необходимо изменить DOM, следует использовать
Window.requestAnimationFrame()для планирования этого. -
Избегайте задач, время выполнения которых невозможно предсказать. Ваш обратный вызов при бездействии должен избегать выполнения действий, которые могут занять непредсказуемое количество времени. Например, следует избегать любых действий, которые могут повлиять на макет. Также следует избегать разрешения или отклонения
Promise, поскольку это вызовет обработчик разрешения или отклонения этой промисы сразу по возвращении вашего обратного вызова. - Используйте таймауты при необходимости, но только тогда, когда это необходимо. Использование таймаутов может гарантировать выполнение вашего кода в нужное время, но оно также может привести к задержкам или подергиваниям анимации, требуя от браузера вызвать вас, когда времени недостаточно для выполнения без нарушения производительности.
Интерфейсы
API задач в фоновом режиме добавляет только один новый интерфейс:
IdleDeadline-
Объект этого типа передаётся обратному вызову при бездействии, чтобы предоставить оценку ожидаемой длительности периода бездействия, а также информацию о том, выполняется ли обратный вызов из-за истечения срока его ожидания.
Интерфейс Window также дополняется этим API, чтобы предложить новые методы requestIdleCallback() и cancelIdleCallback().
Пример
В этом примере мы рассмотрим, как использовать requestIdleCallback() для выполнения ресурсоёмких задач низкого приоритета в моменты бездействия браузера. Кроме того, этот пример демонстрирует, как планировать обновления содержимого документа с помощью requestAnimationFrame().
Ниже представлен только HTML и JavaScript для этого примера. CSS не показан, так как он не особенно важен для понимания этой функциональности.
HTML
Для того чтобы понимать, что мы пытаемся сделать, давайте посмотрим на HTML. Он устанавливает поле (id="container") для отображения прогресса операции (ведь никогда не знаешь, сколько времени займёт декодирование «квантовых тахионных излучений нитей», например), а также второе основное поле (id="logBox") для отображения текстового вывода.
<p>
Demonstration of using cooperatively scheduled background tasks using the
<code>requestIdleCallback()</code> method.
</p>
<div id="container">
<div class="label">Decoding quantum filament tachyon emissions…</div>
<progress id="progress" value="0"></progress>
<button class="button" id="startButton">Start</button>
<div class="label counter">
Task <span id="currentTaskNumber">0</span> of
<span id="totalTaskCount">0</span>
</div>
</div>
<div id="logBox">
<div class="logHeader">Log</div>
<div id="log"></div>
</div>
Поле прогресса использует элемент <progress> для отображения прогресса, а также метку с разделами, которые изменяются для представления числовой информации о прогрессе. Кроме того, есть кнопка «Начать» (творчески названная «startButton»), которую пользователь будет использовать для запуска обработки данных.
JavaScript
Теперь, когда структура документа определена, создайте код JavaScript, который выполнит работу. Цель: добавить запросы для вызова функций в очередь с обратным вызовом при бездействии, который выполняет эти функции всякий раз, когда система бездействует достаточно долго, чтобы сделать прогресс.
Объявление переменных
const taskList = []; let totalTaskCount = 0; let currentTaskNumber = 0; let taskHandle = null;
Эти переменные используются для управления списком задач, ожидающих выполнения, а также информацией о состоянии очереди задач и ее выполнении:
-
taskList— этоArrayобъектов, каждый из которых представляет одну задачу, ожидающую выполнения. -
totalTaskCount— счетчик количества задач, добавленных в очередь; он будет только увеличиваться, никогда не уменьшаться. Мы используем это для расчета отображения прогресса в процентах от общего объема работы. -
currentTaskNumberиспользуется для отслеживания того, сколько задач было обработано на данный момент. -
taskHandle— ссылка на задачу, которая обрабатывается в данный момент.
const totalTaskCountElem = document.getElementById("totalTaskCount");
const currentTaskNumberElem = document.getElementById("currentTaskNumber");
const progressBarElem = document.getElementById("progress");
const startButtonElem = document.getElementById("startButton");
const logElem = document.getElementById("log");
Далее у нас есть переменные, которые ссылаются на элементы DOM, с которыми нам нужно взаимодействовать. Эти элементы:
-
totalTaskCountElem— это<span>, который мы используем для вставки общего количества созданных задач в отображение состояния в поле прогресса. -
currentTaskNumberElem— элемент, используемый для отображения количества обработанных задач. -
progressBarElem— элемент<progress>, отображающий процент обработанных задач. -
startButtonElem— кнопка запуска. -
logElem—<div>, куда мы будем вставлять сообщения об отлове.
let logFragment = null; let statusRefreshScheduled = false;
Наконец, мы задаем несколько переменных для других элементов:
-
logFragmentбудет использоваться для храненияDocumentFragment, сгенерированного нашими функциями регистрации для создания содержимого для добавления в журнал, когда будет рендериться следующий кадр анимации. -
statusRefreshScheduledиспользуется для отслеживания того, планировали ли мы уже обновление поля состояния для следующего кадра, чтобы делать это только один раз за кадр.
Управление очередью задач
Далее, давайте рассмотрим способ управления задачами, которые необходимо выполнить. Мы будем использовать очередь задач FIFO, которую будем выполнять по мере возможности во время периода обратного вызова при бездействии.
Добавление задач в очередь
Сначала нам нужна функция, которая добавляет задачи в очередь для последующего выполнения. Эта функция, enqueueTask(), выглядит следующим образом:
function enqueueTask(taskHandler, taskData) {
taskList.push({
handler: taskHandler,
data: taskData,
});
totalTaskCount++;
if (!taskHandle) {
taskHandle = requestIdleCallback(runTaskQueue, { timeout: 1000 });
}
scheduleStatusRefresh();
}
enqueueTask() принимает два параметра:
-
taskHandler— функция, которая будет вызвана для обработки задачи. -
taskData— объект, который передается обработчику задачи в качестве входного параметра, чтобы позволить задаче получать пользовательские данные.
Для добавления задачи в очередь мы push объект в массив taskList; объект содержит значения taskHandler и taskData под именами handler и data соответственно, затем увеличивает totalTaskCount, что отражает общее количество задач, которые когда-либо были добавлены в очередь (мы не уменьшаем его, когда задачи удаляются из очереди).
Затем мы проверяем, был ли уже создан обратный вызов при бездействии; если taskHandle равно 0, мы знаем, что обратного вызова при бездействии еще нет, поэтому мы вызываем requestIdleCallback() для его создания. Он настроен так, чтобы вызывать функцию с именем runTaskQueue(), которую мы рассмотрим вскоре, с интервалом timeout в 1 секунду, чтобы он запускался по крайней мере один раз в секунду, даже если доступного реального времени бездействия нет.
Запуск задач
Наш обработчик обратного вызова при бездействии, runTaskQueue(), вызывается, когда браузер определяет, что есть достаточно времени бездействия, чтобы позволить нам выполнить некоторую работу, или истекает наш таймаут в одну секунду. Эта функция предназначена для выполнения наших задач в очереди.
function runTaskQueue(deadline) {
while (
(deadline.timeRemaining() > 0 || deadline.didTimeout) &&
taskList.length
) {
const task = taskList.shift();
currentTaskNumber++;
task.handler(task.data);
scheduleStatusRefresh();
}
if (taskList.length) {
taskHandle = requestIdleCallback(runTaskQueue, { timeout: 1000 });
} else {
taskHandle = 0;
}
}
Ядро runTaskQueue() — это цикл, который продолжается до тех пор, пока есть оставшееся время (определяется проверкой deadline.timeRemaining), чтобы быть уверенным, что оно больше 0, или если был достигнут лимит таймаута (deadline.didTimeout равно true), и до тех пор, пока в списке задач есть задачи.
Для каждой задачи в очереди, которую мы успеваем выполнить, мы делаем следующее:
- Мы удаляем объект задачи из очереди.
- Мы увеличиваем
currentTaskNumberдля отслеживания того, сколько задач мы выполнили. - Мы вызываем обработчик задачи,
task.handler, передавая в него объект данных задачи (task.data). - Мы вызываем функцию
scheduleStatusRefresh(), чтобы запланировать обновление экрана для отражения изменений в нашем прогрессе.
Когда время заканчивается, если в списке все еще есть задачи, мы снова вызываем requestIdleCallback(), чтобы мы могли продолжить обработку задач в следующий раз, когда будет доступно время бездействия. Если очередь пуста, мы устанавливаем taskHandle в 0, чтобы указать, что у нас нет запланированного обратного вызова. Таким образом, мы будем знать, чтобы запросить обратный вызов в следующий раз, когда будет вызван enqueueTask().
Обновление отображения состояния
Мы хотим обновлять наш документ с выходными данными журнала и информацией о прогрессе. Однако вы не можете безопасно изменять DOM внутри обратного вызова при бездействии. Вместо этого мы будем использовать requestAnimationFrame(), чтобы попросить браузер вызвать нас, когда будет безопасно обновить отображение.
Планирование обновлений отображения
Изменения в DOM планируются путем вызова функции scheduleStatusRefresh().
function scheduleStatusRefresh() {
if (!statusRefreshScheduled) {
requestAnimationFrame(updateDisplay);
statusRefreshScheduled = true;
}
}
Это простая функция. Она проверяет, планировали ли мы уже обновление отображения, проверяя значение statusRefreshScheduled. Если оно равно false, мы вызываем requestAnimationFrame() для планирования обновления, передавая функцию updateDisplay() для обработки этой работы.
Обновление отображения
Функция updateDisplay() отвечает за рисование содержимого поля прогресса и журнала. Она вызывается браузером, когда DOM находится в безопасном состоянии для применения изменений во время процесса рендеринга следующего кадра.
function updateDisplay() {
const scrolledToEnd =
logElem.scrollHeight - logElem.clientHeight <= logElem.scrollTop + 1;
if (totalTaskCount) {
if (progressBarElem.max !== totalTaskCount) {
totalTaskCountElem.textContent = totalTaskCount;
progressBarElem.max = totalTaskCount;
}
if (progressBarElem.value !== currentTaskNumber) {
currentTaskNumberElem.textContent = currentTaskNumber;
progressBarElem.value = currentTaskNumber;
}
}
if (logFragment) {
logElem.appendChild(logFragment);
logFragment = null;
}
if (scrolledToEnd) {
logElem.scrollTop = logElem.scrollHeight - logElem.clientHeight;
}
statusRefreshScheduled = false;
}
Сначала scrolledToEnd устанавливается в true , если текст в журнале прокручен до конца; в противном случае он устанавливается в false. Мы будем использовать это для определения того, следует ли обновлять положение прокрутки, чтобы журнал оставался в конце, когда мы закончим добавление содержимого в него.
Далее мы обновляем информацию о прогрессе и состоянии, если в очередь были добавлены задачи.
- Если текущее максимальное значение полосы прогресса отличается от текущего общего количества задач в очереди (
totalTaskCount), то мы обновляем содержимое отображаемого общего количества задач (totalTaskCountElem) и максимальное значение полосы прогресса, чтобы она правильно масштабировалась. - Мы делаем то же самое с количеством обработанных задач; если
progressBarElem.valueотличается от номера задачи, которая обрабатывается в данный момент (currentTaskNumber), то мы обновляем отображаемое значение текущей обрабатываемой задачи и текущее значение полосы прогресса.
Затем, если есть текст, ожидающий добавления в журнал (то есть, если logFragment не равно null), мы добавляем его к элементу журнала с помощью Element.appendChild() и устанавливаем logFragment в null, чтобы мы не добавляли его снова.
Если журнал был прокручен до конца при запуске, мы убеждаемся, что он по-прежнему прокручен до конца. Затем мы устанавливаем statusRefreshScheduled в false , чтобы указать, что мы обработали обновление и что можно запросить новое.
Добавление текста в журнал
Функция log() добавляет указанный текст в журнал. Поскольку мы не знаем на момент вызова log() , безопасно ли немедленно взаимодействовать с DOM, мы будем кэшировать текст журнала до тех пор, пока не будет безопасно обновить его. Выше, в коде для updateDisplay(), вы можете найти код, который фактически добавляет залогированный текст в элемент журнала, когда обновляется кадр анимации.
function log(text) {
if (!logFragment) {
logFragment = document.createDocumentFragment();
}
const el = document.createElement("div");
el.textContent = text;
logFragment.appendChild(el);
}
Сначала мы создаем объект DocumentFragment с именем logFragment , если он не существует в данный момент. Этот элемент — псевдо-DOM, в который мы можем вставлять элементы, не изменяя сразу сам основной DOM.
Затем мы создаем новый элемент <div> и устанавливаем его содержимое в соответствии с входным значением text. Затем мы добавляем новый элемент в конец псевдо-DOM в logFragment. logFragment будет накапливать записи журнала до следующего вызова updateDisplay(), когда DOM будет готов к изменениям.
Запуск задач
Теперь, когда код управления задачами и поддержки отображения готов, мы можем начать настройку кода для выполнения задач, которые выполняют работу.
Обработчик задач
Функция, которую мы будем использовать в качестве обработчика задач — то есть функция, которая будет использоваться в качестве значения свойства handler объекта задачи, — это logTaskHandler(). Это простая функция, которая выводит множество данных в журнал для каждой задачи. В вашем собственном приложении вы замените этот код тем, что хотите выполнить в течение простоя. Просто помните, что все, что вы хотите сделать, изменяя DOM, должно обрабатываться через requestAnimationFrame().
function logTaskHandler(data) {
log(`Running task #${currentTaskNumber}`);
for (let i = 0; i < data.count; i += 1) {
log(`${(i + 1).toString()}. ${data.text}`);
}
}
Основная программа
Все запускается при нажатии пользователем кнопки «Начать», что вызывает вызов функции decodeTechnoStuff().
function decodeTechnoStuff() {
totalTaskCount = 0;
currentTaskNumber = 0;
updateDisplay();
const n = getRandomIntInclusive(100, 200);
for (let i = 0; i < n; i++) {
const taskData = {
count: getRandomIntInclusive(75, 150),
text: `This text is from task number ${i + 1} of ${n}`,
};
enqueueTask(logTaskHandler, taskData);
}
}
document
.getElementById("startButton")
.addEventListener("click", decodeTechnoStuff, false);
decodeTechnoStuff() начинается с обнуления значений totalTaskCount (количество задач, добавленных в очередь до сих пор) и currentTaskNumber (текущая выполняемая задача), а затем вызывает updateDisplay() для сброса отображения в состояние «еще ничего не произошло».
Этот пример создаст случайное количество задач (от 100 до 200). Для этого мы используем функцию getRandomIntInclusive(), которая приводится в качестве примера в документации для Math.random(), чтобы получить количество задач для создания.
Затем мы начинаем цикл для создания фактических задач. Для каждой задачи мы создаем объект taskData, который включает два свойства:
-
count— это количество строк, которые нужно вывести в журнал из задачи. -
text— это текст, который нужно вывести в журнал указанное количество раз свойствомcount.
Затем каждая задача помещается в очередь путем вызова enqueueTask(), передав logTaskHandler() в качестве функции-обработчика и объект taskData в качестве объекта, который нужно передать в функцию при ее вызове.
Результат
Ниже приведен фактический рабочий результат кода выше. Попробуйте его, поработайте с ним в инструментах разработчика вашего браузера и экспериментируйте с его использованием в собственном коде.
Спецификации
Совместимость с браузерами
| Рабочие столы | Мобильные устройства | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Chrome | Edge | Firefox | Opera | Safari | Chrome Android | Firefox для Android | Opera Android | Safari на iOS | Samsung Internet | WebView Android | |
Background_Tasks_API |
47 | 79 | 55 | 34 | Нет | 47 | 55 | 34 | Нет | 5.0 | 47 |
См. также
© 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/Background_Tasks_API