“Не люблю темные стекла, сквозь них темное небо.
Дайте мне войти, откройте двери.”

(Виктор Цой)

Введение

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

Как работает Javascript

Кто-то так сразу и спрашивал: "Как работает Javascript?". Но были и те кто заходил издалека и с помощью наводящих вопросов пытались вытянуть из меня эту информацию. Список вопросов, которые мне задавали:

  • Что такое асинхронность в Javascript?

  • Что такое event loop?

  • Что такое контекст выполнения?

  • Что такое стек вызовов?

  • Что такое JavaScript AST?

Ответы на эти вопросы требуют понимания того, как работает Javascript, что он из себя представляет и как он взаимодействует с окружением. Свою популярность он обрёл благодаря Web браузерам, которые выбрали его в качестве языка сценариев, позволяющих гибко взаимодействовать с пользователем. Javascript – это высокоуровневый язык программирования и для работы с ним у каждого браузера есть специальный JavaScript engine, точнее ECMAScript engine. На данный момент самыми популярными ECMAScript engine являются следующие:

  • Chakra, Microsoft IE/Edge

  • SpiderMonkey, FireFox

  • V8, Chrome

Движки (engine) которые работают с Javascript преобразовывают его в байт код, который потом непосредственно исполняется. Преобразование оригинального кода в byte code “движком” V8 от Chrome выполняется по следующей схеме:

  1. Первым шагом, парсер проводит лексический и синтаксический анализ кода. На выходе получается Абстрактное синтаксическое дерево (AST) (ссылка). В AST Explorer можно посмотреть, как выглядит это дерево. Стоит отметить такие узлы дерева как FunctionDeclaration и VariableDeclaration в них хранятся объявления функций и переменных. Парсеры исходного кода в AST могут быть от разных производителей. Ниже список самых популярных среди них:

    1. babel-parser

    2. espree

    3. acorn

    4. esprima

    5. cherow

  2. Interpreter на вход получает AST и строит Byte code, при этом вызывая Profiler. Пока работает Profiler, V8 engine исполняет байт код.

  3. Profiler в это время проводит оптимизацию кода и передаёт оптимизированный код Compiler’у

  4. Compiler создает оптимизированный byte code.

  5. Временный byte code заменяется оптимизированным.

Далее написанный код начинает исполняться. Код, написанный на Javascript, выполняется синхронно – одна команда в один момент времени. Код работает в браузере, в котором множество процессов работают параллельно и для взаимодействия с этими процессами придумали следующее:

  1. Для каждой html страницы в браузере Javascript выполняется в своем отдельном потоке (Main Thread).

  2. Содержимое тега <script>…</script> или содержимое файла переданного в свойстве тега <script src=”file.js”>…</script> начнёт исполняться в процессе загрузки документа в браузер. Причём подгрузиться он может раньше, чем HTML-элементы на этой странице (ссылка).

  3. Для того чтобы взаимодействовать с Web формой в HTML можно назначить функцию обработки на какое-либо событие:

    1. В HTML: onclick = ‘myFunc()’

    2. В Javascript: element.addEventListener("click", myFunc(), false);

  4. Для взаимодействия с другими процессами браузера существует набор интерфейсов Web API (ссылка). При вызове функций этих интерфейсов нужно обязательно в качестве параметра передать функцию обратного вызова (callback function), которой будет передано управление после того, как Web API функция отработает. Эти интерфейсы не ограниченны одним потоком и могут работать параллельно. Кстати, setTimeout() так же является частью Web API.

Далее при наступлении события или по окончанию работы функции Web API, функции обработки события и функции обратного вызова попадают в очередь - Task Queue. Откуда их извлекает и передаёт на исполнение Event Loop. Кроме Task Queue есть ещё:

  • Render Queue, которая отслеживает все изменения DOM модели. На данный момент раз в 16.6 мс. (при 60FPS) происходит перерисовка Web страницы и обновляются все связанные с ней элементы DOM.

  • Microtask Queue, которая выполняет код на Javascript, в функциях Promise.prototype.then() и Promise.prototype.catch(), а так же код который выполнится внутри async функций после выполнения таски с ключевым словом await. Микротаски выполняются сразу после завершения Таски, c которой они связанны. Таким образом это не совсем отдельная очередь. Просто это некий довесок к таске, который должен выполнится сразу после неё. Сюда так же входит код, который выполниться сразу после await, в функции с ключевым словом async.

Event Loop работает с очередями в следующем приоритете:

  1. Render Queue

  2. Task Queue (или Callback Queue, Macrotask Queue, Event Queue)

  3. После каждой выполненной Таски выполняются связанные с ней Микротаски

В различных Web browser’ах могут быть свои особенности реализации Event Loop, но суть и принцип его работы в общем похожи.

В Node.js концепт тот же – внешняя функция выполняется параллельно и возвращает результат своей работы в callback функцию, которая будет вызвана в порядке очереди, но есть отличия. Вместо Web API используется библиотека libuv и Node API. DOM, HTML и Render Queue отсутствуют, а callback функции имеют свой приоритет обработки. Сам Event Loop реализован на функциях библиотеки libuv. Так же она используется для взаимодействия с операционной системой, на которой развернут сервер Node.js.

