Клавиша / esc

queueMicrotask()

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

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

Кратко

Скопировано

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

Пример

Скопировано

Убедимся, что переданная в 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().

        
          
          const handleFormSubmit = (e) => {  e.preventDefault()  setTimeout(() => {    output.innerText += 'Фраза добавлена из setTimeout()\n\n'  }, 0)  queueMicrotask(() => {    output.innerText += 'Фраза добавлена из queueMicrotask()\n'  })}
          const handleFormSubmit = (e) => {
  e.preventDefault()

  setTimeout(() => {
    output.innerText += 'Фраза добавлена из setTimeout()\n\n'
  }, 0)
  queueMicrotask(() => {
    output.innerText += 'Фраза добавлена из queueMicrotask()\n'
  })
}

        
        
          
        
      

Вот и всё! Посмотрим, что у нас получилось:

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

В этом примере queueMicrotask() принимает функцию, которая передаётся в очередь микрозадач, и возвращает undefined.

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

        
        
          
        
      

Как понять

Скопировано

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

Представим ситуацию, когда получаем данные по указаному URL или когда запрос выполнялся ранее. Запрашиваем данные из кэша:

        
          
          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)

        
        
          
        
      

Посмотрим, к каким результатам это приведёт. Для этого нажмите в демке на кнопку получения данных два раза.

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

Можете заметить недочёт после второго нажатия, когда данные берутся из кэша. Строка «Процесс загрузки данных выполняется…» выводится после «Данные загружены». Причём, когда данные приходили впервые, вывод строк был иным. Это происходит из-за того, что событие 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'))
  })
}

        
        
          
        
      

Посмотрим на итоговое решение после корректировки:

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

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

В этом примере код схож со сценарием использования setTimeout().

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

        
        
          
        
      

Оба сценария выполнят код асинхронно:

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

        
        
          
        
      

Так в чём же разница между обоими сценариями?

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

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

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

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

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

  • стек вызовов — контейнер для выполнения синхронных операций;
  • очередь микрозадач — контейнер для хранения асинхронных операций с высоким приоритетом;
  • очередь макрозадач — контейнер для хранения асинхронных операций с низким приоритетом.

Рассмотрим, как работают элементы процесса:

  1. Первый, кто начинает процесс выполнения, — стек вызовов.
  2. После того, как JavaScript убеждается, что стек пуст, в него добавляются задачи из очереди микрозадач.
  3. Если в процессе выполнения одной микрозадачи в очередь добавится другая, обе задачи выполнятся в этот же проход. Процесс выполнения продолжается, пока очередь не опустеет. Как только это произойдёт — выполняется одна задача из очереди макрозадач.
  4. Сразу после макрозадачи в стек добавятся микрозадачи. Это произойдёт перед выполнением следующей макрозадачи, отображением изменений на странице или другими действиями.

На практике

Скопировано

Артём Гусев советует

Скопировано

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

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