Generated with Avocode. Generated with Avocode. Generated with Avocode. Group 15 close hat Generated with Avocode. Generated with Avocode. Generated with Avocode. Generated with Avocode. Generated with Avocode. Generated with Avocode. path40

Как управлять event loop в JavaScript. Часть 2

Продолжаем рассказывать об event loop, понимание принципов работы которого важно для карьерного роста middle-разработчика.

Автор текста — Александр Кузьмин, ведущий программист и руководитель отдела клиентской разработки компании IT-Park, имеющий за плечами десятилетний опыт во frontend. Передаем ему слово.

В прошлый раз я рассказал об устройстве event loop в JavaScript, принципах работы и подводных камнях, с которыми сталкивается разработчик. Сегодня рассмотрим основные практики, позволяющие использовать возможности событийного цикла на полную, помимо setTimeout. Но сначала расскажу о том, как не стоит делать.

В первой части статьи я нарочно использовал синтаксис стандарта ECMAScript 5. Здесь применю современный стандарт, поскольку он больше подходит предмету нашего разговора.

Блокирующие операции vs. event loop

Такое название было придумано не зря: блокирующими называют операции, которые не дают контексту выполнения завершиться в адекватное время, что влечет за собой блокирование очереди контекстов. Рассмотрим классический пример — циклы.

Взгляните на код:

const arr = [];

for (let i = 0; i < 10000; i++) {

arr[i] = Math.pow(2, i);

}

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

Во-первых, заключенный в фигурные скобки цикла код выполняется в том же контексте исполнения, в котором цикл объявлен. Это значит, что контекст не завершится, пока не будут обработаны все десять тысяч итераций цикла.

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

А если вместо Math.pow() мы бы использовали функцию, которая делает запрос на сервер и возвращает полученное значение? Это выглядело бы так:

async function fillArray() {

const arr = [];

for (var i = 0; i < 10000; i++) {

        arr[i] = await getData(i);

}

}

Конструкция async-await появилась в стандарте ECMAScript 2015, и само слово await четко дает понять, что мы должны дождаться выполнения асинхронной функции getData(), не помещать ее в event loop, а выполнить прямо здесь и записать результат в i-й элемент массива.

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

Сравните этот код со следующим:

async function fillArray() {

const arr = [];

for (var i = 0; i < 10000; i++) {

       arr.push(await getData(i));

}

}

Что здесь происходит? Мы избавились от операции присвоения внутри цикла, и на каждую итерацию arr.push() будет помещен в event loop, как и getData() для него. По мере готовности данных будет вызываться push, при этом блокирования очереди не произойдет, так как контексты теперь оказываются в событийном цикле. Ниже представлен JSFiddle, в котором нам придется немного забежать вперед, чтобы продемонстрировать саму концепцию, о которой идет речь (откройте консоль, чтобы видеть результат).

Смотрите код на JSFiddle.

Давайте чуть изменим наш код:

function mapArray(incoming = [], func = (x) => x) {

const outcoming = [];

for (var i = 0; i < incoming.length; i++) {

        outcoming.push(func(incoming[i]));

}

return outcoming;

}

const d = mapArray([1,2,3], (x) => x + 1);

Этот код поэлементно преобразует входной массив, выполняя для каждого из его элементов функцию-преобразователь, не блокируя очередь контекстов. На самом деле такая функция уже есть в языке — это метод map() объекта Array. Она работает заметно быстрее, поскольку реализована на стороне интерпретатора, но алгоритмически делает ровно то же самое. И взаимодействие с event loop у нее такое же.

Куда важнее сама концепция: стараться писать код так, чтобы тяжелые вычисления помещались в event loop и уже по готовности оказывались на месте.

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

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

Высокоуровневый подход к управлению event loop

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

JavaScript Promise — отложенное обещание

Созданием объекта Promise мы даем обещание: как только произойдет какое-либо событие, мы его разрешим и запустим обработку данных. Технически это выглядит так:

