Параллелизм в JavaScript основывается на модели "событийного цикла". Эта модель отличается от модели других языков, например C или Java.
Концепция жизненного цикла программы
В следующей секции, объясняется теоретическая модель. Современные JavaScript движки существенно оптимизируют этот процесс.
Визуальное представление
Стек
Вызов любой функции создает контекст выполнения. При вызове вложенной функции создается новый контекст, а старый сохраняется в специальной структуре. Так формируется стек контекстов.
function f(b) { var a = 12; return a + b + 35; } function g(x) { var m = 4; return f(m * x); } g(21);
Когда вызывается g
, создаётся область видимости, содержащая аргументы g
и локальные переменные. Когда g
вызывает f
, создаётся вторая область видимости и помещается в стэк вперед первой, которая содержит аргументы f
и её локальные переменные. Когда f
возвращает результат, верхний элемент из стека удаляется. Когда g
возвращает результат, ее контекст также удалится, и стэк будет пуст.
Куча
Объекты размещаются в куче. Куча — это ссылка на определённую неструктурированную области памяти.
Очередь событий
Жизненный цикл JavaScript имеет очередь событий. Очередь событий — это список событий, подлежащих обработке. Каждое событие связано с функцией. Когда стэк пуст, событие извлекается из очереди и обрабатывается. Вызывается соответствующая функция (и, таким образом, создаётся начальный элемент стека). Обработка событий заканчивается, когда стек снова становится пустым.
Цикл событий
Модель событийного цикла (event loop
) называется так, потому что отслеживает новые события в цикле:
while(queue.waitForMessage()){ queue.processNextMessage(); }
queue.waitForMessage
ожидает события если очередь пуста.
Запуск до завершения
Каждое событие должно быть полностью обработано, прежде чем начнет обрабатываться следующее. Благодаря этому мы можем точно знать: когда бы и где бы не выполнялась функция – она не может быть упреждающей, и полностью выполнится до того, как начнет выполняться другой код (который может изменять данные, с которыми работает текущая функция). Это отличается от, например, языка программирования C, где функция запускается в потоке. В этом случае она может быть остановлена в любой момент для запуска другого кода в другом потоке.
У данного подхода есть и минусы. Если событие обрабатывается слишком долго, то приложение в это время не имеет возможности обработать действия пользователя (например, скролл, или клик). Браузер может смягчать последствия такой проблемы. Internet Explorer, в таком случае, выводит сообщение "A script on this page is causing Internet Explorer to run slowly" и предлагает завершить обработку слишком тяжелого события. Хорошая практика для предохранения от этого – не создавать события, которые могут выполняться долго, и разбивать большие события на несколько мелких.
Добавление событий в очередь
В браузерах события добавляются в очередь в любое время, если событие произошло, а так же если у него есть обработчик. В случае, если обработчика нет – событие потеряно. Так, клик по элементу, имеющему обработчик события по событию click
– добавит событие в очередь, а если обработчика нет – то и событие в очередь не попадет.
Вызов setTimeout добавит событие в очередь по прошествии времени, указанного во втором аргументе вызова. Если очередь событий на тот момент будет пуста, то событие обработается сразу же, в противном случае, событию функции setTimeout
придется ожидать завершения обработки остальных событий в очереди. Именно поэтому второй аргумент setTimeout
корректно считать не временем, через которое выполнится функция из первого аргумента, а минимальное время, через которое она сможет выполниться.
Связь нескольких потоков между собой
Web Worker или кросс-доменный фрейм имеют свой собственный стэк, кучу и очередь событий. Два отдельный событийных потока могут связываться друг с другом, только через отправку сообщений с помощью метода postMessage.
Этот метод добавляет сообщение
в очередь другого, если он конечно принимает их.
Никогда не блокируется
Очень интересное свойство цикла событий в JavaScript, что в отличие от множества других языков, поток выполнения никогда не блокируется. Обработка I/O обычно осуществляется с помощью событий и функций обратного вызова, поэтому даже когда приложение ожидает запрос от IndexedDB или ответ от XHR, оно может обрабатывать другие процессы, например пользовательский ввод.
Существуют хорошо известные исключения как alert
или синхронный XHR, но считается хорошей практикой избегать их использования.