Часть первая: стек вызовов, механизмы JavaScript и веб-API

Вы ждете своей очереди. Кассир звонит пьяному студенту Funyuns через несколько мест перед вами. Но старшекурсник понял, что забыл свой бумажник. Он говорит, что скоро вернется, и выбегает за дверь. Что должно произойти сейчас? Там очередь, но кассирша уже начала обзванивать заказ этого парня. Он бежит через улицу? Через город? Мы все ждем его возвращения? Он вообще не забудет вернуться? Если бы мы были синхронной функцией, нам пришлось бы ждать столько, сколько потребуется.

В синхронизации

Одним из решений описанной выше проблемы может быть: перейти на другую линию. Если бы мы были в магазине Ruby, или Python, или Java, или Haskell, или Elixer, это могло бы быть решением менеджера. В конце концов, это многопоточные языки. Есть несколько линий на выбор.

Тем не менее, мы находимся в ночном винном погребе с синим флуоресцентным освещением, которым является JavaScript. Это единственное открытое место в Browsertown (кроме WebAssembly, у которого до сих пор нет доступа к DOM, так что вот). Есть одна линия. У кассира, похоже, афазия, обусловленная настроением, и нет проблем с ожиданием. Другими словами, JavaScript — это однопоточный язык. На высоком уровне это означает, что у него есть один стек вызовов и одна куча памяти.

ПРИМЕЧАНИЕ. Языки изначально не идентифицируются как однопоточные или многопоточные (т. е. однопоточность или многопоточность — это свойство обработки языка, а не «тип» языка). Метафора на основе кода может заключаться в том, что this.isMultithreaded — это метод (метафорически; это не совсем так), а не SingleThread или Multithread. классы, расширяющие родительский класс Language.

Стек вызовов

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

Стеки против кучи

Стеки вызовов и кучи памяти удачно названы, чтобы обозначить их уровень доступа. Кучи строго не организованы. Это нормально, мы можем взять то, что нам нужно, из кучи, когда нам это нужно. Это хорошо для хранения объектов и значений рядом, где мы могли бы снова ссылаться на них в произвольном порядке. Когда нам нужно больше структуры, например, когда мы хотим убедиться, что все происходит в определенном порядке, мы можем использовать стек.

Одним из ограничений стека является то, что у нас есть доступ только к вершине. Представьте себе один из тех диспенсеров для буфетных тарелок; Вы можете добраться до тарелок в нижней части стопки, только сняв тарелки с верхней части стопки. В программировании это называется методом обработки данных LIFO (Last In, First Out).

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

alert('You clicked me!');
  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
/* source: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing */

Блокирующие функции

Javascript будет оценивать код построчно. Итак, когда он дойдет до первой строки — alert() — появится сообщение. Теперь остальная часть нашего кода должна ожидать завершения оповещения. Это пример функции блокировки. Пока функция не будет завершена (мы нажали «ОК»), мы не можем видеть наш недавно добавленный абзац или взаимодействовать с чем-либо еще, что может быть на нашей странице.

Однопоточный асинхронный

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

Но ждать. Если Javascript является однопоточным, вы можете спросить, если он имеет только один стек вызовов, который может работать только с вершиной стека, как он может быть асинхронным? Ответ: он справляется с небольшой помощью своих друзей.

Требуется браузер

Чтобы JavaScript работал асинхронно, ему нужны некоторые вещи, которые может предложить только браузер (или Node.js). А именно, движки JavaScript и веб-API.

Механизмы JavaScript — это компиляторы, которые превращают наш приятный, удобочитаемый сценарий в машинный код. Они делают это с помощью Just-In-Time Compilation, инновации движка Chrome V8, которая с тех пор была подхвачена движком Safari’s Nitro и Firefox’s SpiderMonkey. Это позволяет компилировать наш код по мере необходимости во время выполнения, а не сразу перед выполнением.

Веб-API стоят за многими наиболее распространенными функциями, которые мы пишем в веб-приложениях. Сам DOM исходит из веб-API. Мы также можем узнать вездесущий Fetch API. Это первый ключ к раскрытию асинхронной функциональности JavaScript. Веб-API начинают путешествие функции, обходя наш стек вызовов LIFO. Такие функции, как fetch(), извлекаются из стека и передаются в веб-API для обработки.

Итак, мы понимаем ограничения JavaScript. Мы знаем, какие инструменты он использует, чтобы обойти эти ограничения. Теперь мы перенесли нашу функцию в Web API. Но как оно возвращается? И что с этим связано? Это вопросы, на которые я попытаюсь ответить во второй части: очереди обратного вызова, циклы событий и промисы.