const prom = new Promise((resolve, reject) => {

/* ... */

if (anyCondition) {

        resolve(10);

} else {

        reject(12);

}

});

prom.then(

(result) => console.log(result),

(result) => console.log(result),

);

Метод then() принимает две функции в качестве аргументов: первая выполнится при вызове resolve(), а вторая — при вызове reject(). В общем случае resolve() может вызываться при срабатывании любого события.

Таким образом, если anyCondition === true, в консоль выведется 10. Если же нет — 12. Но мы можем написать то же самое и по-другому:

prom

.then((result) => console.log(result))

.catch((result) => console.log(result));

Все благодаря тому, что then и catch возвращают исходный объект Promise. К тому же это работает и так:

prom

.then((result) => result + 10)
.then((result) => result * 2)
.then((result) => console.log(result))
.catch((result) => console.log(result));

В результате выполнения этого кода в консоль выведется число 40. Поскольку попавший в функцию resolve() результат сохраняется внутри объекта Promise, который мы создали, и принимает значения после выполнения функций, переданных в качестве аргументов в then() по мере путешествия по цепочке. То есть во второй функции result будет результатом выполнения предыдущей. Можете взять пример ниже и поэкспериментировать с ним:

const prom = new Promise((resolve) => resolve(10));

prom

.then((res) => {

           console.log(res); // 10

           return res + 10;

})

.then((res) => {

            console.log(res); // 20

            return res + 10;

});

Но мы не просто так играем с этими цепочками. Они работают напрямую через event loop и в общем случае могут содержать внутри достаточно большие и тяжелые вычисления. Разбивая их на атомарные операции, мы оставляем пространство для выполнения обработчиков пользовательских событий. Чтобы вам было проще понять механизм работы Promise, вот ссылка на Fiddle, где реализован объект, в ключевых моментах повторяющий функциональность Promise.

Пример полностью смотрите здесь.

Он не зря зовется высокоуровневым — в основе асинхронности JavaScript лежит все тот же setTimeout, но это лишь самый простой подход к асинхронной обработке данных с использованием event loop. Существует целый ряд библиотек, реализующих более сложные механизмы: RxJS, Bacon.js и им подобные.

На основе приведенного выше аналога Promise можно построить свой вариант такого решения. Например, сделать отложенный вызов функции promised, что позволит отделить декларацию цепочки обработки от фактического вызова в результате Ajax-запроса. Можно добавить различные обработчики, в частности, привычный для библиотеки Rx flatMap, который возвращает другой объект Promise внутри той же цепочки.

Этот подход зовется функциональным реактивным программированием (FRP). Основное правило, которое он преследует: функции должны быть атомарными — выполнять ровно одну задачу и оставаться неделимыми. Чем меньше и короче функция, тем быстрее она выполняется, гарантируя чистоту и доступность очереди контекстов для пользовательских событий.

Заключение

В этой и первой статье мы рассмотрели понятие асинхронности в JavaScript и области видимости, разобрались в блокирующих операциях и управлении event loop с помощью Promise. Надеемся, вам было полезно!

Приглашаем на двухгодичную программу «Веб-разработчик PRO», которая включает в себя несколько курсов и затрагивает сферу обучения максимально широко.

Я – Веб-разработчик PRO
С нуля до профессионального веб-разработчика. Вы научитесь верстать сайты и создавать интерфейсы, соберете6 проектов в портфолио и получите востребованную профессию. Расходы за первые полгода курса берет на себя Skillbox. В это время вы посещаете все лекции и воркшопы, прокачиваете навыки, находите себе работу и начинаете получать деньги.
  • Живая обратная связь с преподавателями
  • Неограниченный доступ к материалам курса
  • Стажировка в компаниях-партнёрах
  • Дипломный проект от реального заказчика
  • Гарантия трудоустройства в компании-партнёры для выпускников, защитивших дипломные работы

Комментарии

0
Чтобы оставить комментарий,  авторизуйтесь
Хочешь получать крутые статьи по программированию?
Подпишись на рассылку Skillbox
Новогодняя распродажа курсов