Мы знаем javascript как однопоточный, асинхронный, параллельный и неблокирующий язык. Но первый вопрос, который возникает после прочтения этого, заключается в том, как язык может быть однопоточным, то есть способным выполнять одну задачу за раз и при этом быть асинхронным, параллельным и неблокирующим. Однопоточность подразумевает, что javascript имеет один стек вызовов, в котором хранятся вызываемые подпрограммы или функции. Однако, как и в любом стеке, в любой момент времени может выполняться только функция наверху стека вызовов. И эта функция может занять много времени. Это может включать сетевой вызов, выполнение цикла while для 1 000 000 итераций или получение каких-либо данных от пользователя. Все, что занимает много времени, если присутствует в стеке вызовов, блокирует выполнение всего, кроме этого. В браузере используется Javascript. Таким образом, это означает, что если в браузере сделать что-то вроде сетевого запроса, то пользователь не сможет нажать кнопку или заполнить форму в браузере. Но это не то, что пользователи обычно испытывают в Интернете. Пользователи могут одновременно слушать музыку, смотреть видео и загружать или скачивать файлы в веб-браузере.

Чтобы понять это явление, мы должны глубоко погрузиться во внутреннюю работу javascript. Когда вы выполняете сценарий, механизм JavaScript создает глобальный контекст выполнения (GEC) и помещает его в верхнюю часть стека вызовов. Всякий раз, когда вызывается функция, механизм JavaScript создает контекст выполнения функции (FEC) для функции, помещает его поверх стека вызовов и начинает выполнение функции.

Давайте сначала разберемся, как стек вызовов работает только с синхронным кодом. В инфографике GEC и FEC намеренно опущены, чтобы уменьшить беспорядок. Изначально стек вызовов будет пуст.

Прежде всего, console.log(‘Begin’) будет помещен внутрь стека вызовов.

Так как console.log(‘Begin’) является неблокирующей операцией ввода-вывода, она немедленно выведет Begin на консоль. Таким образом, этот элемент в стеке будет извлечен после выполнения. Затем стек вызовов снова становится пустым, как показано на рисунке 3.

Затем следующая выполняемая команда или функция помещается в стек вызовов, т.е. saySomething() в данном случае.

Далее он вызывает printHello(), поэтому он будет помещен в стек.

Затем console.log('Hello') будет помещен в стек, так как он содержится внутри printHello().

Теперь hello будет напечатано, а console.log('Hello') будет извлечено из стека вызовов.

Затем printHello() будет извлечен из стека вызовов.

Наконец, saySomething() будет извлечен из стека вызовов.

Если выполнить grep для исходного кода v8, который является средой выполнения javascript в браузере, вы не найдете ничего похожего на setTimeout(), DOM или XMLHttpRequest. Удивительно, потому что именно это одно из первых, что узнает новичок о javascript. Тогда как можно использовать setTimeout в браузере или сделать сетевой запрос из браузера.

Ответ на этот вопрос — Web Apis. Веб-API не являются частью языка javascript, но построены на основе javascript, чтобы предоставить вам дополнительные функциональные возможности. Они взаимодействуют с асинхронными обработчиками операционной системы для выполнения асинхронных операций, абстрагируя сложности от среды выполнения javascript, т.е. v8. Если кто-то использует Node.js, веб-API заменяется библиотекой libuv, написанной на C++. В этом случае эта библиотека взаимодействует с асинхронными обработчиками операционной системы.

Но следующий вопрос заключается в том, что после того, как веб-API завершит длинную задачу, как они вернут данные обратно пользователю. Они не могут вывести данные в браузер в любой момент времени, так как они могут быть вне контекста. Для этого используются цикл событий и очередь обратного вызова. Очередь обратного вызова, также известная как очередь задач, представляет собой очередь FIFO, в которой хранятся те асинхронные задачи, которые завершены и готовы к отправке в стек вызовов. Цикл событий постоянно проверяет, пуст ли стек вызовов, и если да, то помещает первый элемент очереди обратного вызова в стек вызовов.

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

Теперь управление передается первому оператору кода, т. е. console.log(‘Begin’). Он помещается в стек вызовов, как на шаге 2, выполняется, и строка Begin печатается на консоли после того, как она выталкивается, как показано на шаге 3.

Затем функция printDelayedHi() помещается в стек вызовов, как на шаге 4.

На шаге 5 базовый код внутри printDelayedHi(), который представляет собой setTimeout(() =› {console.log(‘Hi’)}, 3000), помещается в стек. Поскольку это асинхронная функция, она будет обрабатываться веб-API. Таким образом, этот фрагмент извлекается из стека вызовов и передается веб-API, как на шаге 6.

Фрагмент setTimeout займет 3 секунды в веб-API. Между тем, printDelayedHi() будет извлечен из стека вызовов, как на шаге 7.

Затем console.log(‘End’) помещается в стек вызовов, как на шаге 8.

Он немедленно выполняется и выводится на консоль, как на шаге 9. И стек вызовов снова становится пустым.

Теперь, по прошествии 3 секунд, console.log('Hi') извлекается из веб-API и помещается в очередь обратного вызова, как на шаге 10.

Затем следует роль цикла событий. Цикл событий проверяет, пуст ли стек вызовов, и если да, то он помещает передний элемент очереди обратного вызова в стек, как на шаге 11. Обратите внимание, что если в стеке вызовов уже что-то было, то обратный вызов очередь не будет исключена из очереди, и в стек вызовов ничего не будет помещено.

Наконец, как и на шаге 12, верхний элемент стека вызовов, т. е. console.log('Hi"), извлекается из стека и отображается в консоли.

Таким образом, javascript удалось запустить функцию setTimeout, которая является асинхронной функцией, несмотря на то, что она однопоточная и имеет только один стек вызовов. В случае браузера он использует веб-API, а в случае Node js — библиотеку libuv. Кроме того, он использует очередь обратного вызова и цикл обработки событий. Javascript — это мощный язык, который может обрабатывать логику уровня пользовательского интерфейса, а также запускать веб-серверы. Его довольно легко выучить, и это делает его одним из самых популярных языков в мире. Я надеюсь, что эта статья помогла вам понять внутреннюю работу javascript, когда он работает как в вашем браузере, так и на вашем сервере.