Веб-разработчики или фронтенд-инженеры, как мы любим себя называть, теперь делают все, начиная от создания интерактивности в браузере до создания компьютерных игр, настольных виджетов, кроссплатформенных мобильных приложений или написания кода на стороне сервера (наиболее популярно с использованием node.js), чтобы подключить его к любой базе данных - достигая почти всеобщности в качестве языка сценариев. Поэтому важно познакомиться с внутренностями Javascript, чтобы использовать его лучше и эффективнее, и об этом и будет рассказано в статье.
Экосистема Javascript стала более сложной, чем когда-либо, и будет продолжать становиться еще более сложной. Инструменты, необходимые для создания современного веб-приложения, ошеломляют своим разнообразием: Webpack, Babel, ESLint, Mocha, Karma, Grunt... и т.д. - какой инструмент использовать и что делает каждый? Я нашел этот веб-комикс, который идеально иллюстрирует борьбу веб-разработчиков на сегодняшний день.
Независимо от всего этого, каждый разработчик на Javascript сначала должен изучить основы того, как все это работает внутри на корневом уровне, прежде чем погружаться в использование какого-либо фреймворка или библиотеки, доступной на рынке. Большинство JS-разработчиков, вероятно, слышали термин V8, Runtime Chrome, но некоторые даже не знали, что это значит, что это делает. В начале своей профессиональной карьеры я, как разработчик, тоже не знал много о всех этих причудливых терминах, так как в первую очередь дело было в завершении работы. Но мое любопытство не удовлетворилось тем, как вообще Javascript способен делать все это.
Я решил углубиться, порыскать в Google и наткнулся на несколько хороших блогов, в том числе на блог Филиппа Робертса, отличную лекцию на JSConf о цикле событий, и я решил подвести итоги своего обучения и поделиться ими. Поскольку есть множество вещей, которые нужно знать, я разделил статью на 2 части. В этой части будут введены термины, которые часто используются, а во второй части будут рассмотрены связи между всеми этими терминами.
Javascript - это однопоточный язык, что означает, что он может обрабатывать одну задачу за раз или один кусок кода за раз. У него есть один стек вызовов, который вместе с другими частями, такими как куча и очередь, составляет модель параллелизма JavaScript (реализованную внутри V8). Давайте сначала рассмотрим каждый из этих терминов:
- Стек вызовов - это структура данных, которая записывает вызовы функций, где в программе мы находимся. Если мы вызываем функцию для выполнения, мы помещаем что-то в стек, и когда мы возвращаемся из функции, мы удаляем верхний элемент стека.
При запуске данного файла мы сначала ищем функцию main, где начинается вся исполнение. В приведенном выше примере это начинается с console.log(bar(6)), которая помещается в стек, затем на вершину стека помещается функция bar с ее аргументами, которая в свою очередь вызывает функцию foo, которая также помещается на вершину стека, а затем немедленно возвращается и удаляется из стека. Аналогично, функция bar затем удаляется, а наконец, оператор console удаляется и выводится результат. Все это происходит в доли секунды (в миллисекундах) один за другим.
Вы наверняка видели длинный красный стек трассировки ошибок иногда в консоли вашего браузера, который указывает текущее состояние стека вызовов и где в функции произошла ошибка сверху вниз, как стек (см. изображение ниже).
Иногда мы попадаем в бесконечный цикл, вызывая функцию множество раз рекурсивно, а для браузера Chrome есть ограничение на размер стека, которое составляет 16 000 кадров, превышение которого может вызвать ошибку "Max Stack Error Reached" и привести к прекращению работы. (изображение ниже).
2. Куча (Heap): Объекты выделяются в куче, то есть в основном в неструктурированной области памяти. Вся выделенная память для переменных и объектов происходит здесь.
3. Очередь (Queue) - это список сообщений, которые должны быть обработаны и связанных с ними функций обратного вызова, находящийся в JavaScript-рантайме. Когда стек имеет достаточную емкость, сообщение из очереди извлекается и обрабатывается, что состоит в вызове связанной функции (и, следовательно, создании начального кадра стека). Обработка сообщения заканчивается, когда стек становится пустым. В простых словах, эти сообщения ставятся в очередь в ответ на внешние асинхронные события (например, щелчок мыши или получение ответа на HTTP-запрос), если предоставлена функция обратного вызова. Если, например, пользователь нажмет кнопку, и функция обратного вызова не будет предоставлена, сообщение не будет поставлено в очередь.
Цикл событий (Event Loop) - это механизм, используемый в JavaScript для управления асинхронным кодом. В основе его работы лежит принцип очереди сообщений, которые содержат функции обратного вызова, связанные с асинхронными событиями, такими как клики на кнопки или получение ответов на запросы к серверу.
При оценке производительности нашего кода на JavaScript, выполнение функции в стеке может быть медленным или быстрым - вызов console.log() будет быстрым, а выполнение итераций с помощью циклов for или while для тысяч или миллионов строк будет медленным и будет блокировать стек. Это называется блокировкой скрипта, о которой вы читали или слышали в инструментах анализа скорости загрузки веб-страниц.
Запросы к сети могут быть медленными, запросы изображений могут быть медленными, но к счастью, запросы к серверу могут быть выполнены через AJAX, асинхронную функцию. Если бы эти запросы к сети были сделаны через синхронные функции, что бы произошло? Запросы к сети отправляются на другой компьютер/машину где-то в сети, и эти компьютеры могут быть медленными в ответе. Тем временем, если я нажму на какую-то кнопку, или нужно выполнить другой рендеринг, ничего не произойдет, так как стек заблокирован. В многопоточных языках, таких как Ruby, это можно обработать, но в однопоточном языке, таком как JavaScript, это невозможно, если функция внутри стека не возвращает значение. Веб-страница будет работать неустойчиво, так как браузер не сможет ничего сделать. Как мы можем справиться с этим?
Самым простым решением является использование асинхронных обратных вызовов, что означает, что мы запускаем некоторую часть кода и передаем ему обратный вызов (функцию), который будет выполнен позже. Мы все сталкивались с асинхронными обратными вызовами, такими как любой AJAX-запрос с помощью $.get(), setTimeout(), setInterval(), Promises и т.д. Node работает со всеми асинхронными функциями. Все эти асинхронные обратные вызовы не запускаются немедленно и будут запущены через некоторое время, поэтому их нельзя немедленно поместить в стек, в отличие от синхронных функций, таких как console.log(), математические операции. Куда же они идут и как они обрабатываются?
Если мы рассмотрим действие запроса в сеть в JavaScript, как в приведенном выше коде:
- Функция запроса выполняется, передавая анонимную функцию в событие onreadystatechange в качестве обратного вызова для выполнения, когда ответ будет доступен в будущем.
- Сразу же выводится "Script call done!" в консоль.
- В какой-то момент в будущем приходит ответ, и наш обратный вызов выполняется, выводя его тело в консоль.
- Разъединение вызывающего кода от ответа позволяет среде выполнения JavaScript выполнять другие действия, пока ожидается завершение асинхронной операции и выполнение ее обратных вызовов. Именно здесь начинают работать API браузера, вызывающие свои API, которые, по сути, являются потоками, созданными браузером и реализованными на C++, для обработки асинхронных событий, таких как события DOM, HTTP-запросы, setTimeout и т. д. (После того, как мы узнали это, в Angular 2 используются Zones, которые могут изменять работу времени выполнения, монки-патча эти API, я теперь могу представить, как им это удалось сделать.)
API браузера - потоки, созданные браузером и реализованные на C++, для обработки асинхронных событий, таких как события DOM, HTTP-запросы, setTimeout и т. д.
Теперь эти веб-API не могут сами поместить код выполнения на стек, иначе он случайным образом появится в середине вашего кода. Описанная выше очередь сообщений обратных вызовов показывает путь. Когда какой-либо из WebAPI заканчивает выполнение, он помещает обратный вызов в эту очередь. Event Loop теперь отвечает за выполнение этих обратных вызовов в очереди и помещение их в стек, когда он пустой. Основная задача event loop - просматривать стек и очередь задач, помещая первую задачу из очереди на стек, когда стек пуст. Каждое сообщение или обратный вызов обрабатывается полностью, прежде чем обрабатываться другое сообщение.
while (queue.waitForMessage()) {
queue.processNextMessage();
}
В веб-браузерах сообщения добавляются каждый раз, когда происходит событие, и к нему присоединен обработчик событий. Если обработчика нет, событие теряется. Таким образом, щелчок на элементе с обработчиком событий click добавит сообщение, и то же самое произойдет с любым другим событием. Вызов этой функции обратного вызова служит первым фреймом в стеке вызовов, и из-за однопоточности JavaScript дальнейшее опрос сообщений и их обработка останавливается до возврата всех вызовов из стека. Последующие (синхронные) вызовы функций добавляют новые фреймы вызова в стек.
В следующей части я покажу визуальную анимацию выполнения кода для вышеуказанной процедуры, дополнительно объяснив, что такое различные типы асинхронных функций, такие как задачи, микрозадачи, и какой из них имеет приоритет в очереди. Также расскажу о таких хаках, как нулевая задержка, используемая для выполнения некоторых функций.
Надеюсь, вам понравилось, не стесняйтесь оставлять свои ценные комментарии, чтобы помочь мне стать лучше.