Spec-Zone.ru › Web APIs

Использование микрозадач в JavaScript с queueMicrotask()

Микрозадача — это небольшая функция, которая выполняется после завершения функции или программы, которая её создала, и только если стек выполнения JavaScript пуст, но перед возвратом управления в цикл событий, используемый пользовательским агентом для управления средой выполнения скрипта.

Этот цикл событий может быть либо основным циклом событий браузера, либо циклом событий, управляющим веб-рабочим потоком. Это позволяет данной функции выполняться без риска вмешательства в выполнение другого скрипта, а также гарантирует, что микрозадача выполнится до того, как пользовательский агент получит возможность отреагировать на действия, выполненные микрозадачей.

JavaScript обещания и API наблюдателя за изменениями оба используют очередь микрозадач для выполнения своих обратных вызовов, но есть и другие случаи, когда полезно отложить работу до завершения текущего прохода цикла событий. Чтобы позволить микрозадачам использоваться сторонними библиотеками, фреймворками и полифиллами, метод queueMicrotask() доступен в интерфейсах Window и WorkerGlobalScope.

Задачи против микрозадач

Для правильного обсуждения микрозадач сначала полезно знать, что такое задача JavaScript и чем микрозадачи отличаются от задач. Это быстрое, упрощенное объяснение, но если вы хотите более подробной информации, вы можете прочитать статью Подробно: Микрозадачи и среда выполнения JavaScript.

Задачи

Задача — это всё, что запланировано для выполнения стандартными механизмами, такими как первоначальное начало выполнения программы, отправка асинхронного события или срабатывание интервала или таймаута. Все это планируется в очереди задач.

Например, задачи добавляются в очередь задач, когда:

  • Выполняется новая программа или подпрограмма JavaScript (например, из консоли или путём запуска кода в элементе <script>).
  • Пользователь нажимает элемент. Затем создаётся задача и выполняется все обратные вызовы событий.
  • Достигается таймаут или интервал, созданный с помощью setTimeout() или setInterval(), что приводит к добавлению соответствующего обратного вызова в очередь задач.

Цикл событий, управляющий вашим кодом, обрабатывает эти задачи одну за другой в порядке их добавления в очередь. Самая старая выполнимая задача в очереди задач будет выполнена во время одного прохода цикла событий. После этого выполняются микрозадачи до тех пор, пока очередь микрозадач не станет пустой, и тогда браузер может выбрать обновление рендеринга. Затем браузер переходит к следующему проходу цикла событий.

Микрозадачи

Поначалу разница между микрозадачами и задачами кажется незначительной. И они похожи; обе состоят из кода JavaScript, который помещается в очередь и выполняется в подходящее время. Однако, в то время как цикл событий выполняет только задачи, присутствующие в очереди в начале прохода, одну за другой, он обрабатывает очередь микрозадач совершенно по-другому.

Есть две ключевые разницы.

Во-первых, каждый раз, когда задача завершается, цикл событий проверяет, возвращает ли задача управление другому коду JavaScript. Если нет, он выполняет все микрозадачи в очереди микрозадач. Очередь микрозадач обрабатывается несколько раз за один проход цикла событий, включая обработку событий и других обратных вызовов.

Во-вторых, если микрозадача добавляет больше микрозадач в очередь, вызвав queueMicrotask(), эти вновь добавленные микрозадачи выполняются перед запуском следующей задачи. Это потому, что цикл событий будет продолжать вызывать микрозадачи, пока в очереди не останется ни одной, даже если их продолжает добавляться всё больше.

Предупреждение: Поскольку микрозадачи могут сами добавлять новые микрозадачи в очередь, а цикл событий продолжает обрабатывать микрозадачи до тех пор, пока очередь не станет пустой, существует реальный риск того, что цикл событий будет бесконечно обрабатывать микрозадачи. Будьте осторожны, как вы добавляете микрозадачи рекурсивно.

Использование микрозадач

Прежде чем углубляться в этот вопрос, важно ещё раз отметить, что большинство разработчиков не используют микрозадачи очень часто, если вообще используют. Это очень специализированная функция современных JavaScript-разработок на основе браузера, позволяющая планировать код для опережения других вещей в длинном списке вещей, ожидающих выполнения на компьютере пользователя. Неправильное использование этой возможности приведёт к проблемам производительности.

Добавление микрозадач в очередь

