Когда я впервые начал углубленно изучать Javascript, я наткнулся именно на такое описание Javascript:
Однопоточный неблокирующий асинхронный язык программирования.
В этой статье я хотел бы попытаться объяснить приведенное выше описание Javascript.
Исходя из приведенного выше описания, если Javascript однопоточный, то есть он может выполнять только одну задачу за раз, то как он может быть неблокирующим при выполнении асинхронных задач — задач, которые требуют больше времени для завершения, не блокируя выполнение основного потока? Разве Javascript не должен быть многопоточным, если он выполняет такие асинхронные задачи?
Давайте рассмотрим приведенный ниже пример и посмотрим, действительно ли Javascript является однопоточным, как он утверждает:
function getData() {
setTimeout(function print() => {
console.log("hello");
}, 1000);
}
function main() {
getData();
console.log("world");
}
main();
// output1:
// hello
// world
// OR
// output2:
// world
// hello
Основываясь на приведенном фрагменте кода, попробуйте угадать, какой результат выдаст программа — output1
или output2
?
Большинство из нас легко догадается, что это output2
, и да, это правильно. С точки зрения программиста-любителя, output2
имеет смысл, потому что getData
требует 1 секунды для разрешения, следовательно, программа запишет world
, а затем hello
, когда пройдет 1 секунда. Так в чем же здесь подвох? Ну, загвоздка в том, что Javascript является однопоточным!
Если бы Javascript действительно был однопоточным, как он утверждает, он бы выдал output1
вместо output2
. Это происходит потому, что в однопоточных языках ему пришлось бы сначала ждать разрешения getData
и блокировать выполнение последующих строк, поэтому он выводит hello
, за которым следует world
.
Но, как вы можете видеть выше, это не так, потому что вызов getData
не блокировал выполнение последующих строк (это то, что они имеют в виду, когда говорят, что Javascript неблокирующий), и программа сначала регистрировала world
, а когда getData
разрешился через некоторое время (это то, что они имеют в виду под асинхронностью — то, что занимает больше времени для завершения), она затем регистрировала hello
.
Итак, действительно ли Javascript является однопоточным, как он утверждает? Ответ — да. Но как Javascript достигает этой неблокирующей асинхронной природы, будучи однопоточным?
Видите ли, хотя сам Javascript является однопоточным языком, движки Javascript, такие как движок V8, предоставляют такие компоненты, как стек вызовов, очередь обратных вызовов, цикл событий и используют веб-интерфейсы, предоставляемые браузером, и дополняют Javascript для создания иллюзии многопоточности.
Ниже приведено исчерпывающее определение этих компонентов:
- Стек вызовов — структура данных стека, которая отслеживает, в какой части программы мы находимся. Пока движок Javascript интерпретирует код, когда он сталкивается с функцией, она помещается в стек, а когда функция возвращается, верхняя часть стека сбрасывается.
- Web APIs — набор APIs, таких как setTimout, fetchAPI и DOM Event Listeners APIs, предоставляемых браузером, которые будут потребляться движком Javascript. Эти API будут выполняться в отдельном потоке в браузере. Для вызова этих API необходимо предоставить обратный вызов Javascript.
- Очередь обратных вызовов — структура данных очереди, которая отслеживает обратные вызовы. Когда любой вызов Web API завершается, его функция обратного вызова помещается в очередь обратных вызовов.
- Event Loop — наблюдатель, который просматривает стек вызовов и очередь обратных вызовов. Когда стек вызовов пустеет, он извлекает первый элемент из очереди обратных вызовов и помещает его в стек вызовов. Этот процесс будет выполняться вечно в цикле.
Что касается приведенной выше программы, то под капотом происходит следующее:
- Вызов функции
main()
попадает в стек вызовов. - Вызов функции
getData()
попадает в стек вызовов. - Вызов функции
setTimeout()
попадает в стек вызовов. - Выполняется вызов функции Web API
setTimeout()
сprint()
в качестве обратного вызова, и Web API запускает таймер. Когда пройдет 1 секунда, Web API переместит обратный вызовprint()
в очередь обратных вызовов. - Возвращается вызов функции
setTimeout()
, и он выводится из стека вызовов. - Вызов функции
getData()
возвращается и удаляется из стека вызовов. - Вызов функции
console.log("world")
заталкивается в стек вызовов. - Вызов функции
console.log("world")
печатаетworld
и возвращается. - Вызов функции
console.log("world")
выталкивается из стека вызовов. - Вызов функции
main()
выводится из стека вызовов. - Стек вызовов теперь пуст, цикл событий просматривает очередь обратных вызовов, чтобы узнать, есть ли какие-либо обратные вызовы, которые можно поместить в стек вызовов.
- Если бы к этому моменту прошла 1 секунда, обратный вызов
print()
был бы передан в очередь обратных вызовов. Цикл событий вытаскивает обратный вызовprint()
из очереди обратных вызовов и помещает его в стек вызовов. - Функция
console.log("hello")
попадает в стек вызовов. - Функция
console.log("hello")
печатаетhello
и возвращается. - Функция
console.log("hello")
выталкивается из стека вызовов. - Функция
print()
выводится из стека вызовов.
Ниже приведена визуализация вышеупомянутого процесса, а с этой визуализацией можно ознакомиться здесь.
В заключение, я думаю, мы убедились, что Javascript — это однопоточный неблокирующий асинхронный язык программирования. Движки Javascript, такие как движок V8, дополняют Javascript и создают иллюзию его многопоточности, используя различные компоненты, такие как стек вызовов, очередь обратных вызовов, цикл событий и веб-интерфейсы.
Я хотел бы закончить эту статью викториной, чтобы проверить наше понимание внутреннего устройства Javascript, пожалуйста, ответьте на них в комментариях ниже.
// What would be the output when main is called?
// Wiil it output output1 or output2?
function main() {
console.log("1");
setTimeout(function callback() {
console.log("2");
}, 0);
console.log("3");
}
main();
// output1:
// 1
// 2
// 3
// output2:
// 1
// 3
// 2
Справка:
- Что такое цикл событий? | Филип Робертс | JSConf EU