Будьте в курсе

Недавно я смотрел отличные доклады Филиппа Робертса и Эрин Циммер на JSConf EU о цикле событий JavaScript, которые вдохновили меня прочитать саму спецификацию HTML5 и убедиться, что я действительно ее понимаю. Теперь я думаю, что получил хорошее представление о том, что такое цикл событий, и я хотел бы обобщить его здесь.

Стек вызовов

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

JavaScript является однопоточным, что означает, что он может выполнять только одну задачу за раз — есть только один поток и только один стек вызовов. Если в стеке есть «медленный» код, это препятствует выполнению остального кода, а также может привести к тому, что пользовательский интерфейс не будет отвечать на запросы. Из-за этого мы хотим, чтобы этот «медленный» код (например, сетевые вызовы) как можно дольше оставался вне стека.

Очередь задач и цикл событий

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

Базовый пример, показывающий, как работают очередь задач и цикл событий, использует setTimeout . Давайте посмотрим на следующий код:

console.log('Hello');
setTimeout(() => console.log('World!'), 2000);

При запуске этого кода выполняются следующие шаги:

  1. console.log('Hello') помещается в стек вызовов. Он выполняется, и на консоль выводится «hello».
  2. setTimeout(() => console.log('World'), 2000) помещается в стек вызовов. Выполняется метод setTimeout, который отправляет обратный вызов и таймер в setTimeout API, предоставляемый браузером.
  3. Через 2 секунды пришло время для запуска обратного вызова () => console.log('World'), поэтому веб-API помещает его в очередь задач.
  4. Стек вызовов пуст, поэтому цикл событий может взять обратный вызов из очереди задач и поместить его в стек вызовов, где он запустится и выведет «Мир» на консоль.

Важным примечанием является то, что из этих шагов мы можем понять, что setTimeout(cb, 0) не запускает обратный вызов немедленно, а скорее веб-API помещает обратный вызов непосредственно в очередь задач. Поскольку задачи в очереди должны ждать, пока стек вызовов опустеет, до фактического выполнения обратного вызова может пройти больше времени. Это делает тайм-аут фактически минимальным количеством времени, которое пройдет до выполнения обратного вызова, а не точным временем.

Рендеринг

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

До сих пор мы описали базовый цикл обработки событий, состоящий из одной очереди задач и конвейера рендеринга. Итак, логика выглядит так:

Несколько очередей задач

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

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

Итак, теперь наш цикл событий выглядит так:

Очередь микрозадач

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

При добавлении очереди микрозадач в цикл событий получаем следующее:

Очередь обратного вызова кадра анимации

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

Собираем все вместе

Вот и все! Мы рассмотрели все различные типы очередей, которые могут быть в цикле событий, и теперь у нас есть полное понимание того, как работает цикл событий. Собрав все вместе, мы получим следующую логику:

Я надеюсь, что вы нашли это объяснение таким же интересным, как и я, и я был бы рад услышать любые ваши комментарии или идеи. Увидимся в следующий раз!

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter и LinkedIn. Посетите наш Community Discord и присоединитесь к нашему Коллективу талантов.