Согласно документации, Event Loop в Node.js выполняет 6 операций в определенном порядке.

  1. timers: в этой фазе выполняются callback функции от функций setTimeout() и setInterval().

  2. pending callbacks: выполняются callback функции от системных операций. Как я понял, обработчики ошибок TCP скорее всего будут здесь.

  3. idle, prepare: выполняется внутренний код Event Loop

  4. poll: на этом шаге выполняется много всего, но для Event Loop важно то, что здесь выполняются callback функции, кроме тех, что в pending callbacks и close callbacks.

  5. check: выполняются callback функции от setImmediate()

  6. close callbacks: callback функции закрывающие различные соединения (socket.on('close', ...) и т.п.).

Помимо операций выше, существуют callback функции от process.nextTick(), которые выполняются сразу после завершения текущей таски. Так же код, который содержится в Promise.prototype.then() и Promise.prototype.catch() и в функции async после await, будет выполнятся сразу после таски которая связанна с этим кодом (Microtasks).

Все эти 6 шагов можно найти в коде libuv. Задача libuv поддерживать асинхронный ввод/вывод, основанный на цикле событий. Причем сделать так, чтобы это все могло работать под различными операционными системами.  Отсюда появляется шаг “pending callbacks” и “idle, prepare”, которые появились благодаря нюансам работы операционных систем. Эти 2 шага находятся между Timers и Poll. Получается, что прежде, чем вызвать обычные callback функции сначала вызываются системные, потом небольшая задержка и только потом остальные. Для упрощения схемы можно объединить "pending callbacks", "idle, prepare" и "poll" в один большой шаг, в котором вызываются callback функции. В итоге получается следующая схема:

Заключение:

Надеюсь, что после описания того, как это работает ответить на вопросы будет не так сложно. Итак, краткие ответы на вопросы, которые были озвучены вначале:

  • Что такое асинхронность в Javascript?

    Callback функции и механизм работы с ними в Javascript. (ссылка)

  • Что такое event loop?

    Event loop - это бесконечный цикл в котором движок JavaScript ожидает задачи и исполняет их. (ссылка)

  • Что такое контекст выполнения?

    Контекст выполнения – специальная внутренняя структура данных, которая содержит информацию о вызове функции и включает в себя:

    • конкретное место в коде, на котором находится интерпретатор;

    • локальные переменные функции;

    • значение this;

    • прочую служебную информацию. (ссылка)

  • Что такое стек вызовов?

    стек вызовов - это структура данных, которая используется для хранения контекстов выполнения, создаваемых в ходе работы кода. Стек выполнения действует по принципу “первым вошел последним вышел”. (ссылка)

  • Что такое JavaScript AST?

    AST (Абстрактное синтаксическое дерево) - древовидное представление исходного кода. (ссылка)

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

P.S.

Не стоит забывать и про "глупые вопросы", которые вам могут задать на собеседовании (ссылка). Порой они не такие уж и глупые (ссылка). В этом плане мне очень понравился анекдот:

После лекции для HR-специалистов одна из слушательниц спрашивает у докладчика:
— Собеседования отнимают очень много времени. Скажите, как можно максимально быстро определить, что за человек перед тобой — идиот или нормальный?
— Конечно. Задайте ему какой-нибудь простейший вопрос. Например: «Известно, что Кук совершил три путешествия, во время одного их них он погиб. Во время какого именно?»
— А можно какой-нибудь другой пример? А то у меня в школе было плохо с географией. (
ссылка)

Ссылки

https://habr.com/ru/post/439564/

https://astexplorer.net/

https://habr.com/ru/post/439564/

https://blog.bitsrc.io/javascript-under-the-hood-632ccae06b27

https://nuancesprog.ru/p/4553/

https://blog.bitsrc.io/javascript-under-the-hood-632ccae06b27

https://dev-gang.ru/article/kak-rabotaet-javascript-pod-kapotom-dvizhka-v-5ew7muxdnq/

https://medium.com/nuances-of-programming/%D0%BA%D0%B0%D0%BA-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D0%B5%D1%82-javascript-cdbef3f20a66

https://medium.com/@deedee8/event-loop-cycle-in-node-js-bc9dd0f2834f

https://aldizhupani.medium.com/javascript-async-await-microtask-queue-explained-9844f010bb0f

http://imnotgenius.com/21-sobytijnyj-tsikl-biblioteka-libuv/

http://docs.libuv.org/en/v1.x/design.html

https://russianblogs.com/article/51951362749/

https://habr.com/ru/post/336498/

https://snyk.io/blog/nodejs-how-even-quick-async-functions-can-block-the-event-loop-starve-io/

https://habr.com/ru/post/479062/

https://nexocode.com/blog/posts/behind-nodejs-event-loop/

https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

https://developer.mozilla.org/ru/docs/Web/API

https://javascript.info/event-loop

https://habr.com/ru/company/hh/blog/517594/

https://hackernoon.com/is-javascript-a-single-threaded-language-w6v3ujb

https://bool.dev/blog/detail/obyasnenie-event-loop-v-javascript-s-pomoshchyu-vizualizatsii

https://developer.mozilla.org/ru/docs/Web/API/window/requestAnimationFrame

 https://frarizzi.science/journal/web-engineering/browser-rendering-queue-in-depth

 https://habr.com/ru/post/461401/