Понимание выполнения функций JavaScript — стеки вызовов, циклы событий, задачи и многое другое

JavaScript
  • Оригинальный автор:Gaurav Pandvia
  • Оригинальная ссылка:medium.com/@G aura V. Тарелка…
  • Для некоторых ссылок в тексте может потребоваться лестница.
  • Критика и исправления приветствуются.

Сегодня веб-разработчики (мы предпочитаем, чтобы их называли фронтенд-инженерами) могут делать все, что угодно, используя язык сценариев, от обеспечения взаимодействия в браузере до разработки компьютерных игр, настольных инструментов, кроссплатформенных мобильных приложений и даже развертывания на стороне сервера. (например, самый популярный Node.js) для подключения к любой базе данных. Поэтому важно понимать внутренности Javascript, чтобы его можно было использовать оптимально и эффективно. Это также является целью данной статьи.

Экосистема Javascript становится все более сложной. Для создания современного веб-приложения неизбежно использование Webpack, Babel, ESLint, Mocha, Karma, Grunt... Что мне использовать? Для чего они? Я нашел этот комикс, который прекрасно иллюстрирует бедственное положение современных веб-разработчиков:

Javascript疲劳症——学习Javascript是什么感觉
Усталость от Javascript — каково это изучать Javascript

Прежде чем погрузиться с головой в море фреймворков и библиотек, каждый разработчик Javascript должен сначала понять, как реализован Javascript под капотом. Почти каждый JS-разработчик слышал термин «V8», но некоторые могут понятия не иметь, что это слово означает и почему оно используется. В первый год моей карьеры в сфере профессионального развития я очень мало знал об этих причудливых терминах и больше заботился о том, чтобы сначала выполнить работу. Но это не удовлетворяет моего любопытства, мне интересно, как Javascript может все это делать. Я решил копнуть немного глубже, погуглил и нашел несколько отличных блогов, в том числеPhilip Robertsизa great talk at JSConf on the event loop. Поэтому я решил обобщить свой опыт обучения и поделиться им. Так как нужно знать так много, я разделил эту статью на две части. В этой части вводятся общие термины, а во второй части объясняется, как эти термины связаны.

Javascript — это однопоточный одновременный язык, что означает, что он может обрабатывать только одну задачу за раз и выполнять один фрагмент кода. Его стек вызовов вместе с кучей и очередью составляютМодель параллелизма Javascript(реализовано в V8). Давайте рассмотрим эти слова одно за другим.

Visual Representation of JS Model(credits)
Visual Representation of JS Model

  1. Стек вызовов: это структура данных, которая записывает функции, которые мы вызываем в программе. Если мы вызываем функцию для выполнения, мы помещаем какую-то запись в верхнюю часть стека вызовов; когда мы возвращаемся из функции, мы извлекаем запись из вершины стека вызовов.

JS Stack Visualization
JS Stack Visualization

Когда мы запускаем код на изображении выше, мы сначала ищем начало всего выполнения — основную функцию. В приведенном выше примере последовательность выполнения начинается сconsole.log(bar(6)), то на этот раз выполнение помещается в стек вызовов, а слоем над ним является функцияbarи ее параметры, функцияbarВызовите функциюfoo,fooтакже помещается в стек; иfooнемедленноreturnимеет значение, поэтому выталкивается из стека вызовов; аналогично,barто всплывает, наконецconsoleОператор напечатал результат и вытолкнул его. Все эти действия, одно за другим, произошли в мгновение ока.

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

Error stack trace
Error stack trace

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

  1. куча: Объект будет размещен в куче — свободной структуре в памяти. Все выделения памяти для переменных и объектов происходят в куче.
  2. очередь: среда выполнения Javascript, которая содержит очередь сообщений, которая представляет собой серию сообщений, которые необходимо обработать, и связанные с ними функции обратного вызова, которые необходимо выполнить. Когда в стеке вызовов достаточно места, сообщение берется из очереди и обрабатывается, что вызывает связанную функцию (и, таким образом, генерирует слой стека инициализации). Когда стек снова очищается, обработка сообщения заканчивается. Проще говоря, эти сообщения ставятся в очередь, а функции обратного вызова задаются для реагирования на внешние асинхронные события (такие как щелчки мыши или ответы на HTTP-запросы). Например, если пользователь нажимает кнопку без соответствующей функции обратного вызова, сообщение не будет помещено в очередь.

цикл событий

Когда мы оцениваем производительность кода JS, нам нужно знать, что функции в стеке вызовов сделают программу быстрее или медленнее,console.log()будет быстро, но сforилиwhileИтерация в тысячи раз будет медленнее и будет держать стек вызовов занятым и заблокированным. это называетсяблокирующий скрипт, вы можете быть вWebpage Speed Insightsвидел в.

Сетевые запросы будут медленными, а запросы изображений будут медленными, но, к счастью, служебные запросы можно выполнять с помощью асинхронных функций, таких как AJAX. Что, если бы эти сетевые запросы выполнялись с помощью синхронных функций? Сетевой запрос отправляется на сервер — сервер — это просто какая-то машина где-то, теперь предположим, что сервер может медленно возвращать ответ, в этот момент, если я нажму какую-нибудь кнопку CTA (призыв к действию), или что-то другое, что нужно сделать Рендеринг, ничего не происходит, потому что стек вызовов все еще заблокирован предыдущими сетевыми запросами. В многопоточных языках, таких как Ruby, эту ситуацию можно контролировать, но в однопоточных языках, таких как Javascript, если функция в стеке вызовов не возвращает значение, она всегда блокируется. Браузер ничего не делает и страница вылетает. Таким образом, мы не можем обеспечить плавный пользовательский интерфейс для конечного пользователя. Так что же нам делать?

«Параллелизм в JS — делайте одно за раз, кроме асинхронных обратных вызовов»

Самым ранним решением было использование асинхронных обратных вызовов, что означало, что мы добавляли обратный вызов к некоторой части кода, который выполнялся бы после выполнения кода. Наверное, все мы сталкивались с чем-то вроде AJAX-запросов.$.get(),setTimeout(),setInterval(),Promisesасинхронный обратный вызов. Узел основан на асинхронном выполнении функций. Все эти асинхронные обратные вызовы не будут похожи наconsole.log()Подождите, пока синхронная функция запустится немедленно, но в какой-то момент позже, чтобы она не была помещена в стек вызовов немедленно. Так куда они пошли? Как их контролировать?

Как и в приведенном выше примере, если сетевой запрос сделан в Javascript:

1. 请求函数被执行,给`onreadystatechange`事件传一个匿名函数作为回调,用来在将来响应就绪的时候执行。
2. “Script call done!”立刻输出到控制台。
3. 后续某时刻,响应被返回,回调被执行,响应体被输出到控制台。

Раздельный вызов ответа позволяет среде выполнения Javascript выполнять другие действия, ожидая завершения асинхронной операции и разворачивая выполнение обратного вызова. Браузер подключается и вызывает свой API, который представляет собой API, реализованный на C++ для создания потоков для управления асинхронными событиями, такими как события DOM, HTTP-запросы, setTimeout и т. д.

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

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

Javascript Event Loop Visual Representation

В веб-браузере, как только событие происходит и связывает прослушиватель событий, сообщение немедленно добавляется в очередь. Если обработчика нет, значит, событие потеряно. Так что нажмите на обработчик событий щелчка привязки, добавьте сообщение и другие события. Вызов обратного вызова будет начальным слоем в стеке вызовов, а поскольку JavaScript является однопоточным, все вызовы в стеке вызововreturnРанее опрос и обработка последующих сообщений были приостановлены. Последующие (синхронизированные) вызовы функций добавляют новые слои в стек вызова.

В следующей части я покажу выполнение кода описанного выше процесса с анимацией, подробно объясняя, что представляют собой различные типы асинхронных функций, кто выполняется первым в очереди, а также приемы для таких функций, как нулевая задержка.