Что такое цикл событий (event loop) и как он работает?

JavaScript является однопоточным языком программирования (движок JS в 1 единицу времени может выполнять всего 1 операцию, для этого выделяется стек). В стеке хранятся фреймы - это локальные аргументы и переменные, которые хранятся в функции.

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

Координирование этого процесса и происходит в EventLoop.

EventLoop - это механизм, используемый браузером или Node.js для управления асинхронным кодом. Он позволяет синхронизировать callstack и очередь задач.

  1. Выбрать и исполнить старейшую синхронную задачу.
  2. Исполнить все микрозадачи:
    • Пока очередь микрозадач пуста: - выбрать из очереди и исполнить старейшую микрозадачу
  3. Отрисовать изменения страницы (requestAnimationFrame), если они есть.
  4. Если очередь макрозадач пуста – подождать, пока появится макрозадача.
  5. Перейти к шагу 1.

На основе этой схемы строится вся работа Event Loop.

Если у кого-то из заказчиков не оказалось задач, то Event Loop просто идет к следующему. И, наоборот, если у заказчика задачи занимают много времени, то остальные заказчики будут ждать своей очереди. А если задачи от какого-то заказчика оказались бесконечными, то CallStack переполняется, и браузер начинает ругаться:

_Рисунок 2 - Страница не отвечает

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

console.log('Start'); // Синхронный код, выполняется первым
 
// Микрозадача (Microtask)
queueMicrotask(() => {
    console.log('Inside microtask'); // Выполняется после синхронного кода, но перед макрозадачами
});
 
// Макрозадача (Macrotask)
setTimeout(() => {
    console.log('Inside macrotask'); // Выполняется после всех микрозадач
});
 
console.log('End'); // Синхронный код, выполняется вторым
 
/*
Ожидаемый результат выполнения:
1. Start
2. End
3. Inside microtask
4. Inside macrotask
*/

Ссылка: Иллюстрация работы событийного цикла

Разница между микро- и макрозадачами в event loop?
Микрозадачи (microtask)

Асинхронные задачи требуют правильного управления. Для этого стандарт предусматривает внутреннюю очередь PromiseJobs, более известную как очередь-микрозадач (microtask queue)».

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

Микрозадачи приходят только из кода. Обычно они создаются promise : выполнение обработчика .then/catch/finally становится микрозадачей. Микрозадачи также используются «под капотом» await, т.к. это форма обработки промиса.

Микрозадачи включают в себя:

  • обработчики промисов.
  • обработчики notationObserver.
  • функции добавленные с помощью queueMicrotask.

Расскажите о queueMicrotask

![[Pasted image 20230723140208.png|]]

_Рисунок 3 - Иллюстрация вызова queueMicrotask

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

Макрозадачи (macrotask)

Макрозадачи - это задачи, которые добавляются в очередь и добавляются после того как очередь задач пуста.

Задачи из очереди исполняются по правилу «первым пришёл – первым ушёл». Когда браузер заканчивает выполнение скрипта, он обрабатывает событие mousemove, затем выполняет обработчик, заданный setTimeout, и так далее.

Макрозадачи включают в себя:

  • элементы пользовательского ввода;
  • таймеры;
  • загрузка и ресурсов
  • и другие асинхронные задачи.
Решение задач на асинхронность

Рассмотрим примеры:

Promise.resolve()
  .then(() => {
    return Promise.reject(`O_o`)
  })
  .then(() => {
    // Все обработчики будут не выполнены
  })
  .catch((error) => {
    console.log(error) // `O_o`
  })
  .then(() => {
    // Продолжаем выполнять цепочку
  })

Возвращать значение можно и внутри catch, оно будет точно также обработано в цепочке.

Promise.reject("O_o")
  .catch((error) => {
    console.log(error) // O_o
    return "^_^"
  })
  .then((value) => {
    console.log(value) // ^_^
  })

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

Promise.resolve()
  .then(() => {
    Promise.reject("O_o")
  })
  .catch(() => {
    // Будет пропушено
  })

Назад