Как посмотреть стек вызовов в браузере
Продолжаем отладку приложения со списком дел. В этот раз будем чинить удаление задач. Откройте приложение в новой вкладке. Эта версия тоже сломана нарочно, чтобы мы разобрались, что такое стек вызовов.
Как откроете приложение, добавьте несколько своих задач в список дел. Затем удалите первую в списке задачу. Кнопка Delete. Пока всё работает. Теперь удалите последнюю в списке задачу. Опачки! Почему-то удалилась первая. Если не заметили, попробуйте снова: удалите любую задачу, кроме первой. То же самое! Какую бы задачу мы не удаляли, пропадает первая в списке.
Вы знаете, что делать: открываем инструменты разработчика, затем отладчик. Нас интересует функция removeSingle , файл app.js , 39-я строка. Именно эта функция отвечает за удаление задач из списка.
Поставим точку останова на 40-й строке. Попробуйте теперь удалить любую задачу из списка, кроме первой. Сработает точка останова, и мы сможем построчно выполнять код, чтобы найти ошибку.
В прошлой статье мы использовали кнопку навигации «Перешагнуть через», чтобы найти ошибку. Мы можем воспользоваться ей снова, но особенность этой кнопки в том, что код выполняется построчно целиком. Даже если это вызов функции. А что, если ошибка «прячется» в этой функции?
Что, если проблема не в removeSingle , а в spliceItem ? В этом случае выручит кнопка «Зайти в»:
Кнопка «Зайти в» на панели навигации
Кнопка «Зайти в» позволяет «провалиться» в функцию вместо её вызова целиком и отладить её код тоже.
После остановки на 40-й строке нажимайте «Зайти в» до тех пор, пока не окажетесь на 24-й строке, внутри функции createList . Если вы не поймёте, как там оказались, ничего страшного. Разберёмся!
Функция createList принимает массив list с данными для отрисовки списка задач. Давайте посмотрим на его элементы. Наведите курсор на параметр list или загляните в блок «Области видимости».
В массиве с данными вы найдёте задачу, которую удалили. Значит, в функцию createList передана неверная информация. Но в самой функции ошибки нет, она где-то раньше.
Обратимся к панели инструментов отладчика, та что справа. В блоке «Стек вызовов» можно найти список всех вызванных функций до момента остановки. Кроме названия функции, там указаны номер строки и имя файла, где функция была вызвана. Количество функций говорит о размере стека. Выделение показывает, как глубоко мы в стеке.
В нашем случае стек глубиной в четыре вызова. Точка останова в функции removeSingle . Затем мы «провалились» в spliceItem , после в saveList и вот мы в createList :
Блок «Стек вызовов» на панели инструментов, справа, развёрнут
По стеку можно перемещаться. Для этого достаточно кликнуть по нужной функции в списке.
Чтобы найти ошибку, прогуляемся по стеку. Кликнем по функции saveList . Отладчик перенесёт нас к её объявлению. Это простая, возможно даже лишняя, функция. Она всего лишь вызывает createList и передаёт ей аргументами глобальные переменные items и todoList . Вряд ли ошибка в такой простой функции, поэтому двигаемся дальше.
Кликнем в стеке по функции spliceItem . Так-так-так. Значение параметра index не определено. Это уже зацепка!
Значение index (подчёркнуто жёлтым) можно увидеть при наведении или в блоке «Области видимости»
Подымемся выше, в функцию removeSingle , чтобы понять, почему в spliceItem передаётся undefined . Ошибка где-то здесь:
Наведём курсор на index , увидим undefined . Если наведём на number — то же самое. Тогда давайте посмотрим значение dataset . Вот оно! В объекте dataset нет ключа number . Индекс записан по ключу index , что логично.
Но исправления уже за рамками нашей статьи. Дело раскрыто.
Совет в тему
В нашем приложении всего десяток функций. Каждая длинной в пару строк. Найти нужную несложно. Но что, если в файле сотня функций? Также искать глазами? Нет! Воспользуйтесь поиском по функциям.
Для поиска нажмите shift + control + O в Windows и Linux или shift + command + O в macOS. Далее впишите имя функции, которую хотите найти. Или воспользуйтесь стрелками на клавиатуре для навигации по списку всех функций в приложении.
Код уязвим для ошибок. И вы, скорее всего, будете делать ошибки в коде… Впрочем, давайте будем откровенны: вы точно будете совершать ошибки в коде. В конце концов, вы человек, а не робот.
Но по умолчанию в браузере ошибки не видны. То есть, если что-то пойдёт не так, мы не увидим, что именно сломалось, и не сможем это починить.
Для решения задач такого рода в браузер встроены так называемые «Инструменты разработки» (Developer tools или сокращённо — devtools).
Chrome и Firefox снискали любовь подавляющего большинства программистов во многом благодаря своим отменным инструментам разработчика. Остальные браузеры, хотя и оснащены подобными инструментами, но всё же зачастую находятся в роли догоняющих и по качеству, и по количеству свойств и особенностей. В общем, почти у всех программистов есть свой «любимый» браузер. Другие используются только для отлова и исправления специфичных «браузерозависимых» ошибок.
Для начала знакомства с этими мощными инструментами давайте выясним, как их открывать, смотреть ошибки и запускать команды JavaScript.
Google Chrome
В её JavaScript-коде закралась ошибка. Она не видна обычному посетителю, поэтому давайте найдём её при помощи инструментов разработки.
Нажмите F12 или, если вы используете Mac, Cmd + Opt + J .
По умолчанию в инструментах разработчика откроется вкладка Console (консоль).
Она выглядит приблизительно следующим образом:
Точный внешний вид инструментов разработки зависит от используемой версии Chrome. Время от времени некоторые детали изменяются, но в целом внешний вид остаётся примерно похожим на предыдущие версии.
Обычно при нажатии Enter введённая строка кода сразу выполняется.
Чтобы перенести строку, нажмите Shift + Enter . Так можно вводить более длинный JS-код.
Теперь мы явно видим ошибки, для начала этого вполне достаточно. Мы ещё вернёмся к инструментам разработчика позже и более подробно рассмотрим отладку кода в главе Отладка в браузере.
Firefox, Edge и другие
Инструменты разработчика в большинстве браузеров открываются при нажатии на F12 .
Их внешний вид и принципы работы мало чем отличаются. Разобравшись с инструментами в одном браузере, вы без труда сможете работать с ними и в другом.
Safari
Safari (браузер для Mac, не поддерживается в системах Windows/Linux) всё же имеет небольшое отличие. Для начала работы нам нужно включить «Меню разработки» («Developer menu»).
Откройте Настройки (Preferences) и перейдите к панели «Продвинутые» (Advanced). В самом низу вы найдёте чекбокс:
Теперь консоль можно активировать нажатием клавиш Cmd + Opt + C . Также обратите внимание на новый элемент меню «Разработка» («Develop»). В нем содержится большое количество команд и настроек.
Итого
- Инструменты разработчика позволяют нам смотреть ошибки, выполнять команды, проверять значение переменных и ещё много всего полезного.
- В большинстве браузеров, работающих под Windows, инструменты разработчика можно открыть, нажав F12 . В Chrome для Mac используйте комбинацию Cmd + Opt + J , Safari: Cmd + Opt + C (необходимо предварительное включение «Меню разработчика»).
Теперь наше окружение полностью настроено. В следующем разделе мы перейдём непосредственно к JavaScript.
Давайте отвлечёмся от написания кода и поговорим о его отладке.
Отладка – это процесс поиска и исправления ошибок в скрипте. Все современные браузеры и большинство других сред разработки поддерживают инструменты для отладки – специальный графический интерфейс, который сильно упрощает отладку. Он также позволяет по шагам отследить, что именно происходит в нашем коде.
Мы будем использовать браузер Chrome, так как у него достаточно возможностей, в большинстве других браузеров процесс будет схожим.
Панель «Исходный код» («Sources»)
Версия Chrome, установленная у вас, может выглядеть немного иначе, однако принципиальных отличий не будет.
- Работая в Chrome, откройте тестовую страницу.
- Включите инструменты разработчика, нажав F12 (Mac: Cmd + Opt + I ).
- Щёлкните по панели Sources («исходный код»).
При первом запуске получаем следующее:
Кнопка-переключатель откроет вкладку со списком файлов.
Кликните на неё и выберите hello.js в дереве файлов. Вот что появится:
Интерфейс состоит из трёх зон:
- В зоне File Navigator (панель для навигации файлов) показаны файлы HTML, JavaScript, CSS, включая изображения, используемые на странице. Здесь также могут быть файлы различных расширений Chrome.
- Зона Code Editor (редактор кода) показывает исходный код.
- Наконец, зона JavaScript Debugging (панель отладки JavaScript) отведена для отладки, скоро мы к ней вернёмся.
Чтобы скрыть список ресурсов и освободить экранное место для исходного кода, щёлкните по тому же переключателю .
Консоль
При нажатии на клавишу Esc в нижней части экрана вызывается консоль, где можно вводить команды и выполнять их клавишей Enter .
Результат выполнения инструкций сразу же отображается в консоли.
Например, результатом 1+2 будет 3 , а вызов функции hello("debugger") ничего не возвращает, так что результатом будет undefined :
Точки останова (breakpoints)
Давайте разберёмся, как работает код нашей тестовой страницы. В файле hello.js щёлкните на номере строки 4 . Да-да, щёлкайте именно по самой цифре, не по коду.
Ура! Вы поставили точку останова. А теперь щёлкните по цифре 8 на восьмой линии.
Вот что в итоге должно получиться (синим это те места, по которым вы должны щёлкнуть):
Точка останова – это участок кода, где отладчик автоматически приостановит исполнение JavaScript.
Пока исполнение поставлено «на паузу», мы можем просмотреть текущие значения переменных, выполнить команды в консоли, другими словами, выполнить отладку кода.
В правой части графического интерфейса мы видим список точек останова. А когда таких точек выставлено много, да ещё и в разных файлах, этот список поможет эффективно ими управлять:
- Быстро перейдите к точке останова в коде (нажав на неё на правой панели).
- Временно отключите точку останова, сняв с неё галочку.
- Удалите точку останова, щёлкнув правой кнопкой мыши и выбрав Remove (Удалить).
- …и так далее.
Щелчок правой кнопкой мыши по номеру строки позволяет создать условную точку останова. Она сработает только в тот момент, когда выражение, которое вы должны указать при создании такой точки, истинно.
Это удобно, когда нам нужно остановиться только для при определённом значении переменной или для определённых параметров функции.
Команда debugger
Выполнение кода можно также приостановить с помощью команды debugger прямо изнутри самого кода:
Такая команда сработает только если открыты инструменты разработки, иначе браузер ее проигнорирует.
Остановимся и оглядимся
В нашем примере функция hello() вызывается во время загрузки страницы, поэтому для начала отладки (после того, как мы поставили точки останова) проще всего её перезагрузить. Нажмите F5 (Windows, Linux) или Cmd + R (Mac).
Выполнение прервётся на четвёртой строчке (где находится точка останова):
Чтобы понять, что происходит в коде, щёлкните по стрелочкам справа:
Watch – показывает текущие значения для любых выражений.
Вы можете нажать на + и ввести выражение. Отладчик покажет его значение, автоматически пересчитывая его в процессе выполнения.
Call Stack – показывает цепочку вложенных вызовов.
В текущий момент отладчик находится внутри вызова hello() , вызываемого скриптом в index.html (там нет функции, поэтому она называется “анонимной”).
Если вы нажмёте на элемент стека (например, «anonymous»), отладчик перейдёт к соответствующему коду, и нам представляется возможность его проанализировать.
Scope показывает текущие переменные.
Local показывает локальные переменные функций, а их значения подсвечены прямо в исходном коде.
В Global перечисляются глобальные переменные (то есть вне каких-либо функций).
Там также есть ключевое слово this , которое мы ещё не изучали, но скоро изучим.
Пошаговое выполнение скрипта
А теперь давайте пошагаем по нашему скрипту.
Для этого есть кнопки в верхней части правой панели. Давайте рассмотрим их.
– «Resume»: продолжить выполнение, быстрая клавиша F8 .
Возобновляет выполнение кода. Если больше нет точек останова, то выполнение просто продолжается, без контроля отладчиком.
Вот, что мы увидим, кликнув на неё:
Выполнение кода возобновилось, дошло до другой точки останова внутри say() , и отладчик снова приостановил выполнение. Обратите внимание на пункт «Call stack» справа: в списке появился ещё один вызов. Сейчас мы внутри say() .
– «Step»: выполнить следующую команду, быстрая клавиша F9 .
Выполняет следующую инструкцию. Если мы нажмём на неё сейчас, появится alert .
Нажатие на эту кнопку снова и снова приведёт к пошаговому выполнению всех инструкций скрипта одного за другим.
– «Step over»: выполнить следующую команду, но не заходя внутрь функции, быстрая клавиша F10 .
Работает аналогично предыдущей команде «Step», но ведёт себя по-другому, если следующая инструкция является вызовом функции (имеется ввиду: не встроенная, как alert , а объявленная нами функция).
Если сравнить, то команда «Step» переходит во вложенный вызов функцию и приостанавливает выполнение в первой строке, в то время как «Step over» выполняет вызов вложенной функции незаметно для нас, пропуская её внутренний код.
Затем выполнение приостанавливается сразу после вызова функции.
Это хорошо, если нам не интересно видеть, что происходит внутри вызова функции.
– «Step into», быстрая клавиша F11 .
Это похоже на «Step», но ведёт себя по-другому в случае асинхронных вызовов функций. Если вы только начинаете изучать JavaScript, то можете не обращать внимания на разницу, так как у нас ещё нет асинхронных вызовов.
На будущее просто помните, что команда «Step» игнорирует асинхронные действия, такие как setTimeout (вызов функции по расписанию), которые выполняются позже. «Step into» входит в их код, ожидая их, если это необходимо. См. DevTools manual для получения более подробной информации.
– «Step out»: продолжить выполнение до завершения текущей функции, быстрая клавиша Shift + F11 .
Продолжает выполнение и останавливает его в самой последней строке текущей функции. Это удобно, когда мы случайно вошли во вложенный вызов, используя , но это нас не интересует, и мы хотим продолжить его до конца как можно скорее.
– активировать/деактивировать все точки останова(breakpoints).
Эта кнопка не влияет на выполнение кода, она лишь позволяет массово включить/отключить точки останова.
– включить/отключить автоматическую паузу в случае ошибки.
При включении, если открыты инструменты разработчика, ошибка при выполнении скрипта автоматически приостанавливает его. Затем мы можем проанализировать переменные в отладчике, чтобы понять, что пошло не так. Поэтому, если наш скрипт умирает с ошибкой, мы можем открыть отладчик, включить эту опцию и перезагрузить страницу, чтобы увидеть, где он умирает и каков контекст в этот момент.
Щелчок правой кнопкой мыши по строке кода открывает контекстное меню с отличной опцией под названием «Continue to here» («продолжить до этого места»).
Это удобно, когда мы хотим перейти на несколько шагов вперёд к строке, но лень устанавливать точку останова (breakpoint).
Логирование
Чтобы вывести что-то на консоль из нашего кода, существует функция console.log .
Если вы — JavaScript-разработчик или хотите им стать, это значит, что вам нужно разбираться во внутренних механизмах выполнения JS-кода. В частности, понимание того, что такое контекст выполнения и стек вызовов, совершенно необходимо для освоения других концепций JavaScript, таких, как поднятие переменных, области видимости, замыкания. Материал, перевод которого мы сегодня публикуем, посвящён контексту выполнения и стеку вызовов в JavaScript.
Контекст выполнения
Контекст выполнения (execution context) — это, если говорить упрощённо, концепция, описывающая окружение, в котором производится выполнение кода на JavaScript. Код всегда выполняется внутри некоего контекста.
▍Типы контекстов выполнения
В JavaScript существует три типа контекстов выполнения:
- Глобальный контекст выполнения. Это базовый, используемый по умолчанию контекст выполнения. Если некий код находится не внутри какой-нибудь функции, значит этот код принадлежит глобальному контексту. Глобальный контекст характеризуется наличием глобального объекта, которым, в случае с браузером, является объект window , и тем, что ключевое слово this указывает на этот глобальный объект. В программе может быть лишь один глобальный контекст.
- Контекст выполнения функции. Каждый раз, когда вызывается функция, для неё создаётся новый контекст. Каждая функция имеет собственный контекст выполнения. В программе может одновременно присутствовать множество контекстов выполнения функций. При создании нового контекста выполнения функции он проходит через определённую последовательность шагов, о которой мы поговорим ниже.
- Контекст выполнения функции eval . Код, выполняемый внутри функции eval , также имеет собственный контекст выполнения. Однако функцией eval пользуются очень редко, поэтому здесь мы об этом контексте выполнения говорить не будем.
Стек выполнения
Стек выполнения (execution stack), который ещё называют стеком вызовов (call stack), это LIFO-стек, который используется для хранения контекстов выполнения, создаваемых в ходе работы кода.
Когда JS-движок начинает обрабатывать скрипт, движок создаёт глобальный контекст выполнения и помещает его в текущий стек. При обнаружении команды вызова функции движок создаёт новый контекст выполнения для этой функции и помещает его в верхнюю часть стека.
Движок выполняет функцию, контекст выполнения которой находится в верхней части стека. Когда работа функции завершается, её контекст извлекается из стека и управление передаётся тому контексту, который находится в предыдущем элементе стека.
Изучим эту идею с помощью следующего примера:
Вот как будет меняться стек вызовов при выполнении этого кода.
Состояние стека вызовов
Когда вышеприведённый код загружается в браузер, JavaScript-движок создаёт глобальный контекст выполнения и помещает его в текущий стек вызовов. При выполнении вызова функции first() движок создаёт для этой функции новый контекст и помещает его в верхнюю часть стека.
При вызове функции second() из функции first() для этой функции создаётся новый контекст выполнения и так же помещается в стек. После того, как функция second() завершает работу, её контекст извлекается из стека и управление передаётся контексту выполнения, находящемуся в стеке под ним, то есть, контексту функции first() .
Когда функция first() завершает работу, её контекст извлекается из стека и управление передаётся глобальному контексту. После того, как весь код оказывается выполненным, движок извлекает глобальный контекст выполнения из текущего стека.
О создании контекстов и о выполнении кода
До сих пор мы говорили о том, как JS-движок управляет контекстами выполнения. Теперь поговорим о том, как контексты выполнения создаются, и о том, что с ними происходит после создания. В частности, речь идёт о стадии создания контекста выполнения и о стадии выполнения кода.
▍Стадия создания контекста выполнения
Перед выполнением JavaScript-кода создаётся контекст выполнения. В процессе его создания выполняются три действия:
- Определяется значение this и осуществляется привязка this (this binding).
- Создаётся компонент LexicalEnvironment (лексическое окружение).
- Создаётся компонент VariableEnvironment (окружение переменных).
Привязка this
В глобальном контексте выполнения this содержит ссылку на глобальный объект (как уже было сказано, в браузере это объект window ).
В контексте выполнения функции значение this зависит от того, как именно была вызвана функция. Если она вызвана в виде метода объекта, тогда значение this привязано к этому объекту. В других случаях this привязывается к глобальному объекту или устанавливается в undefined (в строгом режиме). Рассмотрим пример:
Лексическое окружение
В соответствии со спецификацией ES6, лексическое окружение (Lexical Environment) — это термин, который используется для определения связи между идентификаторами и отдельными переменными и функциями на основе структуры лексической вложенности ECMAScript-кода. Лексическое окружение состоит из записи окружения (Environment Record) и ссылки на внешнее лексическое окружение, которая может принимать значение null .
Проще говоря, лексическое окружение — это структура, которая хранит сведения о соответствии идентификаторов и переменных. Под «идентификатором» здесь понимается имя переменной или функции, а под «переменной» — ссылка на конкретный объект (в том числе — на функцию) или примитивное значение.
В лексическом окружении имеется два компонента:
- Запись окружения. Это место, где хранятся объявления переменных и функций.
- Ссылка на внешнее окружение. Наличие такой ссылки говорит о том, что у лексического окружения есть доступ к родительскому лексическому окружению (области видимости).
- Глобальное окружение (или глобальный контекст выполнения) — это лексическое окружение, у которого нет внешнего окружения. Ссылка глобального окружения на внешнее окружение представлена значением null . В глобальном окружении (в записи окружения) доступны встроенные сущности языка (такие, как Object , Array , и так далее), которые связаны с глобальным объектом, там же находятся и глобальные переменные, определённые пользователем. Значение this в этом окружении указывает на глобальный объект.
- Окружение функции, в котором, в записи окружения, хранятся переменные, объявленные пользователем. Ссылка на внешнее окружение может указывать как на глобальный объект, так и на внешнюю по отношении к рассматриваемой функции функцию.
- Декларативная запись окружения, которая хранит переменные, функции и параметры.
- Объектная запись окружения, которая используется для хранения сведений о переменных и функциях в глобальном контексте.
Обратите внимание на то, что в окружении функции декларативная запись окружения, кроме того, содержит объект arguments , который хранит соответствия между индексами и значениями аргументов, переданных функции, и сведения о количестве таких аргументов.
Лексическое окружение можно представить в виде следующего псевдокода:
Окружение переменных
Окружение переменных (Variable Environment) — это тоже лексическое окружение, запись окружения которого хранит привязки, созданные посредством команд объявления переменных ( VariableStatement ) в текущем контексте выполнения.
Так как окружение переменных также является лексическим окружением, оно обладает всеми вышеописанными свойствами лексического окружения.
В ES6 существует одно различие между компонентами LexicalEnvironment и VariableEnvironment . Оно заключается в том, что первое используется для хранения объявлений функций и переменных, объявленных с помощью ключевых слов let и const , а второе — только для хранения привязок переменных, объявленных с использованием ключевого слова var .
Рассмотрим примеры, иллюстрирующие то, что мы только что обсудили:
Схематичное представление контекста выполнения для этого кода будет выглядеть так:
Как вы, вероятно, заметили, переменные и константы, объявленные с помощью ключевых слов let и const , не имеют связанных с ними значений, а переменным, объявленным с помощью ключевого слова var , назначено значение undefined .
Это так из-за того, что во время создания контекста в коде осуществляется поиск объявлений переменных и функций, при этом объявления функций целиком хранятся в окружении. Значения переменных, при использовании var , устанавливаются в undefined , а при использовании let или const остаются неинициализированными.
Именно поэтому можно получить доступ к переменным, объявленным с помощью var , до их объявления (хотя они и будут иметь значение undefined ), но, при попытке доступа к переменным или константам, объявленным с помощью let и const , выполняемой до их объявления, возникает ошибка.
Только что мы только что описали, называется «поднятием переменных» (Hoisting). Объявления переменных «поднимаются» в верхнюю часть их лексической области видимости до выполнения операций присвоения им каких-либо значений.
▍Стадия выполнения кода
Это, пожалуй, самая простая часть данного материала. На этой стадии выполняется присвоение значений переменным и осуществляется выполнение кода.
Обратите внимание на то, что если в процессе выполнения кода JS-движок не сможет найти в месте объявления значение переменной, объявленной с помощью ключевого слова let , он присвоит этой переменной значение undefined .
Итоги
Только что мы обсудили внутренние механизмы выполнения JavaScript-кода. Хотя для того, чтобы быть очень хорошим JS-разработчиком, знать всё это и не обязательно, если у вас имеется некоторое понимание вышеописанных концепций, это поможет вам лучше и глубже разобраться с другими механизмами языка, с такими, как поднятие переменных, области видимости, замыкания.
Цикл событий (event loop) — ключ к асинхронному программированию на JavaScript. Сам по себе язык однопоточный, но использование этого механизма позволяет создать дополнительные потоки, чтобы код работал быстрее. В этой статье разбираемся, как устроен стек вызовов и как они связаны с циклом событий.
Это адаптированный перевод выступления Роберта Филлипса, JavaScript-разработчика из британской компании Andea, на конференции JSConf. Повествование ведется от лица автора, скриншоты взяты отсюда.
Программировать на JavaScript я начал около 10 лет назад, и только недавно задумался, как этот язык программирования работает на самом деле. Долгое время однопоточность, движок V8, функции обратного вызова и циклы событий были для меня просто словами: я знал, как ими пользоваться, но не понимал, как они работают на глубоком уровне.
В процессе изучения я узнал, как работают стеки вызовов (call stack), циклы событий (event loop), очереди обратных вызовов (callback queue), интерфейсы API и другие сущности. В этой статье расскажу о некоторых своих открытиях.
Статья будет полезна как новичкам, так и опытным разработчикам. Первым она поможет понять, почему JavaScript настолько сильно отличается от других языков программирования и чем функция обратного вызова очень полезна на практике. Вторым — глубже разобраться в среде исполнения этого языка программирования.
Как устроена среда исполнения
На схеме ниже — упрощенное представление среды исполнения Runtime. В частности, движок V8 представлен как среда выполнения команд внутри браузера Google Chrome:
Распределение памяти происходит в heap, а stack frames хранятся в стеке вызовов (call stack).
Как говорилось выше, JavaScript — это однопоточный (single threaded) язык программирования с одиночным стеком вызовов. Это означает, что в один момент JavaScript может выполнять только одну операцию (обработать только один кусок кода).
Рассмотрим пример с несколькими функциями: одна перемножает числа, другая возводит получившиеся число в квадрат, а третья — выводит результат на экран.
Прежде, чем разбирать этот код, вернемся на шаг назад и выясним, как работает стек вызовов. Это механизм, предназначенный для отслеживания текущего местонахождения интерпретатора в скрипте. Он вызывает несколько функций и определяет, какая из них выполняется на данный момент, какие функции вызываются внутри выполняемой функции и какая будет вызвана следующей.
Стек вызовов создается, когда внутри функции (или метода) существуют другие функции. В реальных приложениях таких уровней могут быть сотни. В нашем примере это возведение в квадрат — функция внутри функции выведения на экран результата. Стек вызовов формируется каждый раз, когда вы запускаете код на стороне браузера.
Рассмотрим пример кода ниже: baz — функция, которая после запуска в Chrome вызывает bar , затем foo и возвращает ошибку.
В качестве результата выполнения функция выводит на экран трассировку стека (stack trace) — состояние стека в тот момент, когда произошла ошибка. Это пример того, что называется «неполадками в стеке» (blowing the stack).
Посмотрим где именно возникла ошибка. В стеке мы видим очередь вложенных функций foo, bar, baz и анонимную функцию, которая в данном случае главная.
В этом случае есть главная функция, которая вызывает foo , которая, в свою очередь вызывает foo , та снова вызывает foo , снова foo и так далее. В какой-то момент Chrome говорит нам: «Возможно, рекурсивно вызывать foo 16000 раз не стоит? Я просто отключу эти функции, чтобы вы могли изучить код программы и найти ошибку».
Перейдем к следующему вопросу: почему программа выполняется медленно? На скорость, например, влияет выполнение условного цикла от одного до миллиарда, а также выполнение запросов по сети. К медленным можно отнести и запросы на загрузку картинок. Большое количество медленных запросов тормозит стек.
Рассмотрим пример: у нас есть код с функцией getSync() — это функция jQuery с AJAX-запросом.
Представим, что эти запросы — синхронные. При выполнении кода в текущем виде сначала вызывается функция getSync , а затем — сетевой запрос (network request). Скорость выполнения последнего зависит от компьютера и может быть довольно низкой.
После того, как сетевой запрос прошел, выполняются все три функции. Теперь мы можем очистить стек.
В однопоточном языке программирования (в отличие, например, от Ruby с его threads) для выполнения всех функций нужно ждать выполнения сетевого запроса. Здесь возникает проблема: код выполняется в браузере, который блокирует его на время выполнения этого синхронного запроса: он не может ни отрисовывать изображения, ни выполнять другой код.
Если мы хотим сделать хороший пользовательский интерфейс, блокировки стека допускать нельзя. Самое очевидное решение — использовать асинхронные обратные вызовы (asynchronous callbacks). В этом случае браузер не блокирует функции, а при выполнении кода задается коллбэк, который выполняется позднее.
Рассмотрим на простом примере, как работают асинхронные вызовы:
При запуске функция setTimeout ставит console.log в очередь на пять секунд. В это время выводится JSConfEU, а в конце на экран выводится there .
При запуске кода мы видим, что функции выполняются одновременно. Среда исполнения JS Runtime в один момент времени может выполнить только одну функцию: например, не может выполнить AJAX-запрос во время выполнения другого кода. В данном случае несколько функций выполняются одновременно, поскольку браузер — это не только среда исполнения. В нем есть еще API, это позволяет совершать несколько действий одновременно.
Функция setTimeout в данном случае выполняется через web-API, который предоставляет браузер. В исходном коде движка V8 этой функции нет. Web-API не изменяет код, а только помещает отдельные команды на стек. Кроме того, любой web-API помещает коллбэк в очередь задач после своего выполнения.
Цикл событий
Теперь мы знаем достаточно, чтобы перейти к главной теме этой статьи — рассказу о том, как работают циклы событий (event loop). Главная задача циклов событий — следить за стеком и очередью задач. Если стек пуст, цикл берет первый элемент из очереди, помещает его в стек и выполняет.
На картинке выше видно, что если стек пуст, то цикл берет первый элемент из очереди задач и помещает его в стек. В очереди задач при этом находится коллбэк.
Первая сложность, с которой мы сталкиваемся — установка нулевого времени в функции setTimeout . Она ведет себя странно: переданный колбек не сразу выполняется, а попадает в очередь задач. Причина в механизме работы цикла событий: он ждет очистки стека и когда это происходит, помещает колбек в стек. Затем выполняются другие функции.
Установка нулевого времени в функции setTimeout приводит к тому, что она откладывает исполнение колбека и переносит его в конец стека.
Все web-API работают похожим образом. Например, AJAX-запрос на URL-адрес с обратным вызовом будет выполняться точно также.
XHR-запрос будет выполняться параллельно — он может быть не выполнен никогда, но стек при этом будет продолжать работать.
Если мы добавим DOM API и задержку по времени, код продолжит работать: коллбэк просто встанет в очередь.
Если я нажму на кнопку «Click Me!», запустится web-API, который добавит в очередь коллбэк и запустит его. Если нажать на кнопку десять раз, вызов встает в очередь и обрабатывается.
Рассмотрим еще один пример с асинхронным API: в нем мы вызываем функцию setTimeout четыре раза, с односекундной задержкой, после чего выполняем команду console.log, которая выводит на экран hi .
К тому времени, как все обратные вызовы окажутся в очереди, четвертый запрос с секундной задержкой будет находиться в состоянии ожидания. Этот пример демонстрирует минимальное время на исполнение функций в очереди — setTimeout не выполняет код сразу же, а сделает это в порядке очереди. Точно также как функция setTimeout с нулевой задержкой не вызывает колбек сразу же.
На этом примере хорошо видна разница между двумя типами обратных вызовов: они могут быть либо функцией, которую вызывает другая функция, либо асинхронным вызовом.
Другой пример: метод forEach берёт переданную функцию-колбек и запускает её внутри стека, не асинхронно.
Метод forEach здесь выполняется медленно — после очереди из обратных вызовов мы можем выполнить код и вывести результат на экран через console.log .
Рендеринг
Определенные действия в JS накладывают на рендеринг в браузере ограничения. В идеальной ситуации браузер перерисовывает экран раз в 16,6 миллисекунд и воспроизводит результат со скоростью 60 кадров в секунду.
Однако пока в стеке есть код, браузер не может проводить рендеринг. Если рендер можно назвать вызовом, то в нем почти всегда есть коллбэк, которому для начала работы нужно дождаться очистки стека. Разница в том, что приоритет рендера выше: каждые 16 миллисекунд он ставит отрисовку в очередь.
Что означает блокировка рендера? Пока выполняется код, невозможно выделить текст на экране, кликать на кнопки и видеть отклик.
Код на картинке выше организован так, что рендер выполняется асинхронно — в коде оставлено место для того, чтобы он выполнял отрисовку между каждым элементом.
Все, о чем мы говорили выше — имитация того, как работает рендер. Но это хороший способ показать, что имеют в виду программисты, которые предостерегают от блокировки цикла событий.
Предостережения звучат так: «не используйте медленный код, иначе браузер не сможет рендерить красивые и органичные элементы интерфейса». Поэтому когда вы решаете задачу обработки изображений или анимирования большого количества изображений, но при этом не уделили должного внимания очереди, код будет работать медленно. В этой статье мы убедились, что это чистая правда.
Читайте также: