0 added
0 removed
Original
2026-01-01
Modified
2026-02-21
1
<p><em>Автор текста - Александр Кузьмин, ведущий программист и руководитель отдела клиентской разработки компании<a>IT-Park</a>, имеющий за плечами десятилетний опыт во frontend. Передаем ему слово.</em></p>
1
<p><em>Автор текста - Александр Кузьмин, ведущий программист и руководитель отдела клиентской разработки компании<a>IT-Park</a>, имеющий за плечами десятилетний опыт во frontend. Передаем ему слово.</em></p>
2
<p><a>В прошлый раз</a>я рассказал об устройстве event loop в JavaScript, принципах работы и подводных камнях, с которыми сталкивается разработчик. Сегодня рассмотрим основные практики, позволяющие использовать возможности событийного цикла на полную, помимо<em>setTimeout</em>. Но сначала расскажу о том, как не стоит делать.</p>
2
<p><a>В прошлый раз</a>я рассказал об устройстве event loop в JavaScript, принципах работы и подводных камнях, с которыми сталкивается разработчик. Сегодня рассмотрим основные практики, позволяющие использовать возможности событийного цикла на полную, помимо<em>setTimeout</em>. Но сначала расскажу о том, как не стоит делать.</p>
3
<p>В первой части статьи я нарочно использовал синтаксис стандарта ECMAScript 5. Здесь применю современный стандарт, поскольку он больше подходит предмету нашего разговора.</p>
3
<p>В первой части статьи я нарочно использовал синтаксис стандарта ECMAScript 5. Здесь применю современный стандарт, поскольку он больше подходит предмету нашего разговора.</p>
4
<p>Такое название было придумано не зря: блокирующими называют операции, которые не дают контексту выполнения завершиться в адекватное время, что влечет за собой блокирование очереди контекстов. Рассмотрим классический пример - циклы.</p>
4
<p>Такое название было придумано не зря: блокирующими называют операции, которые не дают контексту выполнения завершиться в адекватное время, что влечет за собой блокирование очереди контекстов. Рассмотрим классический пример - циклы.</p>
5
<p>Взгляните на код:</p>
5
<p>Взгляните на код:</p>
6
const arr = []; for (let i = 0; i < 10000; i++) { arr[i] = Math.pow(2, i); }<p>Вне контекста клиента он выглядит вполне операбельным. Но давайте разберемся, что здесь происходит.</p>
6
const arr = []; for (let i = 0; i < 10000; i++) { arr[i] = Math.pow(2, i); }<p>Вне контекста клиента он выглядит вполне операбельным. Но давайте разберемся, что здесь происходит.</p>
7
<p>Во-первых, заключенный в фигурные скобки цикла код выполняется в том же контексте исполнения, в котором цикл объявлен. Это значит, что контекст не завершится, пока не будут обработаны все десять тысяч итераций цикла.</p>
7
<p>Во-первых, заключенный в фигурные скобки цикла код выполняется в том же контексте исполнения, в котором цикл объявлен. Это значит, что контекст не завершится, пока не будут обработаны все десять тысяч итераций цикла.</p>
8
<p>Во-вторых, несмотря на вызов функции внутри цикла, возвращаемый ею результат записывается в объявленный выше массив через операцию присвоения по индексу.</p>
8
<p>Во-вторых, несмотря на вызов функции внутри цикла, возвращаемый ею результат записывается в объявленный выше массив через операцию присвоения по индексу.</p>
9
<p>А если вместо Math.pow () мы бы использовали функцию, которая делает запрос на сервер и возвращает полученное значение? Это выглядело бы так:</p>
9
<p>А если вместо Math.pow () мы бы использовали функцию, которая делает запрос на сервер и возвращает полученное значение? Это выглядело бы так:</p>
10
async function fillArray() { const arr = []; for (var i = 0; i < 10000; i++) { arr[i] = await getData(i); } }<p>Конструкция async-await появилась в стандарте ECMAScript 2015, и само слово await четко дает понять, что мы должны дождаться выполнения асинхронной функции getData (), не помещать ее в event loop, а выполнить прямо здесь и записать результат в i-й элемент массива.</p>
10
async function fillArray() { const arr = []; for (var i = 0; i < 10000; i++) { arr[i] = await getData(i); } }<p>Конструкция async-await появилась в стандарте ECMAScript 2015, и само слово await четко дает понять, что мы должны дождаться выполнения асинхронной функции getData (), не помещать ее в event loop, а выполнить прямо здесь и записать результат в i-й элемент массива.</p>
11
<p>Это значит, что при вызове<em>асинхронной</em>функции fillArray () она попадет в очередь контекстов и будет исполнена. Но, как мы уже знаем, следующий контекст будет ждать, пока текущий завершится. Все пользовательские события, таймеры и прочие помещаемые в очередь контексты будут ждать, пока не пройдут десять тысяч запросов к серверу.</p>
11
<p>Это значит, что при вызове<em>асинхронной</em>функции fillArray () она попадет в очередь контекстов и будет исполнена. Но, как мы уже знаем, следующий контекст будет ждать, пока текущий завершится. Все пользовательские события, таймеры и прочие помещаемые в очередь контексты будут ждать, пока не пройдут десять тысяч запросов к серверу.</p>
12
<p>Сравните этот код со следующим:</p>
12
<p>Сравните этот код со следующим:</p>
13
async function fillArray() { const arr = []; for (var i = 0; i < 10000; i++) { arr.push(await getData(i)); } }<p>Что здесь происходит? Мы избавились от операции присвоения внутри цикла, и на каждую итерацию arr.push () будет помещен в event loop, как и getData () для него. По мере готовности данных будет вызываться push, при этом блокирования очереди не произойдет, так как контексты теперь оказываются в событийном цикле. Ниже представлен JSFiddle, в котором нам придется немного забежать вперед, чтобы продемонстрировать саму концепцию, о которой идет речь (откройте консоль, чтобы видеть результат).</p>
13
async function fillArray() { const arr = []; for (var i = 0; i < 10000; i++) { arr.push(await getData(i)); } }<p>Что здесь происходит? Мы избавились от операции присвоения внутри цикла, и на каждую итерацию arr.push () будет помещен в event loop, как и getData () для него. По мере готовности данных будет вызываться push, при этом блокирования очереди не произойдет, так как контексты теперь оказываются в событийном цикле. Ниже представлен JSFiddle, в котором нам придется немного забежать вперед, чтобы продемонстрировать саму концепцию, о которой идет речь (откройте консоль, чтобы видеть результат).</p>
14
Смотрите код на<a>JSFiddle</a>.<p>Давайте чуть изменим наш код:</p>
14
Смотрите код на<a>JSFiddle</a>.<p>Давайте чуть изменим наш код:</p>
15
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);<p>Этот код поэлементно преобразует входной массив, выполняя для каждого из его элементов функцию-преобразователь, не блокируя очередь контекстов. На самом деле такая функция уже есть в языке - это метод map () объекта Array. Она работает заметно быстрее, поскольку реализована на стороне интерпретатора, но алгоритмически делает ровно то же самое. И взаимодействие с event loop у нее такое же.</p>
15
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);<p>Этот код поэлементно преобразует входной массив, выполняя для каждого из его элементов функцию-преобразователь, не блокируя очередь контекстов. На самом деле такая функция уже есть в языке - это метод map () объекта Array. Она работает заметно быстрее, поскольку реализована на стороне интерпретатора, но алгоритмически делает ровно то же самое. И взаимодействие с event loop у нее такое же.</p>
16
<p><strong>Куда важнее сама концепция: стараться писать код так, чтобы тяжелые вычисления помещались в event loop и уже по готовности оказывались на месте.</strong></p>
16
<p><strong>Куда важнее сама концепция: стараться писать код так, чтобы тяжелые вычисления помещались в event loop и уже по готовности оказывались на месте.</strong></p>
17
<p>Так мы расчищаем очередь, позволяя выполняться пользовательским событиям и, как ни странно, анимациям. Да, блокирующие операции влияют на их выполнение - даже CSS-анимации используют тот же поток вычислений, что и JavaScript.</p>
17
<p>Так мы расчищаем очередь, позволяя выполняться пользовательским событиям и, как ни странно, анимациям. Да, блокирующие операции влияют на их выполнение - даже CSS-анимации используют тот же поток вычислений, что и JavaScript.</p>
18
<p>Теперь, вооружившись базовым пониманием этого подхода, мы сможем перейти к следующей теме.</p>
18
<p>Теперь, вооружившись базовым пониманием этого подхода, мы сможем перейти к следующей теме.</p>
19
<p>Событийный цикл не зря так называется - он ожидает событие и после него помещает в очередь исполнения соответствующий контекст. Выше, в примере с JSFIddle, мы уже эмулировали отложенное получение данных при помощи нативного для стандарта ES2015 объекта Promise. Это один из способов управления контекстом исполнения, и ниже мы рассмотрим его подробнее.</p>
19
<p>Событийный цикл не зря так называется - он ожидает событие и после него помещает в очередь исполнения соответствующий контекст. Выше, в примере с JSFIddle, мы уже эмулировали отложенное получение данных при помощи нативного для стандарта ES2015 объекта Promise. Это один из способов управления контекстом исполнения, и ниже мы рассмотрим его подробнее.</p>
20
<p>Созданием объекта Promise мы даем обещание: как только произойдет какое-либо событие, мы его разрешим и запустим обработку данных. Технически это выглядит так:</p>
20
<p>Созданием объекта Promise мы даем обещание: как только произойдет какое-либо событие, мы его разрешим и запустим обработку данных. Технически это выглядит так:</p>
21
const prom = new Promise((resolve, reject) => { /* ... */ if (anyCondition) { resolve(10); } else { reject(12); } }); prom.then( (result) => console.log(result), (result) => console.log(result), );<p>Метод then () принимает две функции в качестве аргументов: первая выполнится при вызове resolve (), а вторая - при вызове reject (). В общем случае resolve () может вызываться при срабатывании любого события.</p>
21
const prom = new Promise((resolve, reject) => { /* ... */ if (anyCondition) { resolve(10); } else { reject(12); } }); prom.then( (result) => console.log(result), (result) => console.log(result), );<p>Метод then () принимает две функции в качестве аргументов: первая выполнится при вызове resolve (), а вторая - при вызове reject (). В общем случае resolve () может вызываться при срабатывании любого события.</p>
22
<p>Таким образом, если anyCondition === true, в консоль выведется 10. Если же нет - 12. Но мы можем написать то же самое и по-другому:</p>
22
<p>Таким образом, если anyCondition === true, в консоль выведется 10. Если же нет - 12. Но мы можем написать то же самое и по-другому:</p>
23
prom .then((result) => console.log(result)) .catch((result) => console.log(result));<p>Все благодаря тому, что then и catch возвращают исходный объект Promise. К тому же это работает и так:</p>
23
prom .then((result) => console.log(result)) .catch((result) => console.log(result));<p>Все благодаря тому, что then и catch возвращают исходный объект Promise. К тому же это работает и так:</p>
24
prom .then((result) => result + 10) .then((result) => result * 2).then((result) => console.log(result)).catch((result) => console.log(result));<p>В результате выполнения этого кода в консоль выведется число 40. Поскольку попавший в функцию resolve () результат сохраняется внутри объекта Promise, который мы создали, и принимает значения после выполнения функций, переданных в качестве аргументов в then () по мере путешествия по цепочке. То есть во второй функции result будет результатом выполнения предыдущей. Можете взять пример ниже и поэкспериментировать с ним:</p>
24
prom .then((result) => result + 10) .then((result) => result * 2).then((result) => console.log(result)).catch((result) => console.log(result));<p>В результате выполнения этого кода в консоль выведется число 40. Поскольку попавший в функцию resolve () результат сохраняется внутри объекта Promise, который мы создали, и принимает значения после выполнения функций, переданных в качестве аргументов в then () по мере путешествия по цепочке. То есть во второй функции result будет результатом выполнения предыдущей. Можете взять пример ниже и поэкспериментировать с ним:</p>
25
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; });<p>Но мы не просто так играем с этими цепочками. Они работают напрямую через event loop и в общем случае могут содержать внутри достаточно большие и тяжелые вычисления. Разбивая их на атомарные операции, мы оставляем пространство для выполнения обработчиков пользовательских событий. Чтобы вам было проще понять механизм работы Promise, вот<a>ссылка на Fiddle</a>, где реализован объект, в ключевых моментах повторяющий функциональность Promise.</p>
25
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; });<p>Но мы не просто так играем с этими цепочками. Они работают напрямую через event loop и в общем случае могут содержать внутри достаточно большие и тяжелые вычисления. Разбивая их на атомарные операции, мы оставляем пространство для выполнения обработчиков пользовательских событий. Чтобы вам было проще понять механизм работы Promise, вот<a>ссылка на Fiddle</a>, где реализован объект, в ключевых моментах повторяющий функциональность Promise.</p>
26
Пример полностью смотрите<a>здесь</a>.<p>Он не зря зовется высокоуровневым - в основе асинхронности JavaScript лежит все тот же setTimeout, но это лишь самый простой подход к асинхронной обработке данных с использованием event loop. Существует целый ряд библиотек, реализующих более сложные механизмы: RxJS, Bacon.js и им подобные.</p>
26
Пример полностью смотрите<a>здесь</a>.<p>Он не зря зовется высокоуровневым - в основе асинхронности JavaScript лежит все тот же setTimeout, но это лишь самый простой подход к асинхронной обработке данных с использованием event loop. Существует целый ряд библиотек, реализующих более сложные механизмы: RxJS, Bacon.js и им подобные.</p>
27
<p>На основе приведенного выше аналога Promise можно построить свой вариант такого решения. Например, сделать отложенный вызов функции promised, что позволит отделить декларацию цепочки обработки от фактического вызова в результате Ajax-запроса. Можно добавить различные обработчики, в частности, привычный для библиотеки Rx flatMap, который возвращает другой объект Promise внутри той же цепочки.</p>
27
<p>На основе приведенного выше аналога Promise можно построить свой вариант такого решения. Например, сделать отложенный вызов функции promised, что позволит отделить декларацию цепочки обработки от фактического вызова в результате Ajax-запроса. Можно добавить различные обработчики, в частности, привычный для библиотеки Rx flatMap, который возвращает другой объект Promise внутри той же цепочки.</p>
28
<p>Этот подход зовется функциональным реактивным программированием (FRP). Основное правило, которое он преследует: функции должны быть атомарными - выполнять ровно одну задачу и оставаться неделимыми. Чем меньше и короче функция, тем быстрее она выполняется, гарантируя чистоту и доступность очереди контекстов для пользовательских событий.</p>
28
<p>Этот подход зовется функциональным реактивным программированием (FRP). Основное правило, которое он преследует: функции должны быть атомарными - выполнять ровно одну задачу и оставаться неделимыми. Чем меньше и короче функция, тем быстрее она выполняется, гарантируя чистоту и доступность очереди контекстов для пользовательских событий.</p>
29
<p>В этой и <a>первой статье</a>мы рассмотрели понятие асинхронности в JavaScript и области видимости, разобрались в блокирующих операциях и управлении event loop с помощью Promise. Надеемся, вам было полезно!</p>
29
<p>В этой и <a>первой статье</a>мы рассмотрели понятие асинхронности в JavaScript и области видимости, разобрались в блокирующих операциях и управлении event loop с помощью Promise. Надеемся, вам было полезно!</p>