Думайте о стеке вызовов Javascript как о списке задач функций. Как следует из названия, он функционирует как стек, в котором функции, ожидающие выполнения, укладываются друг на друга и удаляются, начиная сверху (последний пришел, первый вышел). Точнее говоря, функции вводятся в стек при вызове и удаляются из стека, как только возвращают значение. Инструменты разработчика Chrome позволяют нам видеть стек на вкладке исходного кода, и это отличный способ отладки кода, потому что вы можете точно видеть, какие функции вызываются, аргументы, которые они принимают, и их строку кода. Давайте посмотрим, как работает стек вызовов.

function hello() {  return 'hello'; 
} 
function talk() { 
  return hello(); 
} 
talk(); 

Когда вызывается talk(), функция talk добавляется в стек вызовов и выполняется. Функция hello вызывается из функции talk, поэтому она тоже добавляется в стек вызовов. Стек вызовов теперь выглядит так: talk => hello, где main — это функция-оболочка Node (представьте, что это сама наша программа; когда main добавляется, наша программа запускается). В стек вызовов больше ничего не добавляется, и он начинает раскручиваться. Hello возвращает «hello» и удаляется из стека вызовов. Talk возвращает «привет» и также удаляется из стека. Кода после talk() больше нет, поэтому main удаляется из стека.

Когда мы запускаем обычный синхронный код, все делается через стек вызовов, а функции вызываются блокирующим образом. Вспомните стек. Мы не можем выполнить функцию, которая не находится на вершине стека, поэтому каждая функция под ней должна ждать одну за другой, пока следующая функция не будет удалена. Однако асинхронные функции позволяют выполнять синхронный код, пока он ожидает своей очереди (часто ожидая данных от вызовов API или таймера от setTimeOut). Это приводит к повышению производительности и является одним из преимуществ создания программы на Node. Как это работает?

function fetchSomeData() {
 return setTimeOut(function(){
  console.log('data fetched')
 })
}
function hello() {
console.log('hello')
}
fetchSomeData()
hello()

Если мы запустим этот код, «Hello» записывается перед «выборкой данных», даже если fetchSomeData была вызвана до этого. Давайте погрузимся в стек вызовов, чтобы увидеть, что происходит. fetchSomeData отправляется в стек вызовов. Однако он асинхронный, поэтому на самом деле он будет удален из стека вызовов и отправлен в пул потоков (следовательно, многопоточность. Node.js по умолчанию — 4 потока), где он будет терпеливо ждать поступления своих данных. Когда данные извлекаются, они перемещаются в очередь событий. Тем временем это освобождает стек вызовов, и наш код продолжает работать. Затем Hello добавляется в стек вызовов, выводит «hello» на консоль, возвращает неопределенное значение и удаляется из стека вызовов.

Но у нас все еще есть наша функция setTimeOut, ожидающая в очереди событий. Когда стек вызовов пуст, Node проверит очередь событий и добавит все, что находится внутри нее, в стек вызовов. Таким образом, функция setTimeOut завершает выполнение, и полученные данные выводятся на консоль. Этот цикл переноса асинхронного кода в очередь, а затем обратно в стек вызовов, по сути, является тем, что делает цикл обработки событий Javascript. Это также делает асинхронный код немного непредсказуемым. Мы не знаем, как долго он будет оставаться в пуле потоков, а если у нас есть несколько асинхронных кодов, мы не знаем, в каком порядке они покидают пул потоков. Также обратите внимание, что пул потоков — не единственный способ обработки асинхронного кода, и эта ответственность часто делегируется другому интерфейсу, например операционной системе.