Что такое call stack

Что такое call stack

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Apr 14, 2018 · 5 min read

Стек вызова функций (Call stack in programming languages)

Как рассматривать стек вызова функции (Call Stack) в контексте языков программирования. Хочу сказать, что стек вызова не совсем то, что демонстрирует нам большинство материалов. Чтобы окончательно понять что такое стек вызова, необходимо вернуться к азам и рассмотреть как работает стек вызова в операционной системе, почему именно так? Потому что нельзя рассматривать стек вызова функций оторванно от физической памяти и возможностью управления ею и тем самым не надо забывать, что стек вызова функций это не «виртуальная» операция, а конкретное место в памяти выделяемое операционной системой для выполнение программного кода функции, которая была запущена внутренним или внешним API (будь-то браузера, IDE или движка какого либо языка).

Хочу определить что такое стека вызова функции в низкоуровневых (low-level programming languages) языках (Assembler, C++):

(В современных низкоуровневых языках есть современные библиотеки позволяющие поддерживать работу ООП и контролировать стек функции не на физическом уровне, а использованием вывода консоли: Poppy, Pantheios)

Шаг 1: запуск лейаута кода.

В лейауте кроме самих инструкций находится дополнительная информация (переменные, декларативные данные, дополнительные технические данные и …)

Получив первую инструкцию о запуске ОС начинает строить стек. Сначала выделяет физическое место на кластерах памяти и заполняет его данными, но какими:

Инструкция получена, создается сам стек в котором основная часть — выделенное место для выполнения операций, а сегмент стека для хранения данных для возврата результата выполнения функции.

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

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

Инструкция возврата вызова, это не место вызова этой функции, а следующая за ней инструкция.

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

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

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Далее необходимо понять, как высокоуровневые языки реализовывают работу стека вызова. Давайте рассмотрим два случая: языки использующие многопоточность и однопоточные языки. Почему именно такие варианты? Потому что именно многопоточность (multithreading) и однопоточность определяет взаимодействие виртуальной и физической памяти ресурса.

Языки использующие многопоточность рассмотрим на примере Java.

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

В движок java имплементирована специальная коллекция Stack, у которой есть два метода push(добавит данные) и pop(удалить данные). Виртуальная машина Javaведет запись всех вызванных функций и с каждым вызовом функции создает StackTraceElement. После завершения выполнения функции StackTraceElement уничтожается, поэтому информация о стеке вызовов всегда актуальная. С Помощью метода getMethodName можно всегда получить информацию о методе верхнего элемента StackTraceElement, а значит о методе вызова функции.

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Javascript: Javascript однопоточный скриптовый язык использующий очередь функций обратного вызова(callback functions queue) с методом FIFO (первый пришел — первый уйдешь).

Два варианта взаимодействия физической и виртуальной памяти в Javascript:

Сам стек как и базовые использует LIFO, а очередь колбэков — FIFO. Так же в V8 имплементированы методы push и pop, декларация идентична методам, реализованным в Java.

Методы Javascript использующие таймеры использующие API OS и таймеры которые работают на API браузера. Таймеры всегда передвигают выполнение функции-колбэка в конец стека вызовов. Что это значит? Это значит, что в браузере выполняя методы setTimeout, setInterva сначала запускается таймер, а лишь потом функция-колбэк попадает в очередь ожидания. Если в момент попадания колбэка стек переполнен, колбэку приходится ждать своей очереди. Таймеры запущенные вне браузера реализованы на основании либо внутренних методов V8 либо при обращении к API окружения (API OS).

Особенностью языка Javascript является технология ajax или иначе возможность отправки асинхронных запросов (XMLHTTPRequest) и выполнения каких либо операций без задержки выполнения основного кода. XMLHTTPRequest может быть отправлен синхронно, но при этом весь остальной код будет дожидаться либо результата отправки запроса, что порой нецелесообразно. Технология ajax реализована исключительно в браузерах, что делает выделяет Javascript из группы языков использующих async. Асинхронные запросы в JS используют цикл событий с очередью колбэков (event loop with callback queue). В остальных языках программирования тоже есть возможность запуска асинхронного кода (Python (asyncio), C#(async/await)), что позволяет выполнять какие-то операции без задержки выполнения основных функций, принципом тут тоже выступает цикл событий, где функция колбэк помещается в очередь ожидания. Принцип работы очереди — FIFO.

Очень много в сети информации, поэтому кратко.

Хочу отметить, что Javascript является однопоточным, что означает, что текущий поток Javascript выполняется до завершения, выполняя все инструкции в последовательности до тех пор, пока он не достигнет конца кода. Затем и только тогда он вытаскивает следующий элемент из очереди событий.

Источник

Контекст выполнения и стек вызовов в JavaScript

Если вы — JavaScript-разработчик или хотите им стать, это значит, что вам нужно разбираться во внутренних механизмах выполнения JS-кода. В частности, понимание того, что такое контекст выполнения и стек вызовов, совершенно необходимо для освоения других концепций JavaScript, таких, как поднятие переменных, области видимости, замыкания. Материал, перевод которого мы сегодня публикуем, посвящён контексту выполнения и стеку вызовов в JavaScript.

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Контекст выполнения

Контекст выполнения (execution context) — это, если говорить упрощённо, концепция, описывающая окружение, в котором производится выполнение кода на JavaScript. Код всегда выполняется внутри некоего контекста.

▍Типы контекстов выполнения

В JavaScript существует три типа контекстов выполнения:

Стек выполнения

Стек выполнения (execution stack), который ещё называют стеком вызовов (call stack), это LIFO-стек, который используется для хранения контекстов выполнения, создаваемых в ходе работы кода.

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

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

Изучим эту идею с помощью следующего примера:

Вот как будет меняться стек вызовов при выполнении этого кода.

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Состояние стека вызовов

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

Когда функция first() завершает работу, её контекст извлекается из стека и управление передаётся глобальному контексту. После того, как весь код оказывается выполненным, движок извлекает глобальный контекст выполнения из текущего стека.

О создании контекстов и о выполнении кода

До сих пор мы говорили о том, как JS-движок управляет контекстами выполнения. Теперь поговорим о том, как контексты выполнения создаются, и о том, что с ними происходит после создания. В частности, речь идёт о стадии создания контекста выполнения и о стадии выполнения кода.

▍Стадия создания контекста выполнения

Перед выполнением JavaScript-кода создаётся контекст выполнения. В процессе его создания выполняются три действия:

Привязка this

В глобальном контексте выполнения this содержит ссылку на глобальный объект (как уже было сказано, в браузере это объект window ).

В контексте выполнения функции значение this зависит от того, как именно была вызвана функция. Если она вызвана в виде метода объекта, тогда значение this привязано к этому объекту. В других случаях this привязывается к глобальному объекту или устанавливается в undefined (в строгом режиме). Рассмотрим пример:

Лексическое окружение

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

В лексическом окружении имеется два компонента:

Лексическое окружение можно представить в виде следующего псевдокода:

Окружение переменных

Окружение переменных (Variable Environment) — это тоже лексическое окружение, запись окружения которого хранит привязки, созданные посредством команд объявления переменных ( VariableStatement ) в текущем контексте выполнения.

Так как окружение переменных также является лексическим окружением, оно обладает всеми вышеописанными свойствами лексического окружения.

Рассмотрим примеры, иллюстрирующие то, что мы только что обсудили:

Схематичное представление контекста выполнения для этого кода будет выглядеть так:

Только что мы только что описали, называется «поднятием переменных» (Hoisting). Объявления переменных «поднимаются» в верхнюю часть их лексической области видимости до выполнения операций присвоения им каких-либо значений.

▍Стадия выполнения кода

Это, пожалуй, самая простая часть данного материала. На этой стадии выполняется присвоение значений переменным и осуществляется выполнение кода.

Итоги

Только что мы обсудили внутренние механизмы выполнения JavaScript-кода. Хотя для того, чтобы быть очень хорошим JS-разработчиком, знать всё это и не обязательно, если у вас имеется некоторое понимание вышеописанных концепций, это поможет вам лучше и глубже разобраться с другими механизмами языка, с такими, как поднятие переменных, области видимости, замыкания.

Уважаемые читатели! Как вы думаете, о чём ещё, помимо контекста выполнения и стека вызовов, полезно знать JavaScript-разработчикам?

Источник

Что такое call stack?

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Когда-то был оператор goto, и программы выглядели так

Мы что-то проверяем, если проверка успешна, вызываем какое-то действие. Затем возвращаемся назад.
Но такой вариант оказался неудобным, если это какое-то действие нужно вызывать из разных мест, а потом возвращаться именно в эти разные места. Поэтому сейчас используется не goto, а call (вызов), который кладет в стек адрес текущего места выполнения и переходит в подпрограмму. В конце подпрограммы по команде return, он берет из стека адрес и по нему возвращается назад.
Так как в стек можно положить что-то еще, то можно внутри вызванной подпрограммы вызвать другую подпрограмму, и рекурсивно вызывать столько раз сколько нужно. Потом все call-ы будут красиво закрыты return-ами в обратном порядке.

В данном варианте у нас работает так:
1. из основной части main, вызывается program1 (в стек кладется адрес этой)
2. из вызванного program1 вызывается program3 (в стек добавляется адрес этой команды, там уже две)
3. из program3 мы возвращаемся, беря последнее значение из стека (возвращаемся в program1)
4. снова возвращаемся, беря адрес из стека и попадаем в main
5. тоже самое с вызовом program2-program3-program2-main

Стек обычно растет сверху вниз, каждая команда return берет самый последний нижний адрес и возвращается по нему, что позволяет создавать множество вложенных вызовов, и рекурсивно с ними работать.
Но не нужно забывать, что стек не бесконечен. десять или сто вызовов вообще ни о чем на современных компах, но миллион или миллиард, умножить на размер адреса (например 4 байта), может занять мегабайты и гигабайты.

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

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

Call stack trace of exception:

Английский не очень хорошо знаю,

Источник

Контекст выполнения и стек вызовов в JavaScript

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Если вы — JavaScript-разработчик или хотите им стать, это значит, что вам нужно разбираться во внутренних механизмах выполнения JS-кода. В частности, понимание того, что такое контекст выполнения и стек вызовов, совершенно необходимо для освоения других концепций JavaScript, таких, как поднятие переменных, области видимости, замыкания. Материал посвящён контексту выполнения и стеку вызовов в JavaScript.

Контекст выполнения

Контекст выполнения (execution context) — это, если говорить упрощённо, концепция, описывающая окружение, в котором производится выполнение кода на JavaScript. Код всегда выполняется внутри некоего контекста.

Типы контекстов выполнения

В JavaScript существует три типа контекстов выполнения:

Стек выполнения

Стек выполнения (execution stack), который ещё называют стеком вызовов (call stack), это LIFO-стек, который используется для хранения контекстов выполнения, создаваемых в ходе работы кода.

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

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

Изучим эту идею с помощью следующего примера:

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Вот как будет меняться стек вызовов при выполнении этого кода.

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

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

Когда функция first() завершает работу, её контекст извлекается из стека и управление передаётся глобальному контексту. После того, как весь код оказывается выполненным, движок извлекает глобальный контекст выполнения из текущего стека.

О создании контекстов и о выполнении кода

До сих пор мы говорили о том, как JS-движок управляет контекстами выполнения. Теперь поговорим о том, как контексты выполнения создаются, и о том, что с ними происходит после создания. В частности, речь идёт о стадии создания контекста выполнения и о стадии выполнения кода.

Стадия создания контекста выполнения

Перед выполнением JavaScript-кода создаётся контекст выполнения. В процессе его создания выполняются три действия:

Концептуально контекст выполнения можно представить так:

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Привязка this

В глобальном контексте выполнения this содержит ссылку на глобальный объект (как уже было сказано, в браузере это объект window ).

В контексте выполнения функции значение this зависит от того, как именно была вызвана функция. Если она вызвана в виде метода объекта, тогда значение this привязано к этому объекту. В других случаях this привязывается к глобальному объекту или устанавливается в undefined (в строгом режиме). Рассмотрим пример:

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Лексическое окружение

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

В лексическом окружении имеется два компонента:

Существует два типа лексических окружений:

Существует два типа записей окружения:

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

Лексическое окружение можно представить в виде следующего псевдокода:

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Окружение переменных

Окружение переменных (Variable Environment) — это тоже лексическое окружение, запись окружения которого хранит привязки, созданные посредством команд объявления переменных ( VariableStatement ) в текущем контексте выполнения.

Так как окружение переменных также является лексическим окружением, оно обладает всеми вышеописанными свойствами лексического окружения.

Рассмотрим примеры, иллюстрирующие то, что мы только что обсудили:

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Схематичное представление контекста выполнения для этого кода будет выглядеть так:

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

То что мы только что описали, называется «поднятием переменных» (Hoisting). Объявления переменных «поднимаются» в верхнюю часть их лексической области видимости до выполнения операций присвоения им каких-либо значений.

Стадия выполнения кода

Это, пожалуй, самая простая часть данного материала. На этой стадии выполняется присвоение значений переменным и осуществляется выполнение кода.

Итоги

Только что мы обсудили внутренние механизмы выполнения JavaScript-кода. Хотя для того, чтобы быть очень хорошим JS-разработчиком, знать всё это и не обязательно, если у вас имеется некоторое понимание вышеописанных концепций, это поможет вам лучше и глубже разобраться с другими механизмами языка, с такими, как поднятие переменных, области видимости, замыкания.

Источник

JavaScript-движки: как они работают? От стека вызовов до промисов — (почти) всё, что вам нужно знать

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

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

Начнём наше путешествие в язык с экскурсии в удивительный мир JavaScript-движков.

Откройте консоль в Chrome и перейдите на вкладку Sources. Вы увидите несколько разделов, и один из самых интересных называется Call Stack (в Firefox вы увидите Call Stack, когда поставите брейкпоинт в коде):

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Что такое Call Stack? Похоже, тут много чего происходит, даже ради исполнения пары строк кода. На самом деле JavaScript не поставляется в коробке с каждым браузером. Существует большой компонент, который компилирует и интерпретирует наш JavaScript-код — это JavaScript-движок. Самыми популярными являются V8, он используется в Google Chrome и Node.js, SpiderMonkey в Firefox, JavaScriptCore в Safari/WebKit.

Сегодня JavaScript-движки представляют собой прекрасные образцы программной инженерии, и будет практически невозможно рассказать обо всех аспектах. Однако основную работу по исполнению кода делают для нас лишь несколько компонентов движков: Call Stack (стек вызовов), Global Memory (глобальная память) и Execution Context (контекст исполнения). Готовы с ними познакомиться?

1. JavaScript-движки и глобальная память

Я говорил, что JavaScript является одновременно компилируемым и интерпретируемым языком. Хотите верьте, хотите нет, но на самом деле JavaScript-движки компилируют ваш код за микросекунды до его исполнения.

Волшебство какое-то, да? Это волшебство называется JIT (Just in time compilation). Она сама по себе является большой темой для обсуждения, даже книги будет мало, чтобы описать работу JIT. Но пока что мы пропустим теорию и сосредоточимся на фазе исполнения, которая не менее интересна.

Для начала посмотрите на этот код:

Допустим, я спрошу вас, как этот код обрабатывается в браузере? Что вы ответите? Вы можете сказать: «браузер читает код» или «браузер исполняет код». В реальности всё не так просто. Во-первых, код считывает не браузер, а движок. JavaScript-движок считывает код, и как только он определяет первую строку, то кладёт пару ссылок в глобальную память.

Глобальная память (которую также называют кучей (heap)) — это область, в которой JavaScript-движок хранит переменные и объявления функций. И когда он прочитает приведённый выше код, то в глобальной памяти появятся два биндинга:

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

В данный момент ничего не исполняется. Давайте теперь попробуем исполнить нашу функцию:

Что произойдёт? А произойдёт кое-что интересное. При вызове функции JavaScript-движок выделит два раздела:

2. JavaScript-движки: как они работают? Глобальный контекст исполнения и стек вызовов

Вы узнали, как JavaScript-движок читает переменные и объявления функций. Они попадают в глобальную память (кучу).

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

Это стековая структура данных: элементы могут добавляться в неё сверху, но они не могут исключаться из структуры, пока над ними есть другие элементы. Именно так устроены JavaScript-функции. При исполнении они не могут покинуть стек вызовов, если в нём присутствует другая функция. Обратите на это внимание, поскольку эта концепция помогает понять утверждение «JavaScript является однопоточным».

Но вернёмся к нашему примеру. При вызове функции движок отправляет её в стек вызовов:

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

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

В то же самое время движок размещает в памяти глобальный контекст исполнения, это глобальная среда, в которой исполняется JavaScript-код. Вот как это выглядит:

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

Представьте глобальный контекст исполнения в виде моря, в котором глобальные JavaScript-функции плавают, словно рыбы. Как мило! Но это лишь половина всей истории. Что, если наша функция имеет вложенные переменные или внутренние функции?

Даже в простом случае, как показано ниже, JavaScript-движок создаёт локальный контекст исполнения:

Рядом с pow появится локальный контекст исполнения, внутри зелёного раздела-прямоугольника, расположенного внутри глобального контекста исполнения. Представьте также, как для каждой вложенной функции внутри вложенной функции движок создаёт другие локальные контексты исполнения. Все эти разделы-прямоугольники появляются очень быстро! Как матрёшка!

Давайте теперь вернёмся к истории с однопоточностью. Что это означает?

3. JavaScript является однопоточным, и другие забавные истории

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

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

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

В то же время вы знаете, что когда браузер загружает какой-то JavaScript-код, движок считывает этот код строка за строкой и выполняет следующие шаги:

4. Асинхронный JavaScript, очередь обратных вызовов и цикл событий

Благодаря глобальной памяти, контексту исполнения и стеку вызовов синхронный JavaScript-код исполняется в наших браузерах. Но мы кое о чём забыли. Что происходит, если нужно исполнить какую-нибудь асинхронную функцию?

Под асинхронной функцией я подразумеваю каждое взаимодействие с внешним миром, для завершения которого может потребоваться какое-то время. Вызов REST API или таймера — асинхронны, потому что на их выполнение могут уйти секунды. Благодаря имеющимся в движке элементам мы можем обрабатывать такие функции без блокирования стека вызовов и браузера. Не забывайте, стек вызовов может исполнять одновременно только одну функцию, и даже одна блокирующая функция может буквально остановить браузер. К счастью, JavaScript-движки «умны», и с небольшой помощью браузера могут такие вещи отсортировывать.

Когда мы исполняем асинхронную функцию, браузер берёт её и выполняет для нас. Возьмём такой таймер:

Через 10 секунд браузер берёт callback-функцию, которую мы ему передали, и кладёт её в очередь обратных вызовов. В данный момент в JavaScript-движке появилось ещё два раздела-прямоугольника. Посмотрите на этот код:

Теперь наша схема выглядит так:

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

setTimeout исполняется внутри контекста браузера. Через 10 секунд таймер запускается и callback-функция готова к исполнению. Но для начала она должна пройти через очередь обратных вызовов. Это структура данных в виде очереди, и, как свидетельствует её название, представляет собой упорядоченную очередь из функций.

Каждая асинхронная функция должна пройти через очередь обратных вызовов, прежде чем попасть в стек вызовов. Но кто отправляет функции дальше? Это делает компонент под названием цикл событий.

Пока что цикл событий занимается только одним: проверяет, пуст ли стек вызовов. Если в очереди обратных вызовов есть какая-нибудь функция и если стек вызовов свободен, тогда пора отправлять callback в стек вызовов.

После этого функция считается исполненной. Так выглядит общая схема обработки асинхронного и синхронного кода JavaScript-движком:

Что такое call stack. Смотреть фото Что такое call stack. Смотреть картинку Что такое call stack. Картинка про Что такое call stack. Фото Что такое call stack

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

И если интересно, можете посмотреть любопытное видео «What the heck is the event loop anyway» Филипа Робертса. Это одно из лучших объяснений цикла событий.

Но мы ещё не закончили с темой асинхронного JavaScript. В следующих главах мы рассмотрим ES6-промисы.

5. Callback hell и ES6-промисы

Callback-функции используются в JavaScript везде, и в синхронном, и в асинхронном коде. Рассмотрим этот метод:

Термин Callback hell в JavaScript применяют к «стилю» программирования, при котором callback’и вкладывают в другие callback’и, которые вложены в другие callback’и… Из-за асинхронной природы JavaScript-программисты уже давно попадают в эту ловушку.

Если честно, я никогда не создавал большие пирамиды callback’ов. Возможно, потому что я ценю читабельный код и всегда стараюсь придерживаться его принципов. Если вы попали в callback hell, это говорит о том, что ваша функция делает слишком много.

Я не буду подробно говорить о callback hell, если вам интересно, то сходите на сайт callbackhell.com, там эта проблема подробно исследована и предложены разные решения. А мы поговорим о ES6-промисах. Это аддон к JavaScript, призванное решить проблему ада обратных вызовов. Но что такое «промисы»?

Промис в JavaScript — это представление будущего события. Промис может завершиться успешно, или на жаргоне программистов промис будет «разрешён» (resolved, исполнен). Но если промис завершается с ошибкой, то мы говорим, что он в состоянии «отклонён» (rejected). Также у промисов есть состояние по умолчанию: каждый новый промис начинается в состоянии «ожидания решения» (pending). Можно ли создать собственный промис? Да. Об этом мы поговорим в следующей главе.

6. Создание и работа с JavaScript-промисами

Как видите, resolve — это функция, которую мы вызываем, чтобы промис успешно завершился. А reject создаст отклонённый промис:

Сейчас промисы не выглядят такими полезными, верно? Эти примеры ничего не выводят для пользователя. Давайте кое-что добавим. И разрешённые, от отклонённые промисы могут возвращать данные. Например:

Как JavaScript-разработчик и потребитель чужого кода вы по большей части взаимодействуете с внешними промисами. Создатели библиотек чаще всего обёртывают legacy-код в конструктор промисов, таким образом:

И при необходимости мы также можем создать и разрешить промис, вызвав Promise.resolve() :

7. Обработка ошибок в ES6-промисах

Обрабатывать ошибки в JavaScript всегда было просто, как минимум в синхронном коде. Взгляните на пример:

К счастью, с промисами мы можем обрабатывать асинхронные ошибки, словно они синхронные. В прошлой главе я говорил, что вызов reject приводит к отклонению промиса:

Кроме того, чтобы для создания и отклонения промиса в нужном месте можно вызывать Promise.reject() :

8. Комбинаторы ES6-промисов: Promise.all, Promise.allSettled, Promise.any и другие

Промисы не предназначены для работы по одиночке. Promise API предлагает ряд методов для комбинирования промисов. Один из самых полезных — Promise.all, он берёт массив из промисов и возвращает один промис. Только проблема в том, что Promise.all отклоняется, если отклонен хотя бы один промис в массиве.

Promise.race разрешает или отклоняет, как только один из промисов в массиве получает соответствующий статус.

9. ES6-промисы и очередь микрозадач

Если помните из предыдущей главы, каждая асинхронная callback-функция в JavaScript оказывается в очереди обратных вызовов, прежде чем попадает в стек вызовов. Но у callback-функций, переданных в промис, иная судьба: они обрабатываются очередью микрозадач (Microtask Queue), а не очередью задач.

И здесь вам нужно быть внимательными: очередь микрозадач предшествует очереди вызовов. Обратные вызовы из очереди микрозадач имеют приоритет, когда цикл событий проверяет, готовы ли новые callback’и перейти в стек вызовов.

Подробнее эта механика описана Джейком Арчибальдом в Tasks, microtasks, queues and schedules, замечательное чтиво.

10. JavaScript-движки: как они работают? Асинхронная эволюция: от промисов до async/await

async/await — всего лишь стилистическое улучшение, которое мы называем синтаксическим сахаром. async/await никак не меняет JavaScript (не забывайте, язык должен быть обратно совместим со старыми браузерами и не должен ломать существующий код). Это лишь новый способ написания асинхронного кода на основе промисов. Рассмотрим пример. Выше мы уже сохранили промис в соответствующем then :

Выглядит здраво, верно? Забавно, что async-функция всегда возвращает промис, и никто не может ей в этом помешать:

Давайте снова взглянем на промис, в котором мы обрабатываем ошибки с помощью обработчика catch :

С асинхронными функциями мы можем отрефакторить вот так:

Однако ещё не все перешли на этот стиль. try/catch может усложнить ваш код. При этом нужно учитывать ещё кое-что. Посмотрите, как в этом коде возникает ошибка внутри блока try :

Помимо этого async/await выглядит лучшим способом структурирования асинхронного кода в JavaScript. Мы лучше управляем обработкой ошибок и код выглядит чище.

11. JavaScript-движки: как они работают? Итоги

JavaScript — это скриптовый язык для веба, он сначала компилируется, а затем интерпретируется движком. Самые популярные JS-движки: V8, применяется в Google Chrome и Node.js; SpiderMonkey, разработан для Firefox; JavaScriptCore, используется в Safari.

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

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

Для упрощения работы асинхронного кода в ECMAScript 2015 были внедрены промисы. Промис — это асинхронный объект, используемый для представления успешности или неуспешности любой асинхронной операции. Но улучшения на этом не прекратились. В 2017-м появились async/await : стилистическое улучшение для промисов, позволяющее писать асинхронный код, как если бы он был синхронным.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *