Как работает цикл событий в JavaScript?

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

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

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

Эта статья посвящена одной из очень важных, но редко понимаемых концепций или терминов в JavaScript. ЦИКЛ СОБЫТИЙ!.

В JavaScript нельзя избежать написания асинхронного кода, но что на самом деле означает код, работающий асинхронно? т.е. цикл событий

Прежде чем мы сможем понять, как работает цикл событий, мы должны сначала понять, что такое сам JavaScript и как он работает!

Что такое JavaScript?

Прежде чем мы продолжим, я хотел бы, чтобы мы сделали шаг назад к самым основам. Что такое JavaScript на самом деле? Мы могли бы определить JavaScript как;

JavaScript — это высокоуровневый, интерпретируемый, однопоточный, неблокирующий, асинхронный, параллельный язык.

Подожди, что это? Книжное определение? 🤔

Давайте сломаем это!

Ключевые слова в отношении этой статьи: однопоточный, неблокирующий, параллельный и асинхронный.

Один поток

Поток выполнения — это наименьшая последовательность запрограммированных инструкций, которой планировщик может управлять независимо. Язык программирования является однопоточным, что означает, что он может одновременно выполнять только одну задачу или операцию. Это означает, что он будет выполнять весь процесс от начала до конца без прерывания или остановки потока.

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

Как JavaScript может быть однопоточным и неблокирующим одновременно?

Но что означает блокировка?

Неблокирующий

Не существует единого определения блокировки; это просто означает, что в потоке медленно выполняются вещи. Таким образом, неблокирующий означает, что поток не замедляется.

Но подождите, я сказал, что JavaScript работает в одном потоке? И я также сказал, что это не блокирует, что означает быстрое выполнение задачи в стеке вызовов? Но как??? Как насчет запуска таймеров? Петли?

Расслабляться! Скоро узнаем 😉.

Параллельно

Параллелизм означает, что код выполняется одновременно более чем одним потоком.

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

Асинхронный

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

Но в JavaScript есть один поток? Что тогда выполняет этот блокирующий код, позволяя выполняться другим кодам в потоке?

Прежде чем мы продолжим, давайте подытожим вышесказанное.

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

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

Давайте углубимся, давайте перейдем к механизмам выполнения JavaScript, V8, возможно, в нем есть какие-то скрытые потоки, о которых мы не знаем.

Двигатель V8

Движок V8 — это высокопроизводительный механизм выполнения веб-сборки с открытым исходным кодом для JavaScript, написанный Google на C++. Большинство браузеров запускают JavaScript с использованием движка V8, и даже популярная среда выполнения node js также использует его.

Говоря простым языком, V8 — это программа на C++, которая получает код JavaScript, компилирует и выполняет его.

V8 делает две основные вещи;

  • Распределение кучи памяти
  • Контекст выполнения стека вызовов

К сожалению, наши подозрения не оправдались. V8 имеет только один стек вызовов, думайте о стеке вызовов как о потоке.

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

Изображение — Хакер полдень

Поскольку V8 имеет только один стек вызовов, как тогда JavaScript работает параллельно и асинхронно, не блокируя основной поток выполнения?

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

JavaScript запускает каждый код построчно, один за другим (однопоточный). Как и ожидалось, первая строка здесь печатается в консоли, но почему последняя строка печатается перед кодом тайм-аута? Почему процесс выполнения не ждет кода тайм-аута (блокировки), прежде чем продолжить выполнение последней строки?

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

Давайте украдкой заглянем в Исходный код V8 какое-то время.

Чего ждать??!!! В V8 нет функций таймера, нет DOM? Нет событий? Нет АЯКСА?…. Дааааааааааааааааааааааа!!!

События, DOM, таймеры и т. д. не являются частью базовой реализации JavaScript. JavaScript строго соответствует спецификациям Ecma Scripts, и его различные версии часто упоминаются в соответствии со спецификациями Ecma Scripts Specifications (ES X).

Рабочий процесс выполнения

События, таймеры, запросы Ajax предоставляются браузерами на стороне клиента и часто называются веб-API. Именно они позволяют однопоточному JavaScript быть неблокирующим, параллельным и асинхронным! Но как?

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

Стек вызовов

Стек — это структура данных, в которой последний добавленный элемент всегда удаляется из стека первым, вы можете думать о нем как о стеке пластин, в котором только первая пластина, которая была добавлена ​​последней, может быть удалена первой. Стек вызовов — это не что иное, как структура данных стека, в которой соответственно выполняются задачи или код.

Давайте рассмотрим приведенный ниже пример;

Источник — https://youtu.be/8aGhZQkoFbQ

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

Веб-API

Именно здесь выполняется код, который не обрабатывается движком V8, чтобы не «блокировать» основной поток выполнения. Когда стек вызовов встречает функцию веб-API, процесс немедленно передается веб-API, где он выполняется и освобождает стек вызовов для выполнения других операций во время его выполнения.

Вернемся к нашему примеру setTimeout выше;

Когда мы запускаем код, первая строка console.log помещается в стек, и мы почти сразу же получаем наш вывод, по достижении тайм-аута таймеры обрабатываются браузером и не являются частью основной реализации V8, они отправляются вместо этого к веб-API, освобождая стек, чтобы он мог выполнять другие операции.

Пока тайм-аут все еще работает, стек переходит к следующей строке действий и запускает последний файл console.log, что объясняет, почему мы получаем его до вывода таймера. Как только таймер завершится, что-то произойдет. Таймер console.log волшебным образом снова появляется в стеке вызовов!

Как?

Цикл событий

Прежде чем мы обсудим цикл событий, давайте сначала рассмотрим функцию очереди задач.

Вернемся к нашему примеру с тайм-аутом. Как только веб-API завершает выполнение задачи, он не просто автоматически отправляет ее обратно в стек вызовов. Он переходит в очередь задач.

Очередь — это структура данных, которая работает по принципу «первым поступил — первым вышел», поэтому, когда задачи помещаются в очередь, они выходят в том же порядке. Задачи, которые были выполнены веб-API, помещаются в очередь задач, а затем возвращаются в стек вызовов, чтобы распечатать результат.

Но ждать. КАКОГО, КАКОГО, ЧЕРТА, ЦИКЛ СОБЫТИЙ???

Источник — https://youtu.be/8aGhZQkoFbQ

Цикл событий — это процесс, который ожидает, пока стек вызовов очистится, прежде чем отправлять обратные вызовы из очереди задач в стек вызовов. Как только стек очищается, запускается цикл обработки событий, который проверяет очередь задач на наличие доступных обратных вызовов. Если они есть, он помещает их в стек вызовов, ждет, пока стек вызовов снова не очистится, и повторяет тот же процесс.

Источник — https://www.quora.com/How-does-an-event-loop-work/answer/Timothy-Maxwell.

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

Вывод

Хотя это очень простое введение, концепция асинхронного программирования в JavaScript дает достаточно информации, чтобы четко понять, что происходит под капотом и как JavaScript может работать одновременно и асинхронно всего с одним потоком.

JavaScript всегда доступен по требованию, и если вам интересно его изучить, я бы посоветовал вам ознакомиться с этим Удеми курс.