Кратко
СкопированоБраузерное API, которое выполняет переданный код асинхронно.
Как пишется
Скопированоqueue
:
- Принимает функцию, которая будет передана в очередь микрозадач;
- Возвращает
undefined
.
queueMicrotask(() => { console.log('Хэй, я выполнюсь асинхронно!')})
queueMicrotask(() => { console.log('Хэй, я выполнюсь асинхронно!') })
Как понять
СкопированоКод выше схож со сценарием использования set
. Оба выполнят код асинхронно:
setTimeout(() => { console.log('Хэй, я выполнюсь асинхронно благодаря setTimeout')}, 0)
setTimeout(() => { console.log('Хэй, я выполнюсь асинхронно благодаря setTimeout') }, 0)
Так в чем же принципиальная разница между ними?
queue
добавляет переданную функцию в очередь микрозадач. Функции в этой очереди выполняются одна за другой (FIFO: First in First Out) — когда текущая функция выполнилась, запускается следующая функция в очереди.
Все микрозадачи в очереди будут выполнены только после того, как текущий стек вызовов окажется пустым, но перед выполнением следующей макрозадачей.
Если вернуться к сравнению с set
, то передаваемые в него функции этого относятся к макрозадачам. Каждая из них будет взята из очереди задач, после того как управление передастся циклу событий.
Поэтому, если вызвать queue
после set
, или наоборот — функция, переданная в queue
, начнёт своё исполнение первой.
Подробнее про микро и макрозадачи
JavaScript имеет в своём арсенале различные виды очередей, а также стек вызовов. Давайте кратко разберём необходимый минимум, который поможет разобраться с процессом работы:
- Стек вызовов — служит для выполнения синхронных операций;
- Очередь микрозадач — контейнер для хранения асинхронных операций, имеющих высокий приоритет;
- Очередь макрозадач — контейнер для хранения асинхронных операций с низким приоритетом.
Что же, кажется, самое время рассмотреть процесс работы между этими самыми элементами:
- Первый, кто начинает процесс выполнения — стек вызовов;
- После того, как JavaScript убеждается, в том, что стек пуст — в него по очереди добавляются задачи из очереди микрозадач;
- Процесс выполнения продолжается до тех пор, пока не станет ясно, что очередь опустела. Как только это произойдёт — выполняются задачи из очередь макрозадач;
- Очередь макрозадач является завершающим этапом. После того как список в нем станет пустым — все повторяется по новой.
Пример
СкопированоУбедимся, что функция, переданная в queue
выполнится раньше, чем через set
. Для этого создадим страницу с формой, при отправке которой будут запускаться оба задания. Каждое из них будет печатать на экран уникальный текст:
<form class="compare-form" name="compare-form"> <h2> Вывод значений с помощью <code>queueMicrotask</code> и <code>setTimeout</code>: </h2> <p id="compare-output" class="compare-form__output" ></p> <button type="submit" class="button compare-form__submit-button"> Вывести текст </button> <button type="reset" class="button compare-form__reset-button"> Очистить содержимое </button></form>
<form class="compare-form" name="compare-form"> <h2> Вывод значений с помощью <code>queueMicrotask</code> и <code>setTimeout</code>: </h2> <p id="compare-output" class="compare-form__output" ></p> <button type="submit" class="button compare-form__submit-button"> Вывести текст </button> <button type="reset" class="button compare-form__reset-button"> Очистить содержимое </button> </form>
При отправке формы запустим наши задачи — первым будет располагаться set
, а после него queue
.
<script> const handleFormSubmit = (e) => { e.preventDefault() setTimeout(() => { output.innerText += 'Фраза добавлена из setTimeout()\n\n' }, 0) queueMicrotask(() => { output.innerText += 'Фраза добавлена из queueMicrotask()\n' }) }</script>
<script> const handleFormSubmit = (e) => { e.preventDefault() setTimeout(() => { output.innerText += 'Фраза добавлена из setTimeout()\n\n' }, 0) queueMicrotask(() => { output.innerText += 'Фраза добавлена из queueMicrotask()\n' }) } </script>
Вот и все! Давайте посмотрим, что у нас получилось:
Подсказки
СкопированоОсновная причина использования queue
— обеспечение последовательности выполнения задач, одновременно снижая риск заметных пользователю задержек в операциях.
Представим ситуацию, в которой необходимо получать данные по указанному урлу. Либо же, если запрос выполнялся ранее — запросить данные из кэша:
const output = document.querySelector('.logging-form__output')let data = []const cache = {}function getData(url) { if (url in cache) { data = cache[url] output.dispatchEvent(new Event('data-loaded')) } else { fetch(url) .then((response) => response.json()) .then(({ data }) => { cache[url] = data data = data output.dispatchEvent(new Event('data-loaded')) }) }}
const output = document.querySelector('.logging-form__output') let data = [] const cache = {} function getData(url) { if (url in cache) { data = cache[url] output.dispatchEvent(new Event('data-loaded')) } else { fetch(url) .then((response) => response.json()) .then(({ data }) => { cache[url] = data data = data output.dispatchEvent(new Event('data-loaded')) }) } }
Какую проблему тут можно заметить?
В теле одного условия используется цепочка промисов, в другом — обычное синхронное выполнение. Из этого можно сделать вывод, что в разных условиях, процесс выполнения также будет отличаться.
Для наглядности, навесим обработчик на событие submit
, в котором будет происходить вызов функции get
:
const form = document.querySelector('.logging-form')const handleFormSubmit = (e) => { e.preventDefault() output.innerText += 'Процесс загрузки данных...\n' getData('https://reqres.in/api/users/2') output.innerText += 'Процесс загрузки данных выполняется...\n'}form.addEventListener('submit', handleFormSubmit)
const form = document.querySelector('.logging-form') const handleFormSubmit = (e) => { e.preventDefault() output.innerText += 'Процесс загрузки данных...\n' getData('https://reqres.in/api/users/2') output.innerText += 'Процесс загрузки данных выполняется...\n' } form.addEventListener('submit', handleFormSubmit)
Не забываем про кастомное событие data
, инициируемое внутри функции get
. Навесим обработчик и на него:
const output = document.querySelector('.logging-form__output')const handleOutputDataLoaded = () => { output.innerText += 'Данные загружены\n'}output.addEventListener('data-loaded', handleOutputDataLoaded)
const output = document.querySelector('.logging-form__output') const handleOutputDataLoaded = () => { output.innerText += 'Данные загружены\n' } output.addEventListener('data-loaded', handleOutputDataLoaded)
Давайте посмотрим, к каким результатам это может привести. Для этого необходимо нажать на кнопку получения данных 2 раза:
После второго нажатия, когда данные берутся из кэша, можно заметить недочёт. Строка «Процесс загрузки данных выполняется...» выводится после «Данные загружены». Причём, когда данные приходили впервые — вывод строк был совершенно иным. Это происходит из-за того, что при первом чтении событие data
отправляется из асинхронного кода, а в случае чтения из кэша — из синхронного.
Чтобы исправить проблему, необходимо обернуть тело первого условного блока в queue
и таким образом сделать чтение данных из кэша асинхронной операцией:
if (url in cache) { queueMicrotask(() => { data = cache[url] textarea.dispatchEvent(new Event('data-loaded')) })}
if (url in cache) { queueMicrotask(() => { data = cache[url] textarea.dispatchEvent(new Event('data-loaded')) }) }
Взглянем на итоговое решение после небольшой корректировки:
Отлично! Теперь процесс выполнения работает идентично как при получении данных с сервера, так и при вытаскивании их из кэша.
На практике
Скопированосоветует Скопировано
🛠️ «Выбирайте инструмент с умом»
Скопированоqueue
— полезная вещь когда у вас есть потребность отложить запуск задачи на ближайшее время. Но также не стоит забывать о том, что выполнение больших объёмов работы на стороне микрозадач может стать проблемной точкой для интерактивности вашего приложения. Поэтому подходите к выбору с умом. Возможно, что для решения вашей проблемы стоит рассмотреть set
или request
.
🛠️ «Возможные риски»
СкопированоПомните, что queue
влияет на процесс работы очереди микрозадач. Вызываемая в нем функция, которая становится микрозадачей, может создать ряд других микрозадач (например, благодаря циклу). Случайно созданная рекурсия приведёт к полному прекращению работы приложения.