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

JavaScript - это однопоточный язык: одновременно может выполняться только одна задача. Обычно в этом нет ничего сложного, но теперь представьте, что вы запускаете задачу, которая занимает 30 секунд... Да. Во время этой задачи мы ждем 30 секунд, прежде чем что-либо еще может произойти (по умолчанию JavaScript запускается в главном потоке браузера, поэтому весь пользовательский интерфейс будет ждать).

К счастью, браузер предоставляет нам некоторые функции, которые сам механизм JavaScript не предоставляет: Web API. Который включает в себя DOM API, setTimeout, HTTP-запросы и так далее. Это может помочь нам создать асинхронное неблокирующее поведение.

Когда мы вызываем функцию, она добавляется в call stack (стек вызовов). Стек вызовов является частью механизма JS, это не зависит от браузера. Это классический взгляд на стек, т.е first in - last out. Когда функция возвращает значение, она "выталкивается" из стека.

Функция respond возвращает функцию setTimeout. SetTimeout предоставляется нам через веб-API: он позволяет нам делать задачи, не блокируя основной поток. Callback функция, которую мы передали в функцию setTimeout, лямбда функция () => {return 'Hey'} добавляется в веб-API. Тем временем функция setTimeout и функция respond извлекаются из стека, они оба возвращают свои значения.

В Web API таймер работает до тех пор, пока второй аргумент, который мы передали ему, не подождет 1000 мс. Callback не сразу добавляется в стек вызовов, а передается в нечто, называемое очередью.

Это может сбивать с толку: это не означает, что callback функия добавляется в стек вызовов (таким образом, возвращает значение) через 1000 мс! Он просто добавляется в очередь через 1000 мс. Но в этой очереди, функция должна ждать пока придет ее черёд.

Теперь это та часть, которую мы все ждали... Время для Event Loop выполнить единственную задачу: соединить очередь со стеком вызовов! Если стек вызовов пуст, то есть, если все ранее вызванные функции вернули свои значения и были извлечены из стека, первый элемент в очереди добавляется в стек вызовов. В этом случае никакие другие функции не были вызваны, что означает, что стек вызовов был пуст к тому времени, когда callback функция была первым элементом в очереди.

Callback добавляется в стек вызовов, вызывается и возвращает значение, а также извлекается из стека.

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

const foo = () => console.log("First")
const bar = () => setTimeout(() => console.log("Second"), 500)
const baz = () => console.log("Third")

bar()
foo()
baz()

Давайте посмотрим, что происходит, когда мы запускаем этот код в браузере:

  1. Мы вызываем bar. bar возвращает функцию setTimeout.
  2. Callback который мы передали в setTimeout добавляется в Web API, функция setTimeout и bar извлекаются из стека вызовов.
  3. Таймер запускается, тем временем foo вызывается и записывает в журнал First. foo возвращает (undefined), baz вызывается и callback добавляется в очередь.
  4. baz логирует Third. Цикл обработки событий видит, что call stack пуст после возврата baz, после чего callback добавляется в стек вызовов.
  5. Callback логирует Second.

Visualizing the JavaScript runtime at runtime.