Самый распространенный вопрос на собеседовании и необходимая основа для написания эффективного кода.

«Как работает цикл событий браузера?» Этот вопрос можно задать почти на каждом собеседовании по JavaScript. Интервьюеров часто ругают за плохие вопросы, но вряд ли кто-то станет утверждать, что этот вопрос просто великолепный! Хорошее знание цикла событий дает вам понимание того, как браузер будет обрабатывать код и как не ставить палки в колеса, а помочь ему работать эффективно, чтобы пользователи ваших приложений получали наилучшие впечатления.

Цикл событий похож на менеджера проектов. Он координирует события, рендеринг, сетевые запросы, взаимодействия с пользователем и весь ваш асинхронный код.

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

Викторина 1.

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



Попробуй себя

Викторина 2.

Этот тест немного более запутанный.

Попробуй себя

Викторина 3.

Эта викторина на первый взгляд похожа на предыдущие. Однако только 17% аудитории нашего телеграм-канала (который невероятно силен в JavaScript, судя по ответам на другие викторины) сумели ответить правильно.

Попробуй себя

Чтобы понять код в этих примерах, давайте более подробно рассмотрим цикл событий.

Если вы правильно ответили на все вопросы (а главное, можете объяснить результат) — поздравляем! Не стесняйтесь пропустить часть «Как работает цикл событий» и перейти к разделу «Рендеринг». Мы вас там обязательно удивим.

Как работает цикл событий.

Цикл событий работает с очередью задач (или макрозадач) и микрозадач.

Задача (и микрозадача) — это часть работы, которую должен выполнить браузер. В целом можно сказать, что задача — это функция.

Примеры задач:

  • Выполнение скрипта
  • Парсинг html
  • Обратный вызов SetTimeout
  • Обратный вызов по клику мыши или любому другому событию

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

Примеры микрозадач:

Мы рассмотрим алгоритм цикла событий, для наглядности приведя себе графический пример.

Рассмотрим следующие задачи, ожидающие выполнения:

Для упрощения цикл событий бесконечно проходит следующие этапы:

1. Если в очереди задач есть хотя бы одна задача, возьмите первую (самую старую) задачу и выполните ее. В нашем случае это функция bar().

2. Если в очереди микрозадач есть задачи, выполните все задачи, начиная с самой старой. Функция qqz(), затем функция baz().

3. Обновите рендеринг.

В результате повторения действий обе очереди станут пустыми.

Стек вызовов.

Что происходит с задачей, когда ее берут на выполнение? Он помещается в стек вызовов. Давайте посмотрим на пример.

Первая задача, которая стоит перед браузером — выполнение скрипта.

В стек вызовов попадает задача, которую берет на выполнение браузер.

Все функции, которые вызывает этот код, также попадают в стек вызовов.

Если в результате выполнения функции создается новая задача, она помещается в очередь задач.

Когда функция выполняется, она извлекается из стека. (setTimeout в нашем случае)

В структуре данных «стек» — последний пришедший, последний ушедший. В структуре данных «очереди» все наоборот — первым пришел, первым вышел.

Если в результате выполнения функции создается новая микрозадача, она помещается в очередь микрозадач.

Затем Promise(), bar(), foo() и script по очереди извлекаются из стека.

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

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

Когда обе очереди пусты, браузер ничего не делает, ожидая новых задач.

Теперь мы можем вернуться к нашим тестам и посмотреть, какие шаги пройдет браузер при их выполнении.

Викторина 1. Объяснение.

Действия браузера:

  1. Первой задачей, которую необходимо выполнить, является сценарий. Результат этого шага:
  • Обратный вызов setTimeout добавляется в очередь задач.
  • Функция-исполнитель promise res => { console.log(3); res(); } выполняется синхронно. Итак, результат этого шага: 2, 3, 5.
  • Обратный вызов promise добавляется в очередь микрозадач.

2. Выполнять задачи из очереди микрозадач. Вывод: 4.

3. Выполняем последнюю задачу — обратный вызов setTimeout. Выход: 1.

Результат: 2, 3, 5, 4, 1.

Викторина 2. Объяснение.

Действия браузера:

  1. Выполните сценарий. Результат этого шага:
  • Оба обратных вызова setTimeout добавляются в очередь задач в том порядке, в котором они встречаются в коде.
  • Обратный вызов второго promise добавляется в очередь микрозадач.

2. Выполнять задачи из очереди микрозадач. Выход: promise 2.

3. Выполните самую старую задачу из очереди макрозадач. Результат этого шага:

  • Выход: timeout 1.
  • Новый обратный вызов promise добавляется в очередь микрозадач.

4. Выполнять задачи из очереди микрозадач. Выход: promise 1.

5. Выполните последнюю макрозадачу. Выход: timeout 2.

Результат: promise 2, timeout 1, promise 1, timeout 2.

Викторина 3. Объяснение.

Действия браузера:

  1. Выполните сценарий. Результат этого шага:
  • Обратные вызовы timeout 1 и timeout 2 добавляются в очередь задач.
  • Функция-исполнитель promise resolve => setTimeout(resolve) выполняется синхронно. Таким образом, в очередь задач добавляется еще один setTimeout.
  • Promise.resolve() немедленно выполняет обещание, его обратный вызов добавляется в очередь микрозадач. Поскольку первый promise еще не выполнен, его обратный вызов еще не добавлен в очередь микрозадач.

2. Выполнять задачи из очереди микрозадач. Выход: promise 2.

3. Выполнить задачу из очереди задач, начиная с самой старой задачи. Выход: timeout 1.

4. Поскольку очередь микрозадач пуста, снова выполните самую старую задачу в очереди задач. В результате в очередь микрозадач добавляется () => console.log(‘promise 1’)функция.

5. Выполняйте задачи из очереди микрозадач. Выход: promise 1.

6. Выполните задачу из очереди задач. Наконец, мы увидим timeout 2 в консоли.

Результат: promise 2, timeout 1, promise 1, timeout 2.

Рендеринг.

Давайте подробнее рассмотрим этап рендеринга.

Викторина 1.

Попробуйте угадать, что будет отображено на экране:

"Проверь себя"

По данным МДН:

Метод window.requestAnimationFrame() сообщает браузеру, что вы хотите выполнить анимацию, и запрашивает, чтобы браузер вызвал указанную функцию для обновления анимации непосредственно перед следующей перерисовкой.

Другими словами, метод requestAnimationFrame() вызывается непосредственно перед этапом рендеринга.

Согласно алгоритму, после выполнения задачи (и всех микрозадач) браузер обновляет рендеринг.

Итак, первый рендеринг ожидается после выполнения скрипта. И тогда порядок вывода букв на экран такой: B, A, C. D никогда не будет отображаться на экране, поскольку он разделяет один шаг рендеринга с обратным вызовом requestAnimationFrame, который будет выполнен последним.

Таким образом, пользователь увидит на экране C. Вот как этот код сейчас работает в Safari.

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

По этой причине большинство современных браузеров, выполняющих этот пример, пропустят рендеринг после выполнения сценария и объединят setTimeout обратные вызовы. Таким образом, для трёх задач будет только один рендеринг. Это небольшое упущение кардинально изменит конечный результат. Единственное, что отображает браузер, — это B.

Чтобы убедиться в этом, запустите код, откройте Chrome Dev Tools -> вкладку Performance -> нажмите Record, обновите страницу. Вы увидите только один нарисованный кадр.

Такой противоречивый результат может быть не только между браузерами, но и в одном и том же браузере от запуска к запуску. Поэтому следует избегать такого неоднозначного кода.

Викторина 2.

В приведенном ниже коде есть две кнопки, которые анимируют поворот логотипа на 360 градусов. Одна кнопка анимируется с помощью setTimeout, другая — с requestAnimationFrame. Как вы думаете, какая анимация позволит повернуть логотип на 360 градусов быстрее?

"Проверь себя"

Это еще один пример того, как браузер пропускает этап рендеринга. Функция requestAnimationFrame() вызывается каждый раз перед рендерингом, поэтому она будет вызываться ровно 360 раз. В случае setTimeout() браузер выполняет один рендеринг для нескольких (3–4) вызовов setTimeout. Поэтому такая анимация будет работать быстрее, и многие кадры будут пропущены. Смотрите демо.

Кончик! Используйте метод requestAnimationFrame() для анимации, чтобы избежать пропуска кадров.

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

В заключение.

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

Как всегда, мы хотим призвать вас продолжать изучать язык, на котором вы пишете, каждый день, и давайте сделаем его лучше!

Подпишитесь на телеграм-канал, чтобы узнать:

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

И самое главное, все это с помощью викторин, которые облегчают усвоение материала.

Подписывайтесь на нас на Medium, чтобы не пропустить новинки.

Загляните в нашу LinkedIn, чтобы узнать о других интересных вещах, которые мы делаем.

Ресурсы.

  1. https://html.spec.whatwg.org/multipage/webappapis.html#event-loops
  2. https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
  3. https://t.me/intspirit