24 сентября 2019

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

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

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

Noveo JavaScript noveogroup

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

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

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

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

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

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

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

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

Стек вызовов

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

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

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

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

Noveo JavaScript noveogroup

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

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

Noveo JavaScript noveogroup
(имена функций bar, baz, foo взяты в качестве примера)

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

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

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

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

Noveo JavaScript noveogroup

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

Noveo JavaScript noveogroup

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

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

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

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

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

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

Noveo JavaScript noveogroup

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

Noveo JavaScript noveogroup

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

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

Noveo JavaScript noveogroup

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

Noveo JavaScript noveogroup

 

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

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

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

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

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

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

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

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

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

Обещания, которые разрешаются до завершения текущей функции, будут выполняться сразу после текущей функции.

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

Пример:

Noveo JavaScript noveogroup

Результат:

Noveo JavaScript noveogroup

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

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

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Читайте в нашем блоге

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: