Привет, добро пожаловать обратно.
Если вы уже знакомы с первой частью этого руководства, то эта часть не вызовет у вас затруднений. Но если нет, то я настоятельно рекомендую вам ознакомиться с частью 1, которая служит основой для этого руководства. Но вы также можете прочитать и почерпнуть что-то из этой статьи. Это руководство полностью независимо от предыдущего.
К настоящему времени мы узнали в части 1
-
Немного истории JavaScript
-
JavaScript Runtime, который, по сути, является специальной средой, предоставляемой браузером или контекстом, в котором мы выполняем наш код. Эта среда предоставляет нам объекты, API и другие компоненты, чтобы наш код мог взаимодействовать с внешним миром и выполняться.
-
Компоненты среды выполнения: движок JavaScript, Web APIs, очередь обратных вызовов и Eventloop.
-
Движок JavaScript, который состоит из Callstack или Execution Stack и Heap.
-
Контекст выполнения, который, по сути, является специальной средой для выполнения кода JavaScript, содержит код, который выполняется в данный момент, и все, что помогает его выполнению. Эта специальная среда называется контекстом выполнения.
-
Scoping и Temporal Dead Zone. Мы узнали, как функции могут получать доступ к объявлениям из родительской области видимости с помощью Lexical Scoping. Мы также вкратце обсудили TDZ.
В этой статье мы сосредоточимся на остальных трех основных компонентах времени выполнения JavaScript в контексте браузера;
- Веб-интерфейсы
- Очередь обратных вызовов
- Eventloop
Недостаток однопоточной природы JS
Как мы все знаем, JavaScript является однопоточным по своей природе, поскольку у него есть только одна куча и один стек. Следующая программа должна сидеть и ждать, пока текущая программа закончит выполнение, очистит кучу и стек вызовов и начнет выполнение новой программы.
Но что, если текущая выполняемая задача занимает так много времени, что, если наш текущий контекст выполнения запрашивает некоторые данные с сервера (медленный сервер), определенно это займет некоторое время. В этом случае очередь Callstack застрянет, так как JavaScript выполняет только одну задачу за раз. Все контексты выполнения, которые будут выполняться следующими, будут ждать, пока наш текущий виновный контекст выполнения не будет решен. Как мы можем справиться с подобным поведением. Как мы можем запланировать выполнение некоторых задач или припарковать некоторые дорогостоящие задачи, чтобы наше приложение продолжало работать? Как мы можем сделать наш синхронный JavaScript асинхронным?
Именно здесь на помощь приходят Web API, очередь обратных вызовов и Eventloop.
Веб-интерфейсы
Прежде всего, мы должны знать, что Web API не являются частью движка JavaScript, но они являются частью веб-браузеров. Они предоставляют нашему коду некоторые дополнительные возможности, чтобы наш код мог взаимодействовать с внешним миром. Современные веб-браузеры имеют множество веб-интерфейсов API, которые мы можем использовать в наших приложениях. Мы часто используем веб-интерфейсы API для выполнения этих благородных целей в нашем приложении:
- Манипулирование доменами: Вероятно, наиболее часто используемым веб-интерфейсом среди разработчиков JavaScript является DOM API, который позволяет нам манипулировать DOM и стилями CSS, связанными с элементами DOM. Он дает нам возможность динамически добавлять узлы в DOM, удалять их или применять к ним стили.
Манипуляции с графикой: Веб-интерфейсы, такие как Canvas API и Web Graphics Library API, позволяют рисовать и манипулировать графикой в элементе холста. Они позволяют программно добавлять или изменять графику. API Canvas предоставляет средства для рисования графики с помощью JavaScript и элемента HTML <canvas>
. Помимо прочего, он может использоваться для анимации, игровой графики, визуализации данных, работы с фотографиями и обработки видео в реальном времени. API Canvas в основном ориентирован на двухмерную графику.
Асинхронный JavaScript: Как мы уже знаем, изначально JavaScript является однопоточным синхронным по своей природе. Но мы можем сделать его асинхронным с помощью таких веб-интерфейсов, как XMLHttpRequest и его замена на основе обещаний, Fetch API. Мы можем позволить нашему JavaScript-коду выполнять другие жизненно важные задачи, пока не будет получен ответ от сервера. Мы еще поговорим об асинхронном JavaScript. А пока давайте просто двигаться вперед.
Работа с мультимедиа: Веб-интерфейсы, такие как Audio API и HTMLMediaElement, предоставляют нам контроль над аудио и видео в Интернете. Мы можем приостанавливать, воспроизводить, пересылать наши медиафайлы или добавлять определенные эффекты к потоку.
Работа с файлами: File API позволяет веб-приложениям получать доступ к файлам и их содержимому. Веб-приложения получают доступ к файлам, когда пользователь делает их доступными, либо с помощью элемента <input>
, либо путем перетаскивания.
Все слушатели событий, вызовы AJAX и функции таймера находятся в контейнере Web APIs до тех пор, пока не сработает событие.
Прежде чем углубиться в очередь обратных вызовов, мы должны сначала обсудить функции обратного вызова.
Что такое функции обратного вызова?
Функция обратного вызова — это обычная функция, предоставляемая в качестве аргумента другой обычной функции, ожидающая вызова внутри функции-получателя в подходящее время.
Позвольте мне объяснить вам это. Обратный вызов не является особым видом функции. Это просто обычная функция, которая передается какой-то другой функции, и эта функция обрабатывает вызов (call) нашей функции обратного вызова. См. эту функцию обратного вызова, переданную клику eventlistner;
Как вы видите, мы предоставляем слушателю функцию обратного вызова. Эта функция вызывается не сразу, а когда происходит щелчок. Таким образом, функция обратного вызова сидит и ждет своего срабатывания где-то в движке JavaScript. Когда пользователь взаимодействует и нажимает кнопку мыши, вызывается эта функция. Место, где сидит и ждет эта функция, — очередь обратного вызова.
Сначала функции обратного вызова использовались для обработки асинхронных задач в JavaScript. Они работали довольно хорошо, но обработка более сложных задач превратилась в ад. Потому что когда функция обратного вызова также является асинхронной функцией и получает другой обратный вызов, синтаксис становился очень запутанным, что в наши дни мы называем «Ад обратного вызова».
Ну, это не наша тема на сегодня, вы можете ознакомиться с обратными вызовами здесь.
Очередь обратных вызовов
Очередь обратного вызова — это структура данных, в которой хранятся функции обратного вызова, отправленные веб-интерфейсами. Функции обратного вызова находятся здесь до тех пор, пока Callstack не опустеет, и Eventloop передает их в Callstack.
Как мы все знаем, JavaScript изначально является однопоточным синхронным языком, и он выполняет ваши фрагменты кода последовательно. Строка за строкой. Если строка 13 занимает больше времени, чем ожидалось, наша программа замирает и не пытается выполнить строку 14, пока не завершится строка 13. Именно эту проблему решает очередь обратного вызова. Взгляните на веб-интерфейс setTimeout:
setTimeout Web API выполняет заданную функцию обратного вызова через указанное время. Подробнее о setTimeout можно узнать здесь.
Если рассматривать JavaScript как однопоточный, синхронный язык программирования, то какой результат мы ожидаем от приведенного выше кода:
Ожидаемый
Во-первых, 1
будет записано в консоль в результате console.log(1)
.
Затем, после ожидания в течение 1 секунды (1000 мс), будет выполнена функция обратного вызова, регистрирующая 2
в консоли.
И, наконец, программа завершится после записи 3
в консоль в результате console.log(3)
.
Вывод
Первое из 1
записывается в журнал, как и ожидалось.
Неожиданно, следующим в консоль выводится 3
. Очевидно, движок JavaScript пропустил setTimeout и перешел к третьему утверждению console.log(3)
.
Затем после 1
в консоль выводится второй 2
.
Объяснение
Позвольте мне объяснить это поведение; сначала JavaScript встретил функцию console.log()
и сохранил ее в стеке выполнения или Callstack для выполнения. Для выполнения этой специальной встроенной функции (метода) создается Functional Execution Context (FEC), и она выполняется и исчезает из Callstack.
Во-вторых, в качестве второго утверждения, движок JavaScript сталкивается с setTimeout Web API. Как мы все знаем, setTimeout не является функцией самого JavaScript. Во-первых, setTimeout добавляется в Callstack. Но он не выполняется здесь, поскольку, будучи веб-интерфейсом, он создает задачу внутри браузера и выходит из стека вызовов.
JavaScript движок продолжает выполнять код, снова встречает функцию console.log(3)
и создает Functional Execution Context для ее обработки. Очень похоже на утверждение 1 console.log(1)
, оно выполняется и в результате выходит из основного стека выполнения.
По истечении заданного API setTimeout времени, которое в нашем случае составляет 1000 мс, браузер отправляет эту задачу (нашу функцию обратного вызова) в очередь обратных вызовов.
В этот момент Execution Context пуст, а наша функция обратного вызова находится в очереди обратного вызова, ожидая возможности выполнения. Здесь возникает очень важный вопрос: что дальше, и что связывает очередь обратных вызовов и стек вызовов? Вы правильно догадались, цикл событий.
Цикл событий
Цикл событий — это специальный механизм в JavaScript, который следит за стеком вызовов и очередью обратных вызовов. Его основное назначение — переносить функции обратного вызова из очереди обратных вызовов в стек вызовов для выполнения, как только он опустеет.
Очередь обратных вызовов работает по принципу FIFO (первым пришел, первым ушел). Это означает, что самый старый обратный вызов будет отправлен в стек вызовов первым. Вы можете представить очередь обратных вызовов просто как массив, который добавляет новые обратные вызовы с помощью метода Array.push()
в конец очереди и убирает первый обратный вызов с помощью метода Array.shift()
.
Цикл событий забирает обратные вызовы из очереди обратных вызовов один за другим и продолжает делать это до тех пор, пока очередь обратных вызовов не опустеет.
Как только стек вызовов опустел, цикл событий ищет в очереди обратных вызовов любую ожидающую функцию обратного вызова. Но не нашел ни одной. Вероятно, вы знаете причину. Остальная часть нашей программы будет выполнена за несколько миллисекунд, но браузер отправит нашу функцию обратного вызова (задачу) в очередь обратных вызовов через определенное время.
И что дальше? Провалилась ли здесь наша программа? Конечно же, нет. Как ясно из названия, цикл событий является циклом и продолжает проверку снова и снова.
Через 1000 мс наш обратный вызов (задача) отправляется браузером в очередь обратных вызовов. В этот момент времени стек вызовов уже пуст, и сразу же цикл событий передает нашу задачу в стек вызовов, где она выполняется аналогично любой другой программе.
Надеюсь, большинство понятий уже прояснилось в вашем сознании. Если у вас все еще остались некоторые неясности, давайте рассмотрим еще пару примеров.
Эта программа точно такая же, как и предыдущая, только с одним изменением: для параметра setTimeout время задержки установлено на 0. Можете ли вы угадать, что будет выведено этим кодом?
Если вы угадали 1 2 3, то это не правильный ответ. Позвольте мне объяснить, как?
Первый оператор console.log(1)
будет выполнен точно так, как мы уже обсуждали.
Загвоздка происходит во втором операторе setTimeout. Мы установили время задержки 0 мс и ожидаем, что оно будет выполнено немедленно. И тогда 3-е утверждение должно быть выполнено. Но это не тот случай.
Когда setTimeout встречается и добавляется в Callstack, и, будучи веб-API, он добавляет задачу в браузер и исчезает из контекста выполнения. Поскольку мы не добавили никакой задержки, браузер немедленно отправляет нашу задачу в очередь обратного вызова.
В этот самый момент JavaScript-движок выполняет 3-й оператор нашей программы console.log(3)
. Поскольку обратные вызовы переносятся в основной стек вызовов только после того, как он опустеет. Наш обратный вызов должен сидеть и ждать в очереди обратных вызовов, пока наш основной стек вызовов не опустеет.
Как только FEC из console.log(3)
завершает выполнение, стек вызовов пустеет, в дело вступает цикл событий, который проверяет очередь обратных вызовов на наличие отложенных запланированных задач и, угадайте, находит одну. Цикл событий передает наш обратный вызов в стек вызовов, и он начинает выполняться.
Таким образом, на выходе получается 1 3 2, а не 1 2 3. Причина в том, что eventloop передает обратные вызовы из очереди Callback в Callstack только после того, как она опустеет.
Задача для вас
Надеюсь, теперь вам ясны ваши понятия об очереди обратных вызовов, Web API и eventloop. Поэтому у меня есть небольшое задание для вас. Рассмотрите этот код и спрогнозируйте результат. Звучит хорошо?
Не торопитесь, делайте заметки и дайте мне знать вашу первую оценку в разделе комментариев.
Давайте сделаем это вместе.
Шаг1
console.log(1)
добавляется в основной стек вызовов и выполняется. Выскакивает из стека выполнения.
Шаг2
setTimeout no 1 добавляется в стек вызовов, добавляет задачу в браузер и выходит из стека.
На этом этапе контейнер браузера имеет 1 задачу(setTimeout 1), с таймером 1000 мс.
Шаг 3
setTimeout no 2 добавляется в стек вызовов следующим, добавляет задачу в браузер и выходит из стека.
На этом этапе контейнер браузера имеет 2 задачи, task1(setTimeout1) и task2(setTimeout2) с таймером 1000мс и 0мс соответственно.
Вот в чем загвоздка. Мы знаем, что task2 была добавлена после task1, но task2 вообще не имеет задержки 0мс. Поэтому задача2 будет отправлена в очередь обратного вызова раньше задачи1.
Давайте обновим наше состояние, на данный момент у нас осталась только 1 задача в контейнере браузера (task1), и у нас есть одна функция обратного вызова (task2) в очереди обратного вызова. Пока что был выполнен только 1 оператор console.log(1)
.
Шаг 4
Далее идет третий API setTimeout. Он добавляется в стек вызовов, добавляет задачу в браузер. Эта задача3 имеет задержку в 500 мс. Это означает, что она будет отправлена в очередь обратных вызовов через полсекунды.
На данный момент у нас есть 1 функция обратного вызова (task2), сидящая в очереди обратных вызовов, и 2 задачи, task1 и task3, ожидающие в контейнере браузера с задержкой 1000 мс и 500 мс соответственно.
Шаг5
Выражение 5 в основном скрипте выполняется немедленно и выводится из Callstack.
Теперь мы выполнили 2 утверждения, 2 задачи ожидают в контейнере браузера, а 1 задача сидит в очереди обратного вызова.
Шаг 6
Здесь находится четвертый и последний setTimeout. Его задержка установлена на 250 мс. Она добавляется в стек вызовов и вылетает после добавления очередной задачи в браузер. На данный момент у нас есть 3 задачи в контейнере браузера;
- Задача1: Добавлена командой setTimeout1 с таймером 1000 мс.
- Задача3: добавлена setTimeout1 с таймером 500 мс.
- Задача4: добавлена setTimeout1 с таймером 250 мс.
Как мы знаем, задача отправляется в очередь обратного вызова по истечении указанного времени. Поэтому задача 4 будет отправлена первой, за ней последует задача 3, а задача 1 будет добавлена в очередь обратного вызова последней.
Состояние очереди обратных вызовов
Поскольку на данный момент наш стек обратных вызовов пуст, Eventloop начнет передавать обратные вызовы в стек обратных вызовов по одному. Это будет происходить по принципу «первым пришел — первым ушел». Это означает, что самая старая функция обратного вызова будет отправлена в Callstack первой.
Итак, сначала в стек вызовов будет передана задача 2. Она будет выполнена с записью лога 3
в консоль.
Затем будет передана задача 4, в результате чего в консоль будет выведено 6
.
Затем задача 3 и, наконец, задача 1 выводят на консоль логи 4
и 2
соответственно.
Итак, мы получаем: 1 5 3 6 4 2. Это и есть волшебный ключ. Надеюсь, у вас получилось то же самое.
Кредиты и мотивация
- Элегантное руководство по JavaScript Runtime Environment от Gemma Croad.
Подведение итогов
Итак, это была вторая часть «JavaScript под капотом». Надеюсь, вы получили определенную пользу от этого руководства, а если получили, пожалуйста, дайте мне знать в разделе комментариев. Это даст мне еще один уровень мотивации, чтобы я продолжал писать подобные статьи.
До тех пор, оставайтесь в безопасности и старайтесь обезопасить других.
До скорой встречи💓