Поэтому вы обычно должны использовать микрозадачи только тогда, когда нет другого решения или при создании фреймворков или библиотек, которые нуждаются в использовании микрозадач для создания реализуемой ими функциональности. Хотя раньше были доступны хитрости, позволявшие добавлять микрозадачи в очередь (например, путём создания обещания, разрешаемого немедленно), добавление метода queueMicrotask() предоставляет стандартный способ безопасного добавления микрозадачи без хитростей.

Внедряя queueMicrotask(), можно избежать проблем, возникающих при использовании обещаний для создания микрозадач. Например, при использовании обещаний для создания микрозадач исключения, брошенные обратным вызовом, сообщаются как отклоненные обещания, а не как стандартные исключения. Кроме того, создание и уничтожение обещаний требует дополнительных затрат времени и памяти, которых удается избежать функции, которая правильно добавляет микрозадачи в очередь.

Передайте JavaScript Function для вызова, когда контекст обрабатывает микрозадачи, в метод queueMicrotask(), который доступен в глобальном контексте, как определено интерфейсами Window или Worker в зависимости от текущего контекста выполнения.

queueMicrotask(() => {
  /* code to run in the microtask here */
});

Сама функция микрозадачи не принимает параметров и не возвращает значения.

Когда использовать микрозадачи

В этом разделе мы рассмотрим сценарии, в которых микрозадачи особенно полезны. В основном, это касается захвата или проверки результатов, или выполнения очистки после завершения основного тела контекста выполнения JavaScript, но до обработки обработчиков событий, таймаутов и интервалов или других обратных вызовов.

Когда это полезно?

Основная причина использования микрозадач заключается в следующем: для обеспечения согласованного порядка задач, даже когда результаты или данные доступны синхронно, но одновременно с этим снижение риска заметных пользователю задержек в операциях.

Обеспечение порядка при условном использовании обещаний

Одна ситуация, в которой микрозадачи могут использоваться для обеспечения согласованности порядка выполнения, возникает, когда обещания используются в одном из разделов оператора if...else (или другого условного оператора), но не в другом. Рассмотрим такой код:

customElement.prototype.getData = (url) => {
  if (this.cache[url]) {
    this.data = this.cache[url];
    this.dispatchEvent(new Event("load"));
  } else {
    fetch(url)
      .then((result) => result.arrayBuffer())
      .then((data) => {
        this.cache[url] = data;
        this.data = data;
        this.dispatchEvent(new Event("load"));
      });
  }
};

Проблема, возникающая здесь, заключается в том, что путём использования задачи в одном из разделов оператора if...else (в случае, когда изображение доступно в кэше), но с участием обещаний в разделе else, у нас возникает ситуация, в которой порядок операций может изменяться; например, как показано ниже.

element.addEventListener("load", () => console.log("Loaded data"));
console.log("Fetching data…");
element.getData();
console.log("Data fetched");

Выполнение этого кода дважды подряд даёт следующие результаты.

Когда данные не кэшируются:

Fetching data…
Data fetched
Loaded data

Когда данные кэшируются:

Fetching data…
Loaded data
Data fetched

Что ещё хуже, иногда свойство элемента data будет установлено, а иногда нет к моменту завершения выполнения этого кода.

Мы можем обеспечить согласованный порядок этих операций, используя микрозадачу в разделе if для балансировки двух разделов:

customElement.prototype.getData = (url) => {
  if (this.cache[url]) {
    queueMicrotask(() => {
      this.data = this.cache[url];
      this.dispatchEvent(new Event("load"));
    });
  } else {
    fetch(url)
      .then((result) => result.arrayBuffer())
      .then((data) => {
        this.cache[url] = data;
        this.data = data;
        this.dispatchEvent(new Event("load"));
      });
  }
};

Это балансирует разделы, заставляя обе ситуации обрабатывать установку data и срабатывание события load в рамках микрозадачи (используя queueMicrotask() в разделе if и используя обещания, используемые fetch() в разделе else).

Группировка операций

Вы также можете использовать микрозадачи для сбора нескольких запросов из разных источников в одну группу, избегая потенциальной накладных расходов, связанных с несколькими вызовами для обработки одинаковой работы.

Ниже приведен фрагмент кода, создающий функцию, которая группирует несколько сообщений в массив, используя микрозадачу для отправки их как одного объекта при выходе из контекста.

const messageQueue = [];

let sendMessage = (message) => {
  messageQueue.push(message);

  if (messageQueue.length === 1) {
    queueMicrotask(() => {
      const json = JSON.stringify(messageQueue);
      messageQueue.length = 0;
      fetch("url-of-receiver", json);
    });
  }
};

Когда sendMessage() вызывается, указанное сообщение сначала добавляется в массив очереди сообщений. Затем всё становится интересным.

Если сообщение, которое мы только что добавили в массив, является первым, мы добавляем в очередь микрозадачу, которая отправит группу. Микрозадача выполнится, как всегда, когда путь выполнения JavaScript достигнет верхнего уровня, незадолго до запуска обратных вызовов. Это означает, что любые последующие вызовы sendMessage() в промежутке времени добавят свои сообщения в очередь сообщений, но из-за проверки длины массива перед добавлением микрозадачи новая микрозадача не добавляется в очередь.

Когда микрозадача выполняется, у неё есть массив, потенциально содержащий много сообщений, ожидающих обработки. Она начинает с кодирования его в JSON с помощью метода JSON.stringify(). После этого содержимое массива больше не требуется, поэтому мы очищаем массив messageQueue. Наконец, мы используем метод fetch() для отправки строки JSON на сервер.

Это позволяет каждому вызову sendMessage() во время одного прохода цикла событий добавлять свои сообщения в ту же операцию fetch(), не подвергая их возможной задержке со стороны других задач, таких как таймауты и т. п.

Сервер получит строку JSON, затем, предположительно, декодирует её и обработает сообщения, которые он найдёт в получившемся массиве.

Примеры

Пример с простой микрозадачей

В этом простом примере мы видим, что добавление микрозадачи в очередь приводит к выполнению обратного вызова микрозадачи после завершения выполнения тела этого скрипта верхнего уровня.

JavaScript

В приведенном ниже коде мы видим вызов queueMicrotask() для планирования выполнения микрозадачи. Этот вызов заключен в вызовы log(), пользовательской функции, которая выводит текст на экран.

log("Before enqueueing the microtask");
queueMicrotask(() => {
  log("The microtask has run.");
});
log("After enqueueing the microtask");

Результат

Пример таймаута и микрозадачи

В этом примере таймаут запланирован на срабатывание через ноль миллисекунд (или «как можно скорее»). Это демонстрирует разницу между тем, что означает «как можно скорее» при планировании новой задачи (например, с помощью setTimeout()) и использовании микрозадачи.

JavaScript

В приведенном ниже коде мы видим вызов queueMicrotask() для планирования выполнения микрозадачи. Этот вызов заключен в вызовы log(), пользовательской функции, которая выводит текст на экран.

Приведенный ниже код планирует срабатывание таймаута через ноль миллисекунд, а затем помещает микрозадачу в очередь. Этот код заключен в вызовы log() для вывода дополнительных сообщений.

const callback = () => log("Regular timeout callback has run");

const urgentCallback = () => log("*** Oh noes! An urgent callback has run!");

log("Main program started");
setTimeout(callback, 0);
queueMicrotask(urgentCallback);
log("Main program exiting");

Результат

Обратите внимание, что вывод, записанный из тела основной программы, появляется первым, за ним следует вывод от микрозадачи, а затем обратный вызов таймаута. Это связано с тем, что когда задача, обрабатывающая выполнение основной программы, завершается, очередь микрозадач обрабатывается перед очередью задач, в которой находится обратный вызов таймаута. Помните, что задачи и микрозадачи хранятся в отдельных очередях, и микрозадачи выполняются первыми, это поможет разобраться.

Микрозадача из функции

Этот пример немного расширяет предыдущий, добавив функцию, которая выполняет некоторую работу. Эта функция использует queueMicrotask() для планирования микрозадачи. Важно помнить, что микрозадача не обрабатывается при выходе из функции, а при выходе из основной программы.

JavaScript

Следует код основной программы. Функция doWork() здесь вызывает queueMicrotask(), но микрозадача все равно не выполняется до тех пор, пока вся программа не завершится, так как именно в этот момент завершается выполнение задачи и ничего больше нет в стеке выполнения.

const callback = () => log("Regular timeout callback has run");

const urgentCallback = () => log("*** Oh noes! An urgent callback has run!");

const doWork = () => {
  let result = 1;

  queueMicrotask(urgentCallback);

  for (let i = 2; i <= 10; i++) {
    result *= i;
  }
  return result;
};

log("Main program started");
setTimeout(callback, 0);
log(`10! equals ${doWork()}`);
log("Main program exiting");

Результат

См. также

  • Подробнее: Микрозадачи и среда выполнения JavaScript
  • queueMicrotask()
  • Асинхронный JavaScript
    • Введение в асинхронный JavaScript
    • Грамотное асинхронное программирование с Promise

© 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/HTML_DOM_API/Microtask_guide

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API