Цикл событий в JavaScript или как работает асинхронность


14-08-2019
Денис Л.
JavaScript
5
336
Цикл событий в JavaScript или как работает асинхронность

Для рассмотрения такого вопроса как стек событий и асинхронность, давайте начнём с азов. JavaScript - это однопоточная среда выполнения (run time) и это означает, что стек вызовов у него один. Говоря языком определений, JavaScript - однопоточный неблокирующий асинхронный параллельный язык. И он может делать только одну задачу за одну единицу времени. Стек вызовов - это структура данных, которая хранит информацию о том, где мы сейчас в программе находимся. Как только мы начинаем выполнять функцию, мы кладём её в стек. Как только из функции выходим, то из стека её тоже убираем, т.е. элементы добавляются и убираются только сверху.

Для работы в браузере JavaScript использует движок. Например, в Chrome используется движок V8. И как ни странно, в исходном коде V8 нет таких функций как setTimeout или DOM или XMLHttpRequest... Для чего я сейчас говорю об этом? Поймёте чуть позже )) Кстати, отсутствие данных функций в исходном коде - это на первый взгляд довольно странно. Но, покопавшись чуть глубже, вспоминаешь, что существует Web API. Это - расширения браузера, такие как DOM, Ajax, setTimeout, геолокация, видео и прочее. Также есть event loop (цикл событий).

Браузер выполняет работу в стеке. Кстати, Вы наверное слышали про переполнение стека. Посмотрите на следующий пример:

function foo() {
    return foo();
}
foo();

Функция foo() вызывает функцию foo() внутри себя снова и снова, пока не достигается предел стека (в Chrome предел стека равен 16 000, после чего Chrome очищает весь стек, чтобы всё напрочь на зависло). При переполнении стека вызывается ошибка: "Maximum call stack size exceeded".

Если call stack имеет для выполнения что-то сейчас, то браузер не может делать ничего другого. Для решения этой проблемы мы можем использовать асинхронные колбэки. Запускаем код и callback выполняется по его окончании. Но вообще, мы можем делать что-то параллельно в скрипте, за счёт того, что браузер - это не только среда выполнения, но с помощью Web API мы можем делать параллельную работу. Когда какой-то Web API заканчивает свою работу, запланированный callback помещается в очередь (task queue).

А теперь поговорим про цикл событий (event loop). Цикл событий смотрит на стек и на очередь задач. Если стек пуст - он берёт первую задачу в очереди и заталкивает в стек, что заставляет код выполниться. А стек - это часть движка JS (V8).

Сейчас уже можно подойти к пониманию того зачем делаются некоторые странные штуки в асинхронности. Например, Вы наверняка встречались с тем, что иногда рекомендуют вызвать setTimeout с нулевой задержкой. И это делается для того, чтобы что-то выполнить, когда стек очистится. Т.е. иными словами, setTimeout с нулевой задержкой позволяет выполнить что-то после выполнения основного кода. Это довольно удобный приём, который встречается в разработке веб-приложений на JavaScript. Т.е. как только стек пуст, сразу выполняется наш callback.

Собственно, все Web API так и работают. Например, AJAX-запросы. Делаем запрос на адрес и передаём callback. И в это время, код, выполняющий запрос, находится вне движка JavaScript, это другая часть браузера. И как только AJAX-запрос выполняется, можно воспользоваться пустым стеком для выполнения callback-а. По окончанию запроса callback пушится в очередь, откуда подхватывается event loop-ом и отправляется в call stack. Это общая схема для выполнения любых асинхронных запросов.

А теперь давайте подробнее чуть поговорим про callback-и. Callback - это любая функция, которую вызывает другая функция. А ещё есть асинхронный callback, то, что в call stack попадает через очередь и происходит это через определённый промежуток времени. И сейчас между этими двумя вещами будет показана разница.

Как создать асинхронный callback

Давайте посмотрим, как работает обычный, синхронный callback:

[1,2,3,4].forEach(function(i) {
    console.log(i);
})

В примере выше, forEach для массива принимает в качестве аргумента callback - функцию, которая для каждого элемента массива будет вызвана синхронно, в пределах call stek-а.

Чтобы создать асинхронный callback, можно использовать функцию вида:

function asyncForEach(array, cb) {
    array.forEach(function() {
        setTimeout(cb, 0);
    })
}

Функция выше принимает массив и callback. Для каждого элемента в массиве запускается этот callback, в нашем случае, setTimeout с задержкой ноль.

Запускаем функцию так:

asyncForEach([1,2,3,4], function(i) {
    console.log(i);
})

В данном варианте мы наши callback-и отправляем в очередь. И уже после постановки в очередь они запускаются.

Конкретно на данном примере не видно разницу в скорости. Разница будет видна, когда мы будем выполнять внутри функции тяжёлые операции или длительные сетевые запросы. Получается, после каждой очередной операции call stack очищается.

Подписывайтесь на группу в ВКонтакте, чтобы всегда быть в курсе актуальных выпусков Web development blog!

Читайте также:

Защищаем JavaScript от копирования

Node.js: как рекурсивно сжать все изображения на сайте за 1 час

Как запустить Node.js на обычном хостинге