Как работает JavaScript в браузерах и узлах?

JavaScript
Как работает JavaScript в браузерах и узлах?

Оригинальный адрес:How JavaScript works in browser and node?

Есть много увлеченных разработчиков, которые занимаются фронтендом или бэкендом, посвящая свою молодость и пот JavaScript. JavaScript — очень простой для понимания язык и, без сомнения, очень важная часть разработки интерфейса. Но в отличие от других языков, он является однопоточным, что означает, что одновременно может выполняться только один фрагмент кода. Поскольку выполнение кода является линейным, если какой-либо код выполняется в течение длительного времени, он блокирует код, который необходимо выполнить позже. Так что иногда вы увидите что-то вроде этого в Google Chrome:



Когда вы открываете веб-сайт в своем браузере, он использует поток выполнения JavaScript. Этот поток отвечает за реагирование на все, например прокрутку страниц, рендеринг страниц, прослушивание событий DOM (таких как нажатия пользователем кнопок) и так далее. Но если выполнение JavaScript заблокировано, браузер ничего не может сделать, а это значит, что он зависнет и не сможет ответить.

Если вы мне не верите, просто введите в консоль и попробуйте:

while(true){}
// ...

Любой код после приведенного выше оператора не будет выполнен. Этот «бесконечный цикл» будет занимать системные ресурсы, делая браузер неспособным реагировать на действия пользователя. Это также может произойти с бесконечными рекурсивными вызовами, но, как описано ниже, механизм Javascript ограничивает длина стека вызовов, а бесконечная рекурсия вызовет исключение RangeError вместо того, чтобы работать бесконечно.



Благодаря современным браузерам не все открытые вкладки теперь зависят от одного потока JavaScript. Вместо этого у каждой вкладки или домена будет свой собственный поток JavaScript. Таким образом, каждая вкладка не будет блокировать друг друга. Например, вы можете открыть несколько вкладок в Chrome и выполнить описанный выше бесконечный цикл на определенной вкладке.Вы обнаружите, что застряла только та вкладка, которая выполняет указанный выше оператор, а остальные не затронуты.



Стек вызовов

Для того чтобы визуализировать, как JavaScript выполняет программу, нам сначала нужно понимать время выполнения JavaScript.


Как и другие языки программирования, среда выполнения JavaScript имеет стек (Stack) и кучу (Heap) памяти.

Изображение выше взято из статьи [Fhinkel] (Запутался в стеке и куче?), которая делает разницу между стеком и кучей более ясной.Например:

В Java или C# типы значений (примитивы) хранятся в стеке, а ссылочные типы (ссылки) — в куче. Спецификация C++ не определяет выделение памяти в стеке и куче, но использует自动存储(automatic)期а также动态存储(dynamic)期Чтобы различать, локальные переменные имеют автоматическую продолжительность хранения, компилятор будет хранить их в стеке. Динамически размещаемые объекты обычно хранятся в куче. Данные, помещенные в стек, будут автоматически восстановлены после выполнения функции, а объекты, помещенные в кучу, если их не освободить, вызовутутечка памяти


Эта статья не будет подробно объяснять Heap, вы можете прочитатьздесьВ этой статье нас интересуют стеки,Стек представляет собой структуру данных LIFO (последний вошел, первый вышел), используемую для сохранения текущего контекста выполнения функции программы, другими словами, он представляет текущую позицию выполнения программы.Каждый раз, когда функция начинает выполняться, функция выдвигается В стеке извлекайте из стека, когда функция возвращается. Когда стек пуст, это означает, что ни одна программа не выполняется. Поэтому стек часто называют «стеком вызовов»..

function baz(){
   console.log('Hello from baz')
}
function bar() {
   baz();
}
function foo() {
   bar();
}
foo();

Следовательно, когда указанная выше программа будет загружена в память, она начнет выполнять первую функцию, т.е.foo. Таким образом, первый элемент стекаfoo(), потому чтоfooфункция вызоветbarФункция, второй элемент стекаbar(); тоже самоеbarфункция вызоветbaz, третий элемент стекаbaz(). наконец,bazпередачаconsole.log, последний элемент стекаconsole.log('Hello from baz')

Стек извлекается, когда функция завершает выполнение (достигает нижней части функции или вызывает возврат). Затем продолжайте выполнять оператор после вызова функции:


В каждом элементе стека состояние элемента также называется кадром стека.Если во время вызова функции возникает ошибка, JavaScript выводит трассировку стека, которая представляет собой снимок кадра стека во время выполнения кода.

function baz(){
   throw new Error('Something went wrong.');
}
function bar() {
   baz();
}
function foo() {
   bar();
}
foo();

В приведенной выше программе мыbazЕсли возникает ошибка, JavaScript распечатает стек и запись, указав, где произошла ошибка, и сообщение об ошибке.


Размер стека не бесконечен. Например, Chrome ограничивает стек до 16 000 кадров. Таким образом, бесконечная рекурсия заставит Chrome выбрасыватьMaximum Call Stack size exceeded:



Цикл событий и веб-API

Потому что JavaScript является однопотоком, у него есть только один стек и куча.因此,如果其他程序想要执行一些东西,需要等待上一个程序执行完毕

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

Браузер предоставляет среду выполнения JavaScript через механизм JavaScript. Например, Chrome использует движок V8. Но в браузере есть нечто большее, чем просто движок JavaScript. Вот базовая структура браузера:



Это выглядит сложно, но это также легко понять. Механизм JavaScript должен взаимодействовать с двумя другими компонентами, а именноЦикл событий (EventLoop)а такжеОчередь обратного вызова (CallbackQueue), очереди обратного вызова также называются очередями сообщений или очередями задач.

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

Подумайте о том, насколько плохо пользовательский опыт будет, если браузер использовал только ту же нить JavaScript для обработки всех вышеуказанных функций. Потому что даже если пользователь просто прокручивает страницу, существует много вещей, которые необходимо обрабатывать за кулисами, и один javaScript не может быть занят вообще. Таким образом, браузеры будут использовать низкоуровневые языки, такие как C ++, для выполнения этих операций и выставляют краткие apis JavaScript для разработчиков. Эти API коллективно называютсяWeb API.

Эти веб-API обычно являются асинхронными.. Это означает, что вы можете дать команду этим API сделать что-то в фоновом режиме (отдельный поток) и уведомить среду выполнения Javascript, когда задача будет завершена. Тем временем механизм Javascript продолжит выполнение остальной части кода JavaScript. После команды эти API Когда мы делаем что-то в фоновом режиме, нам обычно нужно предоставить им обратный вызов. Этот обратный вызов отвечает за выполнение кода JavaScript после того, как веб-API завершит свою задачу. Давайте соберем все вышеперечисленное вместе, чтобы понять:

Когда вы вызываете функцию, она помещается в стек. Если эта функция содержит вызов веб-API, JavaScript проксирует вызов веб-API, указывает веб-API выполнить задачу и продолжает выполнять следующую строку кода, пока функция не вернется. Как только функция достигает оператора return или нижней части функции, функция извлекается из стека вызовов.

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

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

Псевдокод цикла событий выглядит так:

while(true) {
   let task
   while(task = popCallbackQueue()) {// 弹出回调队列任务
      executeTask(task) // 执行任务, 这里面可能会触发新的Web API调用
   }

   if (hasAnyPendingTask()) {
      sleep() // 睡一觉,有新任务推送到回调队列时时再唤醒我哦
   } else {
      break // 终止程序, 没什么好干的拜拜了
   }
}

Давайте пошагово рассмотрим, как все вышеперечисленное работает на примере веб-API setTimeout. Веб-API setTimeout в основном используется для задержки выполнения некоторых операций, но когда обратный вызов действительно выполняется, ему нужно дождаться окончания выполнения текущей программы (то есть стек пуст), то естьВремя выполнения обратного вызова функции setTimeout может не совпадать с указанным вами временем задержки.. Синтаксис setTimeout следующий:

setTimeout(callbackFunction, timeInMilliseconds);

callbackFunction — это функция обратного вызова, которая будет выполнена через timeInMilliseconds.Мы модифицируем приведенный выше код, чтобы вызвать setTimeout:

function printHello() {
    console.log('Hello from baz');
}
function baz() {
    setTimeout(printHello, 3000);
}
function bar() {
    baz();
}
function foo() {
    bar();
}
foo();

Приведенный выше код задерживает вызов console.log. Стек останется таким же, как и раньше, напримерfoo() => bar() => baz(), Когда baz начнет выполняться и достигнет setTimeout, Javascript передаст функцию обратного вызова в веб-API и перейдет к следующей строке. Поскольку следующей строки нет, в стеке появляется baz, а затем bar и foo.

В это время веб-API выполняет 3-секундное ожидание, а когда приходит время, помещает обратный вызов в очередь обратного вызова. Поскольку стек вызовов в это время пуст, цикл событий поместит обратный вызов в стек и выполнит обратный вызов.


🎉🎉Philip RobersСоздан удивительный онлайн-инструментLoupeВнизу Javascript для визуализации операции. В приведенных выше примерах можно просмотреть этоСвязь🎉🎉

Таким образом, наш Javascript является однопоточным, но выполнение многих веб-API является многопоточным. То есть один поток Javascript означает, что выполнение «кода Javascript» является одним потоком..


Node.js

С Node.js мы можем делать больше, чем просто на стороне браузера. Итак, как это работает?

Node.js, как и Chrome, также использует движок Google V8 для обеспечения среды выполнения Javascript.libuv(написано на C++) для работы с циклом событий V8 для расширения возможностей, которые могут выполняться в фоновом режиме, таких как ввод-вывод файловой системы, сетевой ввод-вывод. API стандартной библиотеки Node использует тот же стиль обратного вызова, что и веб-API браузера.

Если вы сравните структурные диаграммы браузера и узла, вы увидите, что они очень похожи. Правая часть похожа на Web API и также содержит очередь событий (или очередь обратного вызова/очередь сообщений) и цикл обработки событий.

V8, цикл событий, очередь событий выполняется в одном потоке, а также крайний правый из рабочих потоков (Worker Thread) отвечает за обеспечение асинхронных операций ввода/вывода. Именно поэтому Node.js имеет неблокирующую, управляемую событиями архитектуру асинхронного ввода-вывода.

Вышеупомянутое содержание получено изPhilip Roberts30-минутные основные моментыречь(пять лет назад)

Конец этой статьи