Написать пост

Как писать эффективный код на JavaScript с помощью Event Loop

Аватар Типичный программист

В статье рассказываем об Event Loop в JS: как работает основной поток, как он обрабатывает асинхронные функции и почему от этого зависит эффективность кода.

Препарирует принципы работы Event Loop Евгений, старший разработчик Noveo

Event Loop (цикл событий) — один из важнейших аспектов в JavaScript, знание которого позволяет писать более эффективный код. В статье мы рассмотрим, как работает основной поток в JavaScript и как он обрабатывает асинхронные функции.

Как писать эффективный код на JavaScript с помощью Event Loop 1

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

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

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

По факту окружение может одновременно управлять большим количеством «циклов событий» для обработки API-запросов. WebWorkers также имеют свой цикл событий.

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

Блокирование Event Loop

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

Почти все операции ввода/вывода в JavaScript являются неблокирующими — сетевые запросы, операции с файловой системой в Node.js и т. д. Исключением являются блокирующие операции, и именно поэтому в JavaScript так популярны обратные вызовы (callbacks), а в последнее время всё чаще начинают использовать Promise и async/await.

Стек вызовов

Стек вызовов — это очередь LIFO (Last In, First Out).

Цикл событий непрерывно обрабатывает стек вызовов в поиске функции, которая должна быть обработана. При этом он добавляет любой найденный вызов функции в стек вызовов и выполняет каждый по порядку.

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

Как писать эффективный код на JavaScript с помощью Event Loop 2

Примеры работы с Event Loop

На небольшом примере мы рассмотрим, как работает Event Loop:

Как писать эффективный код на JavaScript с помощью Event Loop 3

После выполнения этот код выведет:

Как писать эффективный код на JavaScript с помощью Event Loop 4

В принципе, как и ожидалось.

Давайте подробно разберём, как этот код обрабатывается через Event Loop. Когда код выполняется, первым вызывается foo(), внутри foo() первой вызывается bar(), а затем baz().

В этот момент стек вызовов выглядит так:

Как писать эффективный код на JavaScript с помощью Event Loop 5

Цикл обработки событий на каждой итерации проверяет, есть ли в стеке вызовы, и если да, выполняет их:

Как писать эффективный код на JavaScript с помощью Event Loop 6

Этот процесс продолжается до тех пор, пока стек не станет пустым.

Порядок выполнения функций

В примере выше нет ничего специфичного: JavaScript анализирует код и определяет порядок вызова функций.

Давайте посмотрим, как мы можем изменить порядок вызова функций, сделав так, что определённая функция будет вызвана последней. Для этого мы выполним вызов нашей функции посредством browser API:

setTimeout(() => {}, 0);

Рассмотрим следующий пример:

Как писать эффективный код на JavaScript с помощью Event Loop 7

Результат выполнения этого кода для некоторых может стать неожиданным:

Как писать эффективный код на JavaScript с помощью Event Loop 8

Когда этот код выполняется, сначала вызывается foo(). Внутри foo() мы сначала вызываем setTimeout, передавая bar в качестве аргумента, а временным интервалом указываем 0, чтобы вызов произошёл настолько быстро, насколько это возможно. Затем мы вызываем baz().

На этом этапе стек вызовов выглядит следующим образом:

Как писать эффективный код на JavaScript с помощью Event Loop 9

Порядок вызова функций в этом случае будет выглядеть так:

Как писать эффективный код на JavaScript с помощью Event Loop 10

Далее мы рассмотрим, почему так происходит.

Очередь сообщений (The Message Queue)

Когда вызывается setTimeout, браузер или Node.js запускают таймер. Когда время таймера истекает (в нашем случае это произойдёт немедленно, так как мы указали 0 в качестве временного интервала), наша callback-функция будет помещена в очередь сообщений.

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

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

Нам не нужно ждать, пока такие функции, как setTimeout, fetch или другие выполняют свою работу, поскольку они предоставляются браузером и живут в своих потоках. Например, если вы установите время ожидания setTimeout равным 2 секундам, вам не придётся ждать 2 секунды — ожидание происходит в отдельном потоке.

Очередь заданий (ES6 Job Queue)

ECMAScript 2015 представил концепцию очереди заданий, которая используется в Promises (также представлена в ES6/ES2015). Это способ выполнить результат асинхронной функции как можно скорее, а не помещать его в конец стека вызовов. Обещания, которые разрешаются до завершения текущей функции, будут выполняться сразу после текущей функции.

Это своего рода VIP-очередь, обработка которой имеет приоритет по отношению к обычной очереди.

Пример:

Как писать эффективный код на JavaScript с помощью Event Loop 11

Результат:

Как писать эффективный код на JavaScript с помощью Event Loop 12

В этом большая разница между Promises (а также async/await, который построен на Promises) и привычными асинхронными функциями через setTimeout() или другие API-платформы.

Надеемся, статья поможет вам разобраться с работой Event Loop в JavaScript, включая работу с потоками, очередями событий и API браузера. Для наглядности мы рассмотрели несколько примеров и то, как их можно оптимизировать с точки зрения производительности.

Следите за новыми постами
Следите за новыми постами по любимым темам
20К открытий20К показов