- Вступление
- Блокировка цикла событий
- Стек вызовов
- Простое объяснение цикла событий
- Выполнение функции очереди
- Очередь Сообщений
- Очередь заданий ES6
Вступление
Цикл событий является одним из наиболее важных аспектов для понимания JavaScript.
Я годами программировал на JavaScript, но никогда полностью не понимал, как все работает под капотом. Совершенно нормально не знать эту концепцию в деталях, но, как обычно, полезно знать, как она работает, а также вам может быть просто немного любопытно на этом этапе.
Этот пост призван объяснить внутренние детали того, как JavaScript работает с одним потоком и как он обрабатывает асинхронные функции.
Ваш код JavaScript выполняется в одном потоке. За раз происходит только одно событие.
Это ограничение, которое на самом деле очень полезно, так как оно значительно упрощает процесс программирования, не беспокоясь о проблемах параллелизма.
Вам просто нужно обратить внимание на то, как вы пишете свой код, и избегать всего, что может заблокировать поток, например синхронных сетевых вызовов или бесконечных циклов.
Как правило, в большинстве браузеров существует цикл событий для каждой вкладки браузера, чтобы изолировать каждый процесс и избежать веб-страницы с бесконечными циклами или интенсивной обработкой, блокирующей весь ваш браузер.
Среда управляет несколькими параллельными циклами событий, например, для обработки вызовов API. Веб-работники также работают в своем собственном цикле событий.
Вам в основном нужно беспокоиться о том, что ваш код будет выполняться в одном цикле событий, и пишите код с учетом этого, чтобы избежать его блокировки.
Блокировка цикла событий
Любой код JavaScript, который занимает слишком много времени для возврата управления в цикл событий, заблокирует выполнение любого кода JavaScript на странице, даже заблокирует поток пользовательского интерфейса, и пользователь не сможет щелкнуть мышью, прокрутить страницу и так далее.
Почти все примитивы ввода-вывода в JavaScript неблокируются. Сетевые запросы, Node.js операции с файловой системой и так далее. Блокирование является исключением, и именно поэтому JavaScript так сильно основан на обратных вызовах, а в последнее время – на обещаниях и асинхронности/ожидании.
Стек вызовов
Стек вызовов представляет собой очередь LIFO (Последний вход, Первый выход).
Цикл событий непрерывно проверяет стек вызовов , чтобы увидеть, есть ли какая-либо функция, которую необходимо запустить.
При этом он добавляет любой найденный вызов функции в стек вызовов и выполняет каждый по порядку.
Вы знаете трассировку стека ошибок, с которой вы, возможно, знакомы, в отладчике или в консоли браузера? Браузер просматривает имена функций в стеке вызовов, чтобы сообщить вам, какая функция инициирует текущий вызов:
Простое объяснение цикла событий
Давайте возьмем пример:
Я использую foo , бар и баз как случайные имена . Введите любое имя, чтобы заменить их
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
bar()
baz()
}
foo()Этот код печатает
foo bar baz
как и ожидалось.
Когда выполняется этот код, сначала вызывается foo() . Внутри foo() мы сначала вызываем bar() , затем вызываем baz() .
На данный момент стек вызовов выглядит следующим образом:
Цикл событий на каждой итерации проверяет, есть ли что-то в стеке вызовов, и выполняет это:
пока стек вызовов не опустеет.
Выполнение функции очереди
Приведенный выше пример выглядит нормально, в нем нет ничего особенного: JavaScript находит вещи для выполнения, запускает их по порядку.
Давайте посмотрим, как отложить функцию до тех пор, пока стек не будет очищен.
Пример использования setTimeout(() => {}), 0) это вызов функции, но выполнение ее после выполнения всех остальных функций в коде.
Возьмем этот пример:
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
baz()
}
foo()Этот код печатается, может быть, удивительно:
foo baz bar
Когда выполняется этот код, сначала вызывается foo(). Внутри foo() мы сначала вызываем setTimeout, передавая bar в качестве аргумента, и мы даем ему команду работать немедленно так быстро, как он может, передавая 0 в качестве таймера. Затем мы вызываем baz().
На данный момент стек вызовов выглядит следующим образом:
Вот порядок выполнения всех функций в нашей программе:
Почему это происходит?
Очередь Сообщений
Когда вызывается функция setTimeout(), браузер или Node.js запустите таймер . Как только таймер истечет, в данном случае сразу же, как мы ставим 0 в качестве тайм-аута, функция обратного вызова помещается в Очередь сообщений .
Очередь сообщений также находится там, где инициируемые пользователем события, такие как события нажатия или клавиатуры, или получение ответов, помещаются в очередь до того, как ваш код получит возможность отреагировать на них. Или также DOM события, такие как Загрузка .
Цикл отдает приоритет стеку вызовов, и он сначала обрабатывает все, что находит в стеке вызовов, и как только там ничего нет, он отправляется собирать вещи в очереди сообщений.
Нам не нужно ждать, пока такие функции, как setTimeout , fetch или другие, выполнят свою собственную работу, потому что они предоставляются браузером и живут в своих собственных потоках. Например, если вы установите setTimeout тайм-аут на 2 секунды, вам не нужно ждать 2 секунды – ожидание происходит в другом месте.
Очередь заданий ES6
В ECMAScript 2015 введена концепция очереди заданий, которая используется в Promises (также введена в ES6/ES2015). Это способ выполнить результат асинхронной функции как можно скорее, а не помещать его в конец стека вызовов.
Обещания, которые будут выполнены до завершения текущей функции, будут выполнены сразу после текущей функции.
Я нахожу приятной аналогию с поездкой на американских горках в парке развлечений: очередь сообщений ставит вас в конец очереди, позади всех остальных людей, где вам придется ждать своей очереди, в то время как очередь на работу – это билет fastpass, который позволяет вам совершить еще одну поездку сразу после того, как вы закончили предыдущую.
Пример:
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
new Promise((resolve, reject) =>
resolve('should be right after baz, before bar')
).then(resolve => console.log(resolve))
baz()
}
foo()Это печатает
foo baz should be right after baz, before bar bar
Это большая разница между обещаниями (и Async/await, который построен на обещаниях) и простыми старыми асинхронными функциями через setTimeout() или другие API платформы.
Оригинал: “https://flaviocopes.com/javascript-event-loop/”