queueMicrotask()

Брат setTimeout, или как добавить синхронную функцию в очередь микрозадач

Время чтения: 6 мин

Кратко

Секция статьи "Кратко"

Браузерное API, которое выполняет переданный код асинхронно.

Как пишется

Секция статьи "Как пишется"

queueMicrotask():

  • Принимает функцию, которая будет передана в очередь микрозадач;
  • Возвращает undefined.
        
          
          queueMicrotask(() => {    console.log('Хэй, я выполнюсь асинхронно!')})
          queueMicrotask(() => {
    console.log('Хэй, я выполнюсь асинхронно!')
})

        
        
          
        
      

Как понять

Секция статьи "Как понять"

Код выше схож со сценарием использования setTimeout(). Оба выполнят код асинхронно:

        
          
          setTimeout(() => {    console.log('Хэй, я выполнюсь асинхронно благодаря setTimeout')}, 0)
          setTimeout(() => {
    console.log('Хэй, я выполнюсь асинхронно благодаря setTimeout')
}, 0)

        
        
          
        
      

Так в чем же принципиальная разница между ними?

queueMicrotask() добавляет переданную функцию в очередь микрозадач. Функции в этой очереди выполняются одна за другой (FIFO: First in First Out) — когда текущая функция выполнилась, запускается следующая функция в очереди.

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

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

Поэтому, если вызвать queueMicrotask() после setTimeout(), или наоборот — функция, переданная в queueMicrotask(), начнёт своё исполнение первой.

Подробнее про микро и макрозадачи
схема событийного цикла

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

  • Стек вызовов — служит для выполнения синхронных операций;
  • Очередь микрозадач — контейнер для хранения асинхронных операций, имеющих высокий приоритет;
  • Очередь макрозадач — контейнер для хранения асинхронных операций с низким приоритетом.

Что же, кажется, самое время рассмотреть процесс работы между этими самыми элементами:

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

Пример

Секция статьи "Пример"

Убедимся, что функция, переданная в queueMicrotask() выполнится раньше, чем через setTimeout(). Для этого создадим страницу с формой, при отправке которой будут запускаться оба задания. Каждое из них будет печатать на экран уникальный текст:

        
          
          <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>

        
        
          
        
      

При отправке формы запустим наши задачи — первым будет располагаться setTimeout(), а после него queueMicrotask().

        
          
          <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>

        
        
          
        
      

Вот и все! Давайте посмотрим, что у нас получилось:

Открыть демо в новой вкладке

Подсказки

Секция статьи "Подсказки"

Основная причина использования queueMicrotask() — обеспечение последовательности выполнения задач, одновременно снижая риск заметных пользователю задержек в операциях.

Представим ситуацию, в которой необходимо получать данные по указанному урлу. Либо же, если запрос выполнялся ранее — запросить данные из кэша:

        
          
          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, в котором будет происходить вызов функции getData:

        
          
          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-loaded, инициируемое внутри функции getData. Навесим обработчик и на него:

        
          
          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-loaded отправляется из асинхронного кода, а в случае чтения из кэша — из синхронного.

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

        
          
          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'))
  })
}

        
        
          
        
      

Взглянем на итоговое решение после небольшой корректировки:

Открыть демо в новой вкладке

Отлично! Теперь процесс выполнения работает идентично как при получении данных с сервера, так и при вытаскивании их из кэша.

На практике

Секция статьи "На практике"

corocoto

Секция статьи "corocoto"

🛠️ «Выбирайте инструмент с умом»

Секция статьи "🛠️ «Выбирайте инструмент с умом»"

queueMicrotask() — полезная вещь когда у вас есть потребность отложить запуск задачи на ближайшее время. Но также не стоит забывать о том, что выполнение больших объёмов работы на стороне микрозадач может стать проблемной точкой для интерактивности вашего приложения. Поэтому подходите к выбору с умом. Возможно, что для решения вашей проблемы стоит рассмотреть setTimeout() или requestAnimationFrame().

🛠️ «Возможные риски»

Секция статьи "🛠️ «Возможные риски»"

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