` (например, чтобы сделать элемент DOM видимым после того, как у него есть содержимое). Вы, вероятно, не захотите показывать элемент DOM до того, как у него будет содержимое, поэтому координация должна обеспечивать правильное взаимодействие с упорядочением.
Некоторые сценарии конкуренции *всегда ломаются* (а не просто *иногда*) без скоординированного взаимодействия.
Рассмотрим:
```js
var a, b;
function foo(x) {
a = x * 2;
baz();
}
function bar(y) {
b = y * 2;
baz();
}
function baz() {
console.log(a + b);
}
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
```
В этом примере, независимо от того, срабатывает ли сначала `foo()` или `bar()`, `baz()` всегда будет запускаться слишком рано (либо `a`, либо `b` все еще будут `undefined`), но второй вызов `baz()` будет работать, так как будут доступны и `a`, и `b`.
Существуют разные способы справиться с таким состоянием. Вот один простой способ:
```js
var a, b;
function foo(x) {
a = x * 2;
if (a && b) {
baz();
}
}
function bar(y) {
b = y * 2;
if (a && b) {
baz();
}
}
function baz() {
console.log( a + b );
}
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
```
Условие `if (a && b)` вокруг вызова `baz()` традиционно называется "воротами", потому что мы не уверены, какой порядок `a` и `b` прибудет, но мы ждем оба из них, чтобы попасть туда, прежде чем мы приступим к открытию ворот (вызов `baz()`).
Еще одно условие конкурентного взаимодействия, с которым вы можете столкнуться, иногда называют «гонкой», но правильнее называть его «защелкой». Характеризуется поведением «побеждает только первый». Здесь приемлем недетерминизм, поскольку вы явно говорите, что в «гонке» до финиша может быть только один победитель.
Рассмотрим этот сломанный код:
```js
var a;
function foo(x) {
a = x * 2;
baz();
}
function bar(x) {
a = x / 2;
baz();
}
function baz() {
console.log( a );
}
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
```
Какой бы из них (`foo()` или `bar()`) не срабатывал последним, он не только перезапишет назначенное значение `a` из другого, но также будет дублировать вызов `baz()` (вероятно, нежелательный).
Итак, мы можем согласовать взаимодействие с простой защелкой, чтобы пропускать только первую:
```js
var a;
function foo(x) {
if (a == undefined) {
a = x * 2;
baz();
}
}
function bar(x) {
if (a == undefined) {
a = x / 2;
baz();
}
}
function baz() {
console.log( a );
}
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
```
Условие `if (a == undefined)` допускает только первый из `foo()` или `bar()`, а второй (и любые последующие) вызовы будут просто проигнорированы. Нет ничего хорошего в том, чтобы занять второе место!
**Примечание.** Во всех этих сценариях мы использовали глобальные переменные для упрощения иллюстрации, но в наших рассуждениях здесь нет ничего, что требовало бы этого. Пока рассматриваемые функции могут получить доступ к переменным (через область видимости), они будут работать так, как задумано. Использование переменных с лексической областью видимости (см. название этой серии книг *Scope & Closures*) и фактически глобальных переменных, как в этих примерах, является очевидным недостатком этих форм координации в условиях конкуренции. В следующих нескольких главах мы увидим другие способы координации, которые в этом отношении намного чище.
### Кооперация
Другое выражение координации параллелизма называется «кооперативной конкуренцией» (cooperative concurrency). Здесь основное внимание уделяется не столько взаимодействию через совместное использование значений в областях видимости (хотя это, очевидно, все еще разрешено!). Цель состоит в том, чтобы взять длительный «процесс» и разбить его на шаги или пакеты, чтобы другие конкурирующие «процессы» имели возможность вставить свои операции в очереди цикла событий.
Например, рассмотрим обработчик ответа Ajax, которому необходимо просмотреть длинный список результатов для преобразования значений. Мы будем использовать `Array#map(..)`, чтобы сделать код короче:
```js
var res = [];
// `response(..)` получает массив результатов от Ajax-вызова
function response(data) {
// добавляем в существующий массив `res`
res = res.concat(
// создаем новый преобразованный массив со всеми удвоенными значениями `data`
data.map( function(val){
return val * 2;
} )
);
}
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
```
Если `"http://some.url.1"` сначала получит свои результаты, весь список будет сразу отображен в `res`. Если это несколько тысяч или меньше записей, это, как правило, не имеет большого значения. Но если это, скажем, 10 миллионов записей, это может занять некоторое время (несколько секунд на мощном ноутбуке, намного дольше на мобильном устройстве и т.д.).
Во время выполнения такого «процесса» на странице не может происходить ничего другого, включая другие вызовы `response(..)`, обновления пользовательского интерфейса, даже пользовательские события, такие как прокрутка, ввод текста, нажатие кнопки и тому подобное. Это довольно болезненно.
Таким образом, чтобы сделать более кооперативно конкурентную систему, более дружелюбную и не перегружающую очередь цикла событий, вы можете обрабатывать эти результаты асинхронными пакетами, после того как каждый из них «уступает» обратно в цикл событий, чтобы позволить произойти другим ожидающим событиям.
Вот очень простой подход:
```js
var res = [];
// `response(..)` получает массив результатов от Ajax-вызова
function response(data) {
// давайте просто сделаем 1000 за раз
var chunk = data.splice( 0, 1000 );
// добавляем в существующий массив `res`
res = res.concat(
// создаем новый преобразованный массив со всеми удвоенными значениями `chunk`
chunk.map( function(val){
return val * 2;
} )
);
// осталось что-нибудь обработать?
if (data.length > 0) {
// асинхронно планируем следующую партию
setTimeout( function(){
response( data );
}, 0 );
}
}
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
```
Мы обрабатываем набор данных блоками максимального размера по 1000 элементов. Поступая таким образом, мы обеспечиваем кратковременный «процесс», даже если это означает гораздо больше последующих «процессов», поскольку чередование с очередью цикла событий даст нам гораздо более отзывчивый (производительный) сайт/приложение.
Конечно, мы не координируем порядок взаимодействия любого из этих «процессов», поэтому порядок результатов в `res` не будет предсказуемым. Если бы требовался порядок, вам нужно было бы использовать методы взаимодействия, подобные тем, которые мы обсуждали ранее, или те, которые мы рассмотрим в следующих главах этой книги.
Мы используем `setTimeout(..0)` (хак) для асинхронного планирования, что в основном просто означает «вставить эту функцию в конец текущей очереди цикла событий».
**Примечание:** `setTimeout(..0)` технически не вставляет элемент непосредственно в очередь цикла событий. Таймер вставит событие при следующей возможности. Например, два последовательных вызова `setTimeout(..0)` не будут строго гарантированы для обработки в порядке вызова, поэтому *возможно* увидеть различные условия, такие как дрейф таймера, когда порядок таких событий не предсказуем. В Node.js аналогичный подход — `process.nextTick(..)`. Несмотря на то, насколько удобным (и, как правило, более производительным) это было бы, не существует единого прямого способа (по крайней мере, пока) во всех средах для обеспечения асинхронного упорядочения событий. Мы рассмотрим эту тему более подробно в следующем разделе.
## Задачи
Начиная с ES6, появилась новая концепция над очередью цикла событий, которая называется "Очередь задач". Скорее всего, вам придется столкнуться с асинхронным поведением промисов (см. главу 3).
К сожалению, на данный момент это механизм без открытого API, поэтому демонстрация его немного сложнее. Так что нам нужно просто описать его концептуально, чтобы, когда мы будем обсуждать асинхронное поведение с промисами в главе 3, вы поняли, как эти действия планируются и обрабатываются.
Итак, лучший способ думать об этом, который я нашел, состоит в том, что «Очередь задач» — это очередь, свисающая с конца каждого такта в очереди цикла событий. Некоторые подразумеваемые асинхронные действия, которые могут произойти во время такта цикла событий, не приведут к добавлению нового события в очередь цикла событий, а вместо этого добавят элемент (также известный как задача) в конец очереди задач текущего такта.
Это все равно, что сказать: «О, вот еще одна вещь, которую мне нужно сделать *позже*, но убедитесь, что это произойдет прямо сейчас, прежде чем что-либо еще может произойти».
Или, если использовать метафору: очередь цикла событий подобна аттракциону в парке развлечений, где, как только вы закончите кататься, вам нужно вернуться в конец очереди, чтобы прокатиться снова. Но очередь задач похожа на то, как если бы вы закончили поездку, но затем встали в очередь и сразу же вернулись.
Задача также может привести к добавлению дополнительных задач в конец одной и той же очереди. Таким образом, теоретически возможно, что «цикл» задач (задача, которая продолжает добавлять другую задачу и т.д.) может вращаться бесконечно, лишая программу возможности перейти к следующему такту цикла событий. Концептуально это было бы почти так же, как простое выражение длительного или бесконечного цикла (например, `while (true) ..`) в вашем коде.
Задачи похожи на дух хака `setTimeout(..0)`, но реализованы таким образом, чтобы иметь гораздо более четко определенный и гарантированный порядок: **позже, но как можно скорее**.
Давайте представим API для планирования задач (напрямую, без хаков) и назовем его `schedule(..)`.
```js
console.log( "A" );
setTimeout( function(){
console.log( "B" );
}, 0 );
// теоретический "API задачи"
schedule( function(){
console.log( "C" );
schedule( function(){
console.log( "D" );
} );
} );
```
Вы можете ожидать, что это напечатает `A B C D`, но вместо этого будет напечатано `A C D B`, потому что задачи происходят в конце текущего такта цикла событий, и таймер срабатывает, чтобы запланировать *следующий* такт цикла событий (если доступно!).
В главе 3 мы увидим, что асинхронное поведение промисов основано на задачах, поэтому важно четко понимать, как это связано с поведением цикла обработки событий.
## Порядок операторов
Порядок, в котором мы выражаем операторы в нашем коде, не обязательно совпадает с порядком, в котором JS-движок будет их выполнять. Это может показаться довольно странным утверждением, поэтому мы кратко рассмотрим его.
Но прежде чем мы это сделаем, мы должны кое-что предельно ясно уяснить: правила/грамматика языка (см. название этой серии книг *Типы и грамматика*) диктуют очень предсказуемое и надежное поведение для упорядочения операторов с точки зрения программы. Итак, то, что мы собираемся обсудить, — это **не то, что вы когда-либо сможете наблюдать** в своей JS-программе.
**Предупреждение:** Если вы когда-либо сможете *наблюдать* переупорядочивание операторов компилятора, как мы собираемся проиллюстрировать, это будет явным нарушением спецификации, и это, несомненно, будет связано с ошибкой в движке JS. в вопросе - тот, который должен быть немедленно сообщен и исправлен! Но гораздо чаще вы *подозреваете* что-то сумасшедшее происходит в движке JS, когда на самом деле это просто ошибка (вероятно, "состояние гонки"!) в вашем собственном коде - так что сначала смотрите туда, и снова и снова . Отладчик JS, использующий точки останова и последовательно выполняющий код, станет вашим самым мощным инструментом для обнаружения таких ошибок в *вашем коде*.
Рассмотреть возможность:
```js
var a, b;
a = 10;
b = 30;
a = a + 1;
b = b + 1;
console.log( a + b ); // 42
```
Этот код не имеет выраженной асинхронности (за исключением редкого «консольного» асинхронного ввода-вывода, который обсуждался ранее!), поэтому наиболее вероятным предположением будет то, что он будет обрабатывать строку за строкой сверху вниз.
Но *возможно*, что движок JS после компиляции этого кода (да, JS скомпилирован — см. название *Scope & Closures* этой серии книг!) может найти возможности для более быстрого запуска вашего кода, перестраивая (безопасно) порядок этих выражений. По сути, пока вы не можете наблюдать за изменением порядка, все в порядке.
Например, движок может решить, что на самом деле быстрее выполнить такой код:
```js
var a, b;
a = 10;
a++;
b = 30;
b++;
console.log( a + b ); // 42
```
Или это:
```js
var a, b;
a = 11;
b = 31;
console.log( a + b ); // 42
```
Или даже:
```js
// так как `a` и `b` больше не используются, мы можем
// встроить их и даже не нуждаться в них!
console.log( 42 ); // 42
```
Во всех этих случаях движок JS выполняет безопасную оптимизацию во время компиляции, так как конечный *наблюдаемый* результат будет одним и тем же.
Но вот сценарий, в котором эти конкретные оптимизации были бы небезопасными и, следовательно, не могли бы быть разрешены (конечно, нельзя сказать, что они вообще не оптимизированы):
```js
var a, b;
a = 10;
b = 30;
// нам нужны `a` и `b` в их предварительно увеличенном состоянии!
console.log( a * b ); // 300
a = a + 1;
b = b + 1;
console.log( a + b ); // 42
```
Другие примеры, когда переупорядочивание компилятора может создать наблюдаемые побочные эффекты (и, следовательно, должно быть запрещены), включают в себя такие вещи, как любой вызов функции с побочными эффектами (даже и особенно функции-получатели) или прокси-объекты ES6 (см. заголовок *ES6 & Beyond* статьи эту серию книг).
Рассмотрим возможность:
```js
function foo() {
console.log( b );
return 1;
}
var a, b, c;
// Синтаксис литерала геттера ES5.1
c = {
get bar() {
console.log( a );
return 1;
}
};
a = 10;
b = 30;
a += foo(); // 11
b += c.bar; // 31
console.log( a + b ); // 42
```
Если бы не операторы `console.log(..)` в этом фрагменте (просто используемые как удобная форма наблюдаемого побочного эффекта для иллюстрации), движок JS, вероятно, мог бы свободно, если бы захотел (кто знает, захочет ли!?), изменить порядок кода:
```js
// ...
a = 10 + foo();
b = 30 + c.bar;
// ...
```
В то время как семантика JS, к счастью, защищает нас от *наблюдаемых* кошмаров, которым может угрожать переупорядочивание операторов компилятора, по-прежнему важно понимать, насколько незначительна связь между способом создания исходного кода (сверху вниз) и как он работает после компиляции.
Переупорядочивание операторов компилятора — это почти микрометафора конкурентности и взаимодействия. В целом такая осведомленность может помочь вам лучше понять проблемы потока асинхронного кода JS.
## Обзор
Программа JavaScript (практически) всегда разбивается на две или более частей, где первая часть выполняется *сейчас*, а следующая часть выполняется *позже*, в ответ на событие. Несмотря на то, что программа выполняется по частям, все они имеют одинаковый доступ к области действия и состоянию программы, поэтому каждое изменение состояния выполняется поверх предыдущего состояния.
Всякий раз, когда есть события для запуска, *цикл событий* выполняется до тех пор, пока очередь не станет пустой. Каждая итерация цикла событий — это "такт". Взаимодействие с пользователем, ввод-вывод и таймеры помещают события в очередь событий.
В любой момент времени из очереди может быть обработано только одно событие. Во время выполнения события оно может прямо или косвенно вызывать одно или несколько последующих событий.
Конкурентность — это когда две или более цепочек событий чередуются во времени, так что с точки зрения высокого уровня кажется, что они выполняются *одновременно* (даже если в любой момент обрабатывается только одно событие).
Часто бывает необходимо выполнить какую-то форму координации взаимодействия между этими конкурирующими «процессами» (в отличие от процессов операционной системы), например, чтобы обеспечить упорядоченность или предотвратить «состояние гонки». Эти «процессы» также могут *взаимодействовать*, разбивая себя на более мелкие фрагменты и допуская чередование других «процессов».
================================================
FILE: async & performance/ch2.md
================================================
# Вы не знаете JS: Асинхронность и Выполнение
# Глава 2: Колбеки
В главе 1 мы рассмотрели терминологию и концепции асинхронного программирования в JavaScript. Наше внимание сосредоточено на понимании однопоточной (по одному за раз) очереди цикла событий, которая управляет всеми «событиями» (вызовами асинхронных функций). Мы также исследовали различные способы, которыми шаблоны конкурентности объясняют отношения (если они есть!) между *одновременно* запущенными цепочками событий или «процессами» (задачами, вызовами функций и т.д.).
Во всех наших примерах в главе 1 функция использовалась как отдельная, неделимая единица операций, при этом внутри функции операторы выполняются в предсказуемом порядке (выше уровня компилятора!), но на уровне упорядочения функций события (также известные как асинхронные вызовы функций) могут происходить в различных порядках.
Во всех этих случаях функция действует как «обратный вызов», потому что она служит целью цикла обработки событий для «обратного вызова» программы всякий раз, когда обрабатывается этот элемент в очереди.
Как вы, несомненно, заметили, обратные вызовы являются наиболее распространенным способом выражения и управления асинхронностью в JS-программах. Действительно, обратный вызов является наиболее фундаментальным асинхронным шаблоном в языке.
Бесчисленные JS-программы, даже очень изощренные и сложные, были написаны только на основе коллбеков (конечно, с использованием шаблонов параллелизма, которые мы рассмотрели в главе 1). Функция обратного вызова — это асинхронная рабочая лошадка для JavaScript, и она достойно выполняет свою работу.
За исключением... обратные вызовы не лишены недостатков. Многие разработчики воодушевлены *обещанием* (каламбур!) улучшенных асинхронных шаблонов. Но невозможно эффективно использовать любую абстракцию, если вы не понимаете, что она абстрагирует и почему.
В этой главе мы подробно рассмотрим пару из них, чтобы понять, почему необходимы и желательны более сложные асинхронные шаблоны (рассматриваемые в последующих главах этой книги).
## Продолжения
Давайте вернемся к примеру с асинхронным обратным вызовом, с которого мы начали в главе 1, но позвольте мне немного изменить его, чтобы проиллюстрировать один момент:
```js
// A
ajax( "..", function(..){
// C
} );
// B
```
`// A` и `// B` представляют первую половину программы (она же *сейчас*), а `//C` отмечает вторую половину программы (она же *позже*). Первая половина выполняется сразу, а потом идет "пауза" неопределенной длины. В какой-то момент в будущем, если вызов Ajax завершится, программа продолжит с того места, где остановилась, и *продолжит* со второй половиной.
Другими словами, функция обратного вызова оборачивает или инкапсулирует *продолжение* программы.
Сделаем код еще проще:
```js
// A
setTimeout( function(){
// C
}, 1000 );
// B
```
Остановитесь на мгновение и спросите себя, как бы вы описали (кому-то другому, менее осведомленному о том, как работает JS) поведение этой программы. Давай, попробуй вслух. Это хорошее упражнение, которое поможет моим следующим пунктам стать более осмысленными.
Большинство читателей сейчас, вероятно, подумали или сказали что-то вроде: «Выполните A, затем установите тайм-аут на 1000 миллисекунд, затем, как только это сработает, выполните C». Насколько точно было ваше толкование?
Возможно, вы поймали себя на том, что сами отредактировали: «Выполните А, установите тайм-аут на 1000 миллисекунд, затем выполните B, затем, когда истечет тайм-аут, сделайте С». Это более точно, чем первая версия. Вы можете заметить разницу?
Несмотря на то, что вторая версия является более точной, обе версии не могут объяснить этот код таким образом, чтобы наш мозг соответствовал коду, а код — движку JS. Это отключение одновременно тонкое и монументальное, и оно лежит в основе понимания недостатков обратных вызовов как асинхронного выражения и управления.
Как только мы вводим одно продолжение (или несколько десятков, как многие программы!) в виде функции обратного вызова, мы позволяем образоваться расхождению между тем, как работает наш мозг, и тем, как будет работать код. Каждый раз, когда эти двое расходятся (и это далеко не единственное место, где это происходит, как я уверен, вы знаете!), мы сталкиваемся с неизбежным фактом, что наш код становится труднее понимать, анализировать, отлаживать и поддерживать.
## Последовательный мозг
Я почти уверен, что большинство из вас, читатели, слышали, как кто-то сказал (даже сами заявили): «Я многозадачный». Эффекты от попытки действовать в качестве многозадачного человека варьируются от забавных (например, глупая детская игра «погладь голову-потирай-живот») до обыденных (жуй жвачку на ходу) и откровенно опасных (текстовые сообщения за рулем).
Но многозадачны ли мы? Можем ли мы действительно совершать два сознательных, преднамеренных действия одновременно и думать/рассуждать о них обоих в один и тот же момент? Есть ли у нашего самого высокого уровня функциональности мозга параллельная многопоточность?
Ответ может вас удивить: **вероятно, нет.**
На самом деле наш мозг просто не так устроен. Мы гораздо больше занимаемся одиночными делами, чем многие из нас (особенно личности типа А!) хотели бы признать. На самом деле мы можем думать только об одном в любой данный момент.
Я не говорю обо всех наших непроизвольных, подсознательных, автоматических функциях мозга, таких как сердцебиение, дыхание и моргание век. Все это жизненно важные задачи для нашей устойчивой жизни, но мы намеренно не выделяем для них никаких умственных способностей. К счастью, пока мы одержимы проверкой ленты социальных сетей в 15-й раз за три минуты, наш мозг продолжает выполнять все эти важные задачи в фоновом режиме (потоки!)
Вместо этого мы говорим о любой задаче, которая находится в центре нашего внимания в данный момент. Для меня это написание текста в этой книге прямо сейчас. Выполняю ли я какую-либо другую функцию мозга более высокого уровня точно в этот же момент? Нет, не совсем. Я отвлекаюсь быстро и легко — несколько десятков раз в этих последних парах абзацев!
Когда мы *имитируем* многозадачность, например, пытаемся что-то напечатать во время разговора с другом или членом семьи по телефону, на самом деле мы, скорее всего, действуем как быстрые переключатели контекста. Другими словами, мы переключаемся между двумя или более задачами в быстрой последовательности, *одновременно* продвигаясь по каждой задаче маленькими, быстрыми порциями. Мы делаем это так быстро, что внешнему миру кажется, будто мы делаем эти вещи *параллельно*.
Звучит ли это для вас подозрительно как конкурентность с асинхронными событиями (подобная той, что происходит в JS)?! Если нет, вернитесь и прочитайте главу 1 еще раз!
На самом деле, один из способов упростить (т. е. злоупотребить) невероятно сложный мир неврологии и превратить его в то, что я могу отдалённо надеяться обсудить здесь, заключается в том, что наш мозг работает примерно так же, как очередь цикла событий.
Если вы думаете о каждой отдельной букве (или слове), которую я печатаю, как об одном асинхронном событии, только в этом предложении есть несколько десятков возможностей для моего мозга быть прерванным каким-то другим событием, например, от моих органов чувств или даже просто от моего сознания. случайные мысли.
Меня не прерывают и не вовлекают в другой «процесс» при каждой возможности (к счастью, иначе эта книга никогда не была бы написана!). Но достаточно часто случается так, что я чувствую, что мой собственный мозг почти постоянно переключается на разные контексты (также известные как «процессы»). И это очень похоже на то, как, вероятно, чувствовал бы себя движок JS.
### Делать VS Планировать
Итак, наш мозг можно представить как работающий в однопоточной очереди цикла событий подобно движку JS. Это звучит как хорошая аналогия.
Но нам нужно быть более тонкими анализируя это. Существует большая, заметная разница между тем, как мы планируем различные задачи, и тем, как наш мозг на самом деле выполняет эти задачи.
Опять же, вернемся к написанию этого текста как моей метафоры. Мой грубый ментальный план состоит в том, чтобы продолжать писать и писать, последовательно проходя через набор пунктов, которые я упорядочил в своих мыслях. Я не планирую никаких перерывов или нелинейной активности в этом письме. Но тем не менее мой мозг все время переключается.
Несмотря на то, что на операционном уровне наш мозг работает асинхронно, кажется, что мы планируем задачи последовательно и синхронно. «Мне нужно сходить в магазин, потом купить молока, а потом сдать вещи из химчистки».
Вы заметите, что это мышление более высокого уровня (планирование) не кажется очень асинхронным в своей формулировке. На самом деле, мы редко намеренно думаем исключительно с точки зрения событий. Вместо этого мы планируем все тщательно, последовательно (А затем Б затем С), и мы допускаем до некоторой степени своего рода временную блокировку, которая заставляет Б ждать А, а С ждать Б.
Когда разработчик пишет код, он планирует ряд действий, которые должны произойти. Если он хороший разработчик, то он **тщательно планирует** это. «Мне нужно установить `z` в значение `x`, а затем `x` в значение `y`» и так далее.
Когда мы пишем синхронный код, оператор за оператором, он работает во многом подобно нашему списку дел:
```js
// поменять местами `x` и `y` (через временную переменную `z`)
z = x;
x = y;
y = z;
```
Эти три оператора присваивания являются синхронными, поэтому `x = y` ожидает завершения `z = x`, а `y = z`, в свою очередь, ожидает завершения `x = y`. Другими словами, эти три утверждения связаны во времени с выполнением в определенном порядке, одно за другим. К счастью, нам не нужно беспокоиться о каких-либо подробностях, связанных с асинхронными событиями. Если бы мы это сделали, код быстро стал бы намного сложнее!
Итак, если синхронное планирование мозга хорошо соотносится с операторами синхронного кода, насколько хорошо наш мозг справляется с планированием асинхронного кода?
Оказывается, то, как мы выражаем асинхронность (с обратными вызовами) в нашем коде, совсем не соответствует этому синхронному поведению планирования мозга.
Можете ли вы на самом деле представить, что у вас есть ход мыслей, который планирует ваши дела таким образом?
> "Мне нужно в магазин, но по дороге я уверен, что мне позвонят, так что "Привет, мама", и пока она начнет говорить, я поищу адрес магазина по GPS, но это займет секунду, поэтому я убавлю радио, чтобы лучше слышать маму, а потом пойму, что забыл надеть куртку, а на улице холодно, но неважно, продолжаю ехать и разговариваю с мамой, а затем звон ремня безопасности напоминает мне о том, что нужно пристегнуться, так что «Да, мама, я пристегнут ремнем безопасности, я всегда пристегиваюсь!». Ах, наконец-то GPS получил направление, теперь...»
Как бы нелепо это ни звучало как формулировка того, как мы планируем свой день и думаем о том, что делать и в каком порядке, тем не менее именно так работает наш мозг на функциональном уровне. Помните, что это не многозадачность, это просто быстрое переключение контекста.
Причина, по которой нам, как разработчикам, трудно писать асинхронный код с событиями, особенно когда все, что у нас есть, это обратный вызов, заключается в том, что поток мыслей/планирование потока сознания неестественен для большинства из нас.
Мы думаем пошагово, но инструменты (обратные вызовы), доступные нам в коде, не выражаются пошагово, как только мы переходим от синхронного к асинхронному.
И **вот** почему так сложно точно написать и обосновать асинхронный код JS с обратными вызовами: потому что это не то, как работает наш мозг.
**Примечание:** Единственное, что может быть хуже, чем не знать, почему некоторые коды не работают, — это не знать, почему они вообще работают! Это классический менталитет «карточного домика»: «он работает, но не знаю почему, поэтому никто его не трогает!» Возможно, вы слышали «Ад — это другие люди» (Сартр), а программистский мем — «Ад — это код других людей». Я искренне верю: «Ад - это не понимать собственный код». И обратные вызовы являются одним из основных виновников.
### Вложенные/связанные обратные вызовы
Рассмотрим:
```js
listen( "click", function handler(evt){
setTimeout( function request(){
ajax( "http://some.url.1", function response(text){
if (text == "hello") {
handler();
}
else if (text == "world") {
request();
}
} );
}, 500) ;
} );
```
Есть хорошие шансы, что такой код узнаваем для вас. У нас есть цепочка из трех вложенных друг в друга функций, каждая из которых представляет собой шаг в асинхронной последовательности (задача, «процесс»).
Этот тип кода часто называют «ад обратных вызовов», а иногда также называют «пирамидой гибели» (из-за его треугольной формы, обращенной вбок из-за вложенного углубления).
Но «ад обратных вызовов» на самом деле не имеет почти ничего общего с вложенностью/отступом. Это гораздо более глубокая проблема. Мы увидим, как и почему, в оставшейся части этой главы.
Во-первых, мы ждем события «click», затем мы ждем срабатывания таймера, затем мы ждем возврата ответа Ajax, после чего он может повторить все это снова.
На первый взгляд может показаться, что этот код естественным образом связывает свою асинхронность с последовательным мозговым планированием.
Сначала (*сейчас*):
```js
listen( "..", function handler(..){
// ..
} );
```
*позже*:
```js
setTimeout( function request(..){
// ..
}, 500) ;
```
*еще позже*:
```js
ajax( "..", function response(..){
// ..
} );
```
Наконец (наиболее *позже*):
```js
if ( .. ) {
// ..
}
else ..
```
Но есть несколько проблем с линейными рассуждениями об этом коде.
Во-первых, случайность примера состоит в том, что наши шаги находятся на следующих строках (1, 2, 3 и 4...). В настоящих асинхронных JS-программах часто присутствует гораздо больше шума, загромождающего вещи, которым мы должны ловко маневрировать в своем мозгу, когда переходим от одной функции к другой. Понимание асинхронного потока в таком перегруженном обратными вызовами коде не является невозможным, но это определенно не естественно и не легко, даже при большой практике.
Но также есть и более глубокая ошибка, которая не очевидна только в этом примере кода. Позвольте мне придумать другой сценарий (псевдокодовый), чтобы проиллюстрировать его:
```js
doA( function(){
doB();
doC( function(){
doD();
} )
doE();
} );
doF();
```
Хотя опытные из вас правильно определят здесь истинный порядок операций, я держу пари, что на первый взгляд он более чем немного сбивает с толку, и для его достижения требуется несколько согласованных умственных циклов. Операции будут происходить в таком порядке:
* `doA()`
* `doF()`
* `doB()`
* `doC()`
* `doE()`
* `doD()`
Вы поняли это правильно, когда впервые взглянули на код?
Хорошо, некоторые из вас думают, что я несправедливо назвал свои функции, чтобы намеренно ввести вас в заблуждение. Клянусь, я просто называл в порядке появления сверху вниз. Но позвольте мне попробовать еще раз:
```js
doA( function(){
doC();
doD( function(){
doF();
} )
doE();
} );
doB();
```
Теперь я назвал их в алфавитном порядке в порядке фактического выполнения. Но я все еще держу пари, даже имея опыт в этом сценарии, отслеживание в порядке «A -> B -> C -> D -> E -> F» не является естественным для многих из вас, читатели. Конечно, ваши глаза очень много прыгают вверх и вниз по фрагменту кода, верно?
Но даже если все это кажется вам естественным, есть еще одна опасность, которая может нанести ущерб. Можете ли вы определить, что это такое?
Что, если `doA(..)` или `doD(..)` на самом деле не являются асинхронными, как мы, очевидно, предполагали? Ой, теперь порядок другой. Если они оба синхронизированы (и, возможно, только иногда, в зависимости от условий программы в то время), порядок теперь будет «A -> C -> D -> F -> E -> B».
Тот звук, который вы только что услышали на заднем плане, — это вздохи тысяч JS-разработчиков, которые только что столкнулись лицом к лицу.
Является ли вложение проблемой? Из-за этого так сложно отследить асинхронный поток? Это часть этого, конечно.
Но позвольте мне переписать предыдущий пример вложенного события/тайм-аута/Ajax без использования вложенности:
```js
listen( "click", handler );
function handler() {
setTimeout( request, 500 );
}
function request(){
ajax( "http://some.url.1", response );
}
function response(text){
if (text == "hello") {
handler();
}
else if (text == "world") {
request();
}
}
```
Эта формулировка кода едва ли так узнаваема, как проблемы вложенности/отступов в его предыдущей форме, и все же она ничуть не менее подвержена «аду обратных вызовов». Почему?
По мере того, как мы переходим к линейному (последовательному) анализу этого кода, мы должны переходить от одной функции к следующей, к следующей и перемещаться по кодовой базе, чтобы «увидеть» поток последовательности. И помните, это упрощенный код в наилучшем случае. Все мы знаем, что базы кода реальных асинхронных JS-программ зачастую фантастически более запутаны, что на несколько порядков усложняет такие рассуждения.
Еще одна вещь, на которую следует обратить внимание: чтобы связать шаги 2, 3 и 4 вместе, чтобы они выполнялись последовательно, единственный доступный способ, которые обратные вызовы дают нам, — это жестко закодировать шаг 2 в шаг 1, шаг 3 в шаг 2, шаг 4 в шаг 3, и так далее. Жесткое кодирование не обязательно плохо, если это действительно фиксированное условие, что шаг 2 всегда должен вести к шагу 3.
Но жесткое кодирование определенно делает код немного более хрупким, поскольку оно не учитывает никаких ошибок, которые могут вызвать отклонение в последовательности шагов. Например, если шаг 2 терпит неудачу, шаг 3 никогда не достигается, и шаг 2 не повторяется, или выполняется переход к альтернативному потоку обработки ошибок и т.д.
Все эти проблемы вы *можете* вручную жестко прописать в каждом шаге, но этот код часто очень повторяющийся и не может быть повторно использован на других шагах или в других асинхронных потоках в вашей программе.
Несмотря на то, что наш мозг может планировать ряд задач в последовательном порядке (это, затем это, затем это), событийный характер работы нашего мозга делает восстановление/повторение/разветвление управления потоком практически без усилий. Если вы бегаете по делам и понимаете, что оставили дома список покупок, день не заканчивается, потому что вы не спланировали это заранее. Ваш мозг легко обходит эту заминку: вы идете домой, берете список и сразу же направляетесь обратно в магазин.
Но хрупкая природа жестко закодированных вручную обратных вызовов (даже с жестко запрограммированной обработкой ошибок) часто гораздо менее изящна. Как только вы в конечном итоге укажете (также известное как предварительное планирование) все различные возможности/пути, код станет настолько запутанным, что его трудно будет поддерживать или обновлять.
**Вот это** и есть "ад обратных вызовов"! Вложение/отступы - это, по сути, второстепенное шоу, отвлекающий маневр.
И как будто всего этого недостаточно, мы даже не коснулись того, что происходит, когда две или более цепочек этих продолжений обратного вызова происходят *одновременно*, или когда третий шаг разветвляется на «параллельные» обратные вызовы с воротами или защелками, или... ОМГ, у меня мозг болит, а у тебя?!
Улавливаете ли вы здесь, что наше последовательное, блокирующее поведение планирования мозга просто не очень хорошо отображается в асинхронном коде, ориентированном на обратных вызовах? Это первый существенный недостаток обратных вызовов, о котором нужно сказать: они выражают асинхронность в коде таким образом, что нашему мозгу приходится бороться только за то, чтобы синхронизироваться с ним (каламбур!).
## Проблемы с доверием
Несоответствие между последовательным мозговым планированием и асинхронным кодом JS, управляемым обратными вызовами, — это только часть проблемы с обратными вызовами. Есть кое-что гораздо более глубокое, о чем следует беспокоиться.
Давайте еще раз вернемся к понятию callback-функции как продолжения (она же вторая половина) нашей программы:
```js
// A
ajax( "..", function(..){
// C
} );
// B
```
`// A` и `// B` происходят *сейчас* под непосредственным контролем основной JS-программы. Но `// C` откладывается, чтобы произойти *позже*, и находится под контролем другой стороны – в данном случае, функции `ajax(..)`. В общем смысле такая передача управления обычно не вызывает много проблем для программ.
Но пусть вас не вводит в заблуждение его редкость, что этот переключатель управления не имеет большого значения. На самом деле, это одна из самых серьезных (и в то же время наиболее тонких) проблем проектирования, основанного на обратных вызовах. Он вращается вокруг идеи, что иногда `ajax(..)` (т.е. "сторона", которой вы передаете продолжение обратного вызова) не является функцией, которую вы написали или которой вы непосредственно управляете. Часто это утилита, предоставляемая третьей стороной.
Мы называем это «инверсией управления (IoC)», когда вы берете часть своей программы и передаете контроль над ее выполнением другому третьему лицу. Между вашим кодом и сторонней утилитой существует негласный «контракт» — набор вещей, которые вы ожидаете поддерживать.
### Сказка о пяти обратных вызовах
Может быть не совсем очевидно, почему это так важно. Позвольте мне построить преувеличенный сценарий, чтобы проиллюстрировать опасности доверия в игре.
Представьте, что вы разработчик, которому поручено создать систему оплаты электронной торговли для сайта, продающего дорогие телевизоры. У вас уже есть все различные страницы системы оформления заказов, созданные просто отлично. На последней странице, когда пользователь нажимает «подтвердить», чтобы купить телевизор, вам нужно вызвать стороннюю функцию (предоставленную, скажем, какой-то аналитической компанией), чтобы можно было отследить продажу.
Вы заметили, что они предоставили то, что выглядит как утилита асинхронного отслеживания, вероятно, ради лучших практик производительности, что означает, что вам нужно передать функцию обратного вызова. В этом продолжении, которое вы передаете, у вас будет окончательный код, который снимает средства с кредитной карты клиента и отображает страницу благодарности.
Этот код может выглядеть так:
```js
analytics.trackPurchase( purchaseData, function(){
chargeCreditCard();
displayThankyouPage();
} );
```
Достаточно легко, верно? Вы пишете код, тестируете его, все работает, и вы запускаете его в производство. Все счастливы!
Прошло полгода и никаких проблем. Вы почти забыли, что даже написали этот код. Однажды утром вы сидите в кофейне перед работой, небрежно наслаждаясь латте, когда вам звонит в панике начальник и настаивает, чтобы вы бросили кофе и сразу же бросились на работу.
Когда вы приедете, вы узнаете, что с кредитной карты высокопоставленного клиента пять раз списали средства за один и тот же телевизор, и он, по понятным причинам, расстроен. Служба поддержки клиентов уже принесла извинения и обработала возврат средств. Но ваш босс требует знать, как это могло произойти. «Разве у нас нет тестов на подобные вещи!?»
Вы даже не помните код, который вы написали. Но вы копаетесь и начинаете пытаться выяснить, что могло пойти не так.
Покопавшись в некоторых логах, вы приходите к выводу, что единственное объяснение в том, что утилита аналитики каким-то образом по какой-то причине вызвала ваш обратный вызов пять раз вместо одного. Ничто в их документации не упоминает ничего об этом.
Разочарованный, вы связываетесь со службой поддержки, которая, конечно, так же удивлена, как и вы. Они соглашаются сообщить об этом своим разработчикам и обещают вернуться к вам. На следующий день вы получаете длинное электронное письмо с объяснением того, что они нашли, и сразу же пересылаете его своему боссу.
Судя по всему, разработчики аналитической компании работали над некоторым экспериментальным кодом, который при определенных условиях будет повторять предоставленный обратный вызов один раз в секунду в течение пяти секунд, прежде чем произойдет сбой с тайм-аутом. Они никогда не собирались продвигать это в производство, но каким-то образом они это сделали, и они полностью смущены и извиняются. Они подробно рассказывают о том, как они определили сбой и что они сделают, чтобы это никогда не повторилось.
Что дальше?
Вы обсуждаете это со своим боссом, но он не чувствует себя особенно довольным положением вещей. Он настаивает, и вы неохотно соглашаетесь, что вы не можете больше доверять *им* (это то, что вас задело), и что вам нужно снова придумать, как защитить код проверки от такой уязвимости.
После некоторой доработки вы реализуете простой специальный код, подобный приведенному ниже, который, похоже, устраивает команду:
```js
var tracked = false;
analytics.trackPurchase( purchaseData, function(){
if (!tracked) {
tracked = true;
chargeCreditCard();
displayThankyouPage();
}
} );
```
**Примечание:** Это должно показаться вам знакомым из главы 1, потому что мы, по сути, создаем защелку для обработки, если произойдет несколько одновременных вызовов нашего обратного вызова.
Но затем один из ваших QA-инженеров спрашивает: «Что произойдет, если они никогда не вызовут обратный вызов?» Упс. Никто из вас не думал об этом.
Вы начинаете искать кроличью нору и думаете обо всех возможных вещах, которые могут пойти не так, если они перезвонят вам. Вот примерный список возможных сбоев в работе аналитической утилиты:
* Вызывать обратный вызов слишком рано (до того, как он будет отслежен)
* Вызов обратного вызова слишком поздно (или никогда)
* Вызовите обратный вызов слишком мало или слишком много раз (например, проблема, с которой вы столкнулись!)
* Не удалось передать любую необходимую среду/параметры вашему обратному вызову
* Проглотить любые ошибки/исключения, которые могут произойти
* ...
Это должно показаться тревожным списком, потому что так оно и есть. Вы, вероятно, постепенно начинаете понимать, что вам придется изобретать очень много специальной логики **в каждом обратном вызове**, которая передается утилите, которой вы не уверены, что можете доверять.
Теперь вы немного более полно осознаете, насколько адским является «ад обратных вызовов».
### Не только чужой код
Некоторые из вас могут сейчас скептически отнестись к тому, настолько ли это важно, как я это преподношу. Возможно, вы мало взаимодействуете с действительно сторонними утилитами. Возможно, вы используете версионные API или самостоятельно размещаете такие библиотеки, чтобы их поведение нельзя было изменить без вас.
Итак, подумайте над этим: можете ли вы вообще *действительно* доверять утилитам, которыми вы теоретически управляете (в своей собственной кодовой базе)?
Подумайте об этом так: большинство из нас согласны с тем, что, по крайней мере, в некоторой степени мы должны создавать свои собственные внутренние функции с некоторыми защитными проверками входных параметров, чтобы уменьшить/предотвратить непредвиденные проблемы.
Чрезмерное доверие входным данным:
```js
function addNumbers(x,y) {
// + перегружен приведением,
// чтобы также быть конкатенацией строк,
// поэтому эта операция не является строго безопасной
// в зависимости от того, что передано.
return x + y;
}
addNumbers( 21, 21 ); // 42
addNumbers( 21, "21" ); // "2121"
```
Защита от ненадежного ввода:
```js
function addNumbers(x,y) {
// обеспечиваем числовой ввод
if (typeof x != "number" || typeof y != "number") {
throw Error( "Bad parameters" );
}
// если мы доберемся сюда, + безопасно выполнит числовое сложение
return x + y;
}
addNumbers( 21, 21 ); // 42
addNumbers( 21, "21" ); // Error: "Bad parameters"
```
Или, возможно, все еще безопасно, но дружелюбнее:
```js
function addNumbers(x,y) {
// обеспечиваем числовой ввод
x = Number( x );
y = Number( y );
// + будет безопасно делать числовое сложение
return x + y;
}
addNumbers( 21, 21 ); // 42
addNumbers( 21, "21" ); // 42
```
Как бы вы это ни делали, такого рода проверки/нормализации довольно распространены на входных данных функций, даже с кодом, которому мы теоретически полностью доверяем. Грубо говоря, это похоже на программный эквивалент геополитического принципа «Доверяй, но проверяй».
Итак, не разумно ли, что мы должны делать то же самое с композицией обратных вызовов асинхронных функций, не только с действительно внешним кодом, но даже с кодом, который, как мы знаем, обычно «находится под нашим собственным контролем»? **Конечно, должны.**
Но обратные вызовы на самом деле не предлагают ничего, чтобы помочь нам. Мы должны создавать весь этот механизм сами, и часто это приводит к большому количеству шаблонов/накладных расходов, которые мы повторяем для каждого отдельного асинхронного обратного вызова.
Самая неприятная проблема с обратными вызовами — это *инверсия управления*, ведущая к полному разрыву всех этих линий доверия.
Если у вас есть код, который использует обратные вызовы, особенно, но не исключительно, со сторонними утилитами, и вы еще не применяете какую-то логику смягчения для всех этих *инверсии управления* проблем с доверием, ваш код *имеет* ошибки в нем прямо сейчас, хотя они, возможно, еще не укусили вас. Скрытые ошибки остаются ошибками.
Действительно ад.
## Попытка сохранить обратные вызовы
Существует несколько вариантов дизайна обратного вызова, которые пытались решить некоторые (не все!) проблемы доверия, которые мы только что рассмотрели. Это доблестная, но обреченная попытка спасти шаблон обратного вызова от разрушения самого себя.
Например, что касается более изящной обработки ошибок, некоторые проекты API предусматривают раздельные обратные вызовы (один для уведомления об успехе, другой для уведомления об ошибке:
```js
function success(data) {
console.log( data );
}
function failure(err) {
console.error( err );
}
ajax( "http://some.url.1", success, failure );
```
В API такого дизайна часто обработчик ошибок `failure()` является необязательным, и если он не указан, предполагается, что вы хотите проглотить ошибки. Фу.
**Примечание.** Этот дизайн с разделенным обратным вызовом используется API ES6 Promise. Мы рассмотрим обещания ES6 более подробно в следующей главе.
Другой распространенный шаблон обратного вызова называется «стиль с ошибкой в первую очередь» (иногда его также называют «стилем узла», поскольку это также соглашение, используемое почти во всех API-интерфейсах Node.js), где первый аргумент одного обратного вызова зарезервирован для объекта ошибки (если есть). В случае успеха этот аргумент будет пустым/ложным (и любые последующие аргументы будут данными об успехе), но если сигнализируется результат ошибки, первый аргумент устанавливается/истинный (и обычно больше ничего не передается):
```js
function response(err,data) {
// ошибка?
if (err) {
console.error( err );
}
// в иных случаях успех
else {
console.log( data );
}
}
ajax( "http://some.url.1", response );
```
В обоих этих случаях следует соблюдать несколько вещей.
Во-первых, это на самом деле не решило большинство проблем с доверием, как может показаться. В обратном вызове нет ничего, что предотвращало бы или отфильтровывало нежелательные повторные вызовы. Более того, сейчас дела обстоят еще хуже, потому что вы можете получить как сигналы об успехе, так и ошибки, или ни одного из них, и вам все равно придется кодировать любое из этих условий.
Кроме того, не упустите тот факт, что, хотя это стандартный шаблон, который вы можете использовать, он определенно более многословен и похож на шаблон без особого повторного использования, поэтому вы устанете вводить все это для каждого обратного вызова в вашем приложении.
А как насчет проблемы доверия никогда не быть вызванным? Если это вызывает беспокойство (и, вероятно, должно быть!), вам, вероятно, потребуется установить тайм-аут, который отменяет событие. Вы можете сделать утилиту (показано только доказательство концепции), которая поможет вам в этом:
```js
function timeoutify(fn,delay) {
var intv = setTimeout( function(){
intv = null;
fn( new Error( "Timeout!" ) );
}, delay )
;
return function() {
// timeout уже произошёл?
if (intv) {
clearTimeout( intv );
fn.apply( this, [ null ].concat( [].slice.call( arguments ) ) );
}
};
}
```
Вот как можно его использовать:
```js
// использование обратного вызова в стиле «сначала ошибка»
function foo(err,data) {
if (err) {
console.error( err );
}
else {
console.log( data );
}
}
ajax( "http://some.url.1", timeoutify( foo, 500 ) );
```
Еще одна проблема доверия называется «слишком рано». С точки зрения конкретного приложения это может фактически включать вызов до завершения какой-либо критической задачи. Но в более общем плане проблема проявляется в утилитах, которые могут либо вызывать предоставленный вами обратный вызов *сейчас* (синхронно) или *позже* (асинхронно).
Этот недетерминизм в отношении синхронного или асинхронного поведения почти всегда приводит к тому, что очень трудно отследить ошибки. В некоторых кругах вымышленный вызывающий безумие монстр по имени Залго используется для описания синхронно-асинхронных кошмаров. «Не выпускайте Залго!» — это распространенный крик, и он приводит к очень здравому совету: всегда вызывайте обратные вызовы асинхронно, даже если это «сразу же» на следующем этапе цикла событий, чтобы все обратные вызовы предсказуемо были асинхронными.
**Примечание.** Дополнительную информацию о Zalgo см. в статье Орена Голана «Не выпускайте Zalgo!» (https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md) и «Разработка API-интерфейсов для асинхронности» Исаака З. Шлютера (http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony).
Рассмотрим:
```js
function result(data) {
console.log( a );
}
var a = 0;
ajax( "..pre-cached-url..", result );
a++;
```
Будет ли этот код печатать «0» (вызов обратного вызова синхронизации) или «1» (вызов асинхронного обратного вызова)? Зависит... от условий.
Вы видите, как быстро непредсказуемость Zalgo может угрожать любой JS-программе. Таким образом, глупо звучащее «никогда не выпускайте Zalgo» на самом деле является невероятно распространенным и надежным советом. Всегда быть асинхронным.
Что, если вы не знаете, всегда ли рассматриваемый API будет выполняться асинхронно? Вы можете изобрести утилиту, подобную этой `asyncify(..)`, для доказательства концепции:
```js
function asyncify(fn) {
var orig_fn = fn,
intv = setTimeout( function(){
intv = null;
if (fn) fn();
}, 0 )
;
fn = null;
return function() {
// срабатывает слишком быстро, прежде чем сработает таймер
// `intv`, чтобы показать, что асинхронный ход прошел?
if (intv) {
fn = orig_fn.bind.apply(
orig_fn,
// добавляем `this` обёртки к `bind(..)`
// параметры вызова, а также каррирование любых
// передается в параметрах
[this].concat( [].slice.call( arguments ) )
);
}
// уже асинхронно
else {
// вызвать исходную функцию
orig_fn.apply( this, arguments );
}
};
}
```
Можно использовать `asyncify(..)` следующим образом:
```js
function result(data) {
console.log( a );
}
var a = 0;
ajax( "..pre-cached-url..", asyncify( result ) );
a++;
```
Независимо от того, находится ли запрос Ajax в кеше и разрешает попытку немедленного вызова обратного вызова, или его нужно получить по сети и, таким образом, выполнить позже асинхронно, этот код всегда будет выводить `1` вместо `0` -- `result(..)` не может не вызываться асинхронно, что означает, что `a++` имеет шанс запуститься раньше, чем `result(..)`.
Ура, еще одна проблема доверия решена! Но это неэффективно и еще больше раздувает шаблон, чтобы отягощать ваш проект.
Это просто история, снова и снова, с обратными вызовами. Они могут делать почти все, что вы хотите, но вы должны быть готовы много работать, чтобы получить это, и часто эти усилия намного больше, чем вы можете или должны потратить на такие рассуждения о коде.
Возможно, вам понадобятся встроенные API или другие языковые механизмы для решения этих проблем. Наконец-то появился ES6 с отличными ответами, так что продолжайте читать!
## Обзор
Обратные вызовы — это фундаментальная единица асинхронности в JS. Но их недостаточно для меняющегося ландшафта асинхронного программирования по мере взросления JS.
Во-первых, наш мозг планирует вещи последовательным, блокирующим, однопоточным семантическим способом, но обратные вызовы выражают асинхронный поток довольно нелинейным, непоследовательным образом, что значительно усложняет правильное рассмотрение такого кода. Плохие рассуждения о коде — это плохой код, который приводит к серьезным ошибкам.
Нам нужен способ выразить асинхронность в более синхронной, последовательной, блокирующей манере, как это делает наш мозг.
Во-вторых, что более важно, обратные вызовы страдают от *инверсии управления*, поскольку они неявно передают управление другой стороне (часто сторонней утилите, не находящейся под вашим контролем!) для вызова *продолжения* вашей программы. Эта передача управления приводит нас к тревожному списку проблем с доверием, например, вызывается ли обратный вызов чаще, чем мы ожидаем.
Изобретение специальной логики для решения этих проблем с доверием возможно, но это сложнее, чем должно быть, и в результате получается более громоздкий и трудный для сопровождения код, а также код, который, вероятно, недостаточно защищен от этих опасностей, пока вы явно не укусите его ошибки.
Нам нужно обобщенное решение **всех проблем с доверием**, которое можно будет повторно использовать для любого количества обратных вызовов, которое мы создадим, без дополнительных шаблонных накладных расходов.
Нам нужно что-то лучше, чем обратные вызовы. До сих пор они хорошо служили нам, но «будущее» JavaScript требует более сложных и эффективных асинхронных шаблонов. В последующих главах этой книги мы углубимся в эти зарождающиеся эволюции.
================================================
FILE: async & performance/ch3.md
================================================
# Вы не знаете JS: Асинхронность и производительность
# Глава 3: Промисы
В главе 2 мы определили две главные категории недостатков в использовании колбеков для того, чтобы выразить асинхронность программы и управлять параллельной обработкой: отсутствие последовательного кода и надежности. Теперь, когда мы разобрали эти проблемы досконально, настало время обратить внимание на шаблоны, которые позволят их решить.
Проблема, с которой мы хотим начать - *инверсия управления (IoC)*, это то доверие, которое так тяжело сохранять и так легко потерять.
Вспомните как мы обработали *продолжение (continuation)* нашей программы в колбек-функции и передали этот колбек в другую часть кода (потенциально даже во внешний код) и просто скрестили пальцы на удачу, что вызов этого колбэка произойдет корректно.
Мы сделали это поскольку мы хотели этим сказать, "вот то, что должно выполниться *позже*, после того, как завершится текущий шаг программы."
Но что если бы мы смогли разинверсировать эту *инверсию управления*? Что если вместо передачи продолжения программы в другой код мы могли бы ожидать от него дать нам возможность узнать когда его задача завершится и затем наш код мог бы решить, что делать дальше?
Эта концепция называется **Промисы**.
Промисы начинают быстро покорять мир JS, поскольку разработчики и создатели спецификаций в равной мере отчаянно ищут возможность избавиться от безумия ада колбеков в своим коде/дизайне. На самом деле, многие новые асинхронные API добавляются в платформу JS/DOM будучи построенным на промисах. Так что, возможно, это неплохая идея уйти с головой и изучить их, Вы так не думаете!?
**Примечание:** Слово "сразу" будет часто использоваться в этой главе, в основном указывая на какое-либо действие по разрешению промиса. Однако, в фактически всех случаях, "сразу" в терминах поведения очереди заданий означает (см. главу 1) не строго синхронное значение *сейчас*.
## Что такое промис?
Когда разработчики решают изучить новую технологию или шаблон, обычно их первым шагом будет "Покажите мне код!". Это вполне естественно для нас просто броситься в омут с головой и изучать по ходу.
Однако бывает так, что одних только API недостаточно, чтобы понять определённые абстракции. Промисы — это как раз тот инструмент, где по тому, как их используют, сразу видно: понимает ли человек, для чего они нужны и как работают, или просто изучил и использует API.
Так что перед тем как я покажу код с промисами, я хочу объяснить целиком и полностью что такое промисы концептуально. Я надеюсь, что это быстрее направит вас на истинный путь по мере того как вы будете постигать теорию интеграции промисов в свой асинхронный поток.
Помня об этом, давайте взглянем на две различные аналогии того что *есть* промисы.
### Будущее значение
Представьте такой сценарий: Я подхожу к стойке в ресторане быстрого питания и делаю заказ на чизбургер. Я даю кассиру $1.47. Разместив и оплатив свой заказ, я сделал запрос возврата *значения* (чизбургера). Я открыл транзакцию.
Но частенько, чизбургер не сразу мне доступен. Кассир даем мне что-то взамен моего чизбургера: чек с номером заказа в нем. Этот номер заказа - это IOU-*обещание* (промис) (Я должен вам ("I owe you")) которое гарантирует, что в итоге я должен получить свой чизбургер.
Так что я храню мой чек и номер заказа. Я знаю, что оно представляет собой мой *будущий чизбургер*, поэтому мне не надо о этом больше волноваться кроме ощущения голода!
Пока я жду Я могу заниматься другими делами, например отправить текстовое сообщение другу, говорящее: "Эй, как насчет присоединиться ко мне за обедом? Я собираюсь съесть чизбургер."
Я уже рассуждаю о своем *будущем чизбургере*, несмотря на то, что у меня еще его нет. Мой мозг способен так делать потому что он воспринимает номер заказа как заменитель чизбургера. Такой заменитель фактически делает значение *независимым от времени*. Это и есть **будущее значение**.
В итоге я слышу "Заказ 113!" и радостно иду обратно к стойке с чеком в руках. Я передаю чек кассиру и взамен беру свой чизбургер.
Другими словами, как только мое *будущее значение* было готово, я обменял промис значения на само значение.
Но есть и еще один возможный исход. Они называют мой номер заказа, но когда я подхожу, чтобы забрать свой чизбургер кассир с сожалению сообщает мне: "Мне жаль, но мы судя по всему остались без чизбургеров." Оставив в стороне разочарование покупателя в таком сценарии на секундочку, мы можем заметить важную характеристику *будущих значений*: они могут сигнализировать либо о завершении, либо об отказе.
Каждый раз, когда я заказываю чизбургер, я знаю что либо я рано или поздно получу чизбургер, либо получу печальные новости о нехватке чизбургеров и я должен буду придумать что-то другое на обед.
**Примечание:** В реальном коде процесс не такой простой, потому что метафорически номер заказа может никогда не быть назван, и в таком случае мы остаемся в неразрешим состоянии вечно. Мы еще вернемся к этому случаю позже.
#### Значения Сейчас и Позже
Всё это может прозвучать слишком абстрактно для применения в вашем коде. Так давайте внесем больше конкретики.
Однако, перед тем, как мы представим как работают промисы подобным образом, мы собираемся покопаться в коде, который мы уже умеем понимать -- колбеки! -- чтобы понять как обрабатывать эти *будущие значения*.
Когда вы пишете код, чтобы обработать каким-либо образом некое значение, например выполнив математические вычисления над `числом`, то осознанно или нет, вы предполагаете кое-что очень фундаментальное об этом значении, а именно, что это - уже конкретное значение *сейчас*:
```js
var x, y = 2;
console.log( x + y ); // NaN <-- потому что в `x` еще не установлено значение
```
Операция `x + y` предполагает, что оба `x` и `y` уже заданы. В терминах, которые мы вскоре разъясним, мы полагаем что значения `x` и `y` уже *разрешены* (т.е. с уж определенными значениями).
Будет абсурдом ожидать, что оператор `+` сам по себе каким-то магическим образом сможет определить и ждать до тех пор, пока оба `x` и `y` разрешатся (т.е. будут готовы), и только затем выполнит операцию. Это может привести к хаосу в программе, если одни выражения закончатся *сейчас*, а другие закончатся *позже*, не так ли?
Как вы сможете потенциально рассуждать о связях между двумя выражениями, если одно из них (или оба) могут быть еще не завершены? Если выражение 2 полагается на то, что выражение 1 будет завершено, то возможны два исхода: либо выражение 1 закончится прямо *сейчас* и всё благополучно продолжится, либо statement 1 еще не завершено, и в итоге выражение 2 приведет к ошибке.
Если такие вещи звучат знакомо после главы 1, хорошо!
Давайте вернемся к нашей математической операции `x + y`. Представьте, что был бы путь сказать, "Сложи `x` и `y`, но если кто-то из них еще не подготовлен, просто подожди пока это не произойдет. Сложи их как можно скорее."
Ваш мозг возможно сразу переключился на колбеки. Хорошо, итак...
```js
function add(getX,getY,cb) {
var x, y;
getX( function(xVal){
x = xVal;
// оба готовы?
if (y != undefined) {
cb( x + y ); // отправить сумму
}
} );
getY( function(yVal){
y = yVal;
// оба готовы?
if (x != undefined) {
cb( x + y ); // отправить сумму
}
} );
}
// `fetchX()` and `fetchY()` синхронные или асинхронные
// функции
add( fetchX, fetchY, function(sum){
console.log( sum ); // это было легко, ага?
} );
```
Подождите минутку, чтобы позволить красоте (или отсутствию таковой) этого кусочка кода уложиться в голове (терпеливо насвистываю).
Хотя это уродство и несомненное, тут есть кое-что очень важное на заметку об этом асинхронном шаблоне.
В этом кусочке кода, мы трактовали `x` и `y` как будущие значения и мы выразили операцию `add(..)` так, что она (снаружи) не заботится о том, доступен ли `x` или `y` прямо сейчас или нет. Другими словами, он нормализует *сейчас* и *потом* таким образом, что мы можем положиться на предсказуемый результат операции `add(..)`.
При использовании `add(..)`, которая временно консистентна, она ведет себя одинаково *сейчас* и *потом* - такой асинхронный код легче себе представлять.
Выражаясь более просто: чтобы обработать согласованно оба *сейчас* и *потом*, мы оба их превращаем в *потом*: все операции становятся асинхронными.
Конечно, этот грубый подход, основанный на колбеках, оставляет желать намного лучшего. Это всего-лишь первый крошечный шаг к пониманию преимуществ представления *будущих значений* без беспокойств о временном аспекте того, доступны они или нет.
#### Промис как значение
Мы определенно углубимся в детали промисов позже в этой главе, поэтому не волнуйтесь если что-то тут покажется запутанным, а просто мельком взгляните на то, как мы выразим пример `x + y` через `Promise`ы:
```js
function add(xPromise,yPromise) {
// `Promise.all([ .. ])` принимает массив промисов
// и возвращает новый промис, который ожидает завершения всех переданных
return Promise.all( [xPromise, yPromise] )
// когда промис разрешен, давайте возьмем
// полученные значения `X` и `Y` и сложим их.
.then( function(values){
// `values` - массив сообщений от
// ранее разрешенных промисов
return values[0] + values[1];
} );
}
// `fetchX()` и `fetchY()` возвращают промисы для
// своих соответствующих значений, которые могут быть готовы
// *сейчас* или *позже*.
add( fetchX(), fetchY() )
// мы получаем обратно промис с суммой этих
// двух чисел.
// теперь мы выполняем в цепочке вызов `then(..)`, чтобы дождаться разрешения
// этого возвращенного промиса.
.then( function(sum){
console.log( sum ); // это намного легче!
} );
```
В этом кусочке кода есть два слоя промисов.
`fetchX()` и `fetchY()` вызываются напрямую и возвращаемые или значения (промисы!) передаются в `add(..)`. Внутренние значения, которые представляют эти промисы, могут быть готовы *сейчас* или *позже*, но каждый промис приводит свое поведение к тому, чтобы вести себя одинаково вне зависимости ни о чего. Мы рассуждаем о значениях `X` и `Y` во время-независимой манере. Они - *будущие значения*.
Второй уровень - это промис, который создается в `add(..)` (через `Promise.all([ .. ])`) и возвращается, и который мы ожидаем вызвав `then(..)`. Когда операция `add(..)` завершена, наше *будущее значение* `sum` готово и можем вывести его на экран. Внутри `add(..)` мы скрываем всю логику ожидания *будущих значений* `X` и `Y`.
**Примечание** Внутри `add(..)`, вызов `Promise.all([ .. ])` создает промис (который ждем разрешения `promiseX` и `promiseY`). Цепочечный Вызов `.then(..)` создает еще один промис, который сразу же разрешает строку `return values[0] + values[1]` (с результатом сложения). Таким образом, вызов `then(..)`, который мы поместили в конец цепочки вызова `add(..)` в конце фрагмента кода, в сущности работает с этим вторым возвращенным промисом, а не с первым, созданным `Promise.all([ .. ])`. Также, хотя мы и не добавили ничего в конец цепочки второго `then(..)`, он также создает еще один промис, невзирая на то, хотим мы его использовать или нет. Эту штуку с цепочками промисов мы поясним в деталях позже в этой главе.
Прямо как с заказами чизбургеров, есть такая же вероятность того, что промис разрешится отказом вместо исполнения. В отличие от исполненного промиса, где значение всегда программно задано, значение при отказе, обычно называемое "причиной отказа", может быть установлено либо напрямую в логике программы, либо может явиться результатом исключения времени выполнения.
С использованием промисов, вызов `then(..)` фактически может принимать две функции: первую - для завершения (как показано ранее), а вторую - для отказа:
```js
add( fetchX(), fetchY() )
.then(
// обработчик завершения
function(sum) {
console.log( sum );
},
// обработчик отказа
function(err) {
console.error( err ); // облом!
}
);
```
Если что-то пошло не так при получении `X` или `Y`, или что-то каким-либо образом привело к сбою во время сложения, промис, который возвращается из `add(..)` - отвергается (завершается отказом) и второй колбек-обработчик ошибок, переданный в `then(..)` получит значение отказа из промиса.
Поскольку промисы инкапсулируют в себе состояние, не зависящее от времени, с ожиданием завершения или отказа получения значения операции снаружи, промис сам по себе является независимым от времени и потому промисы можно компоновать (составлять) предсказуемым образом независимо от времени или внутреннего результата.
Более того, как только промис разрешен, он остается таковым навсегда, он становится *неизменяемым значением* в этот момент и может потом быть *обследован* столько раз, сколько нужно.
**Примечание** Поскольку промис является неизменяемым внешне как только он разрешен, то теперь можно безопасно передавать его куда угодно зная, что он не может быть изменен случайно или злонамеренно. Это особенно верно в связи с тем, что наблюдать за разрешением одного и того же промиса могут разные стороны. Невозможно повлиять на возможность одной стороны наблюдать за разрешением промиса другой стороной. Неизменяемость может прозвучать как какая-то научная тема, но на самом деле это один из самых фундаментальных и важных аспектов дизайна промисов и не должен быть рассмотрен походя мимоходом.
Это один из самых мощных и важных ключей к пониманию промисов. Проделав достаточно большую работу, вы могли бы специально добиться того же эффекта используя только композицию из уродливых колбеков, но это не особенно эффективная стратегия, особенно потому, что вы вынуждены делать это снова и снова.
Промисы - это легко повторяемый механизм инкапсуляции и совмещения *будущих значений*.
### Событие завершения
Как мы только что видели, одиночный промис ведет себя как *будущее значение*. Но есть и другой путь представлять разрешение промиса: как механизм потокового управления, временнОе "это-затем-то" для двух и более шагов в асинхронной задаче.
Давайте представим вызов функции `foo(..)` для выполнения некой задачи. Мы либо не знаем ничего о ее внутренней реализации, или просто не беспокоимся об этом. Она может завершить задачу сразу или может занять некоторое время.
Нам просто нужно знать когда завершится `foo(..)`, чтобы мы могли двигаться к нашей следующей задаче. Другими словами, нам нужна возможность получить оповещение о завершении `foo(..)`, чтобы мы могли *продолжить* выполнение.
В обычном случае в JavaScript если вам нужно получить оповещение, вы вероятно подумаете об этом с точки зрения событий. Таким образом мы можем переформулировать нашу потребность в оповещении как необходимость получить событие *завершения* (или *продолжения*), инициированное функцией `foo(..)`.
**Примечание** Назовете ли вы это "событием завершения" или "событием продолжения" зависит от вашей точки зрения. На чем больше смещен фокус: на том что случится в `foo(..)` или на том что произойдет *после* завершения `foo(..)`? Обе точки зрения точны и полезны. Уведомление о событии сообщает нам, что `foo(..)` *завершилась*, но также и то, что можно *продолжить* выполнение следующего шага. Безусловно, тот колбек, который вы передаете, чтобы он был вызван для уведомления о событии, сам по себе то, что мы ранее назвали *продолжение*. Потому что *событие завершения* немного более сфокусировано на `foo(..)`, что больше привлекает наше внимание в настоящий момент, мы все же чуть больше отдаем предпочтение *событию завершения* до конца этого текста.
С использованием колбеков, "уведомлением" будет наш колбек, вызванный задачей (`foo(..)`). Но с промисами, мы переворачиваем отношения и ожидаем, что можем ждать событие от `foo(..)` и как только получим его может действовать соответственно.
Сперва, обратите внимание на этот псевдокод:
```js
foo(x) {
// начинаем выполнять что-то, что требует времени
}
foo( 42 )
on (foo "completion") {
// теперь мы можем выполнить следующий шаг!
}
on (foo "error") {
// ой, что-то пошло не так в `foo(..)`
}
```
Мы вызываем `foo(..)`, а затем настраиваем два обработчика событий, один для `"completion"` (завершение), а другой для `"error"` (сбоя)-- двух возможных *окончательных* исхода вызова `foo(..)`. По сути, не похоже, что `foo(..)` вообще в курсе о том, что вызывающий код подписался на эти события, что ведет к очень хорошему *разделению обязанностей*.
К сожалению, такой код потребовал бы некоторой "магии" окружения JS, которое не существует (и которое бы весьма вероятно было бы немного непрактичным). Вот более естественный путь, которым мы может это выразить в JS:
```js
function foo(x) {
// начнем выполнять что-нибудь, требующее времени
// создадим обработчик оповещения о событии `listener`,
// чтобы его можно было вернуть из функции
return listener;
}
var evt = foo( 42 );
evt.on( "completion", function(){
// теперь мы можем выполнить следующий шаг
} );
evt.on( "failure", function(err){
// ой, что-то пошло не так в `foo(..)`
} );
```
`foo(..)` специально создает возможность подписки на события, которую можно вернуть из функции и вызывающий код получает и регистрирует два обработчик событий для нее.
Инверсия обычного колбек-ориентированного кода должно быть очевидна и это намеренно. Вместо передачи колбеков `foo(..)`, она возвращает возможность получения событий, которую мы назвали `evt`, которая получает колбеки.
Но если вы вспомните главу 2, колбеки сами по себе являются *инверсией управления* (IoC). Таким образом инвертируя шаблон колбека, в действительности получаем *инверсия инверсии* или *разинверсия управления*, возвращая управление обратно вызывающему коду, туда где оно должно было быть изначально.
Одно важное преимущество - это то, что многим отдельным частях кода можно дать возможность получать события и они все смогут быть независимо уведомлены о том, когда завершится `foo(..)`, чтобы выполнить последующий код после ее завершения:
```js
var evt = foo( 42 );
// пусть `bar(..)` получает уведомление о завершении `foo(..)`
bar( evt );
// пусть `baz(..)` также получает уведомление о завершении `foo(..)`
baz( evt );
```
*Разинверсия управления* открывает возможность лучшего *разделения обязанностей*, где функциям `bar(..)` и `baz(..)` не нужно быть вовлеченными в то, как вызывается `foo(..)`. Аналогично, функции `foo(..)` не нужно ни знать, ни беспокоиться о том, что `bar(..)` и `baz(..)` существуют или ждут уведомления о завершении `foo(..)`.
Фактически, это объект `evt` - это нейтральный сторонний посредник между отдельными функциональными обязанностями.
#### "События" промиса
Как вы можете теперь предположить, возможность получения события `evt` - это аналогия для промиса.
В промисо-ориентированном подходе предыдущий блок кода мог бы содержать `foo(..)`, создающую и возвращающую экземпляр `Promise`, и этот промис был бы передан в `bar(..)` и `baz(..)`.
**Примечание** "События" разрешения промиса, который мы ждем, не являются событиями в строгом смысле (хотя они определенно ведут себя как события в этом сценарии), и они не просто вызывают `"completion"` или `"error"`. Вместо этого, мы используем `then(..)`, чтобы зарегистрировать событие `"then"`. Или чуть более точно, `then(..)` регистрирует событие(я) `"fulfillment"` (выполнения) и/или `"rejection"` (отказа), хотя мы и не видим эти термины в явном виде в коде.
Взгляните:
```js
function foo(x) {
// начнем выполнять что-нибудь, требующее времени
// создает и возвращаем промис
return new Promise( function(resolve,reject){
// в итоге вызвать `resolve(..)` или `reject(..)`,
// которые являются колбеками разрешения для промиса.
} );
}
var p = foo( 42 );
bar( p );
baz( p );
```
**Примечание** Шаблон, показанный с `new Promise( function(..){ .. } )` - обычно называется["открытый конструктор (revealing constructor)"](http://domenic.me/2014/02/13/the-revealing-constructor-pattern/). Переданная функция выполняется сразу (а не асинхронно отложенным вызовом, как колбеки в `then(..)`) и туда передаются два параметра, которые в этом случае называются `resolve` и `reject`. Это функции разрешения промиса. `resolve(..)` обычно сигнализирует о выполнении, а `reject(..)` - об отказе.
Вы вероятно сможете угадать, как выглядят внутри `bar(..)` и `baz(..)`:
```js
function bar(fooPromise) {
// ждать завершения `foo(..)`
fooPromise.then(
function(){
// `foo(..)` теперь закончена, так что
// выполняем задачу `bar(..)`
},
function(){
// ой, что-то пошло не так в `foo(..)`
}
);
}
// то же самое для `baz(..)`
```
Разрешение промиса не обязательно требует отправки сообщения, как это было когда мы исследовали промисы как *будущие значения*. Это может быть просто сигнал управления потоком, как это было в предыдущем блоке кода.
Еще один путь добиться этого:
```js
function bar() {
// `foo(..)` определенно завершилась, поэтому
// выполняем задачу `bar(..)`
}
function oopsBar() {
// ой, что-то пошло не так в `foo(..)`,
// поэтому `bar(..)` не был запущен
}
// то же самое для `baz()` и `oopsBaz()`
var p = foo( 42 );
p.then( bar, oopsBar );
p.then( baz, oopsBaz );
```
**Примечание** Если вы уже видели раньше промис-ориентированный код, у вас может возникнуть соблазн поверить, что две последних строки этого кода можно записать как `p.then( .. ).then( .. )`, используя цепочку вызовов вместо `p.then(..); p.then(..)`. Это было бы совершенно другое поведение, будьте осторожны! Прямо сейчас разница может быть неочевидна, но это на самом деле совершенно другой асинхронный шаблон, нежели мы видели до сих пор: разделение/разветвление. Не волнуйтесь! Мы вернемся к этому позже в этой главе.
Вместо передачи промиса `p` в `bar(..)` и `baz(..)`, мы используем промис, чтобы управлять когда `bar(..)` и `baz(..)` будут выполнены, если вообще будут. Главное отличие - в обработке ошибок.
В подходе, использованном в первом примере кода, `bar(..)` вызывается независимо от того, `foo(..)` или нет, и в нем выполняется своя собственная логика возврата, если получается сообщение о том, что в `foo(..)` произошел сбой. Очевидно, что то же самое верно и для `baz(..)`.
Во втором примере кода, `bar(..)` вызывается только если `foo(..)` завершена, а иначе вызывается `oopsBar(..)`. То же самое для `baz(..)`.
Ни один из этих подходов не является *корректным* сам по себе. Будут случаи, когда один будет предпочитаем другому.
В любом случае, промис `p`, который возвращается из `foo(..)`, используется для контроля того, что произойдет дальше.
Более того, факт того, что оба примера кода заканчиваются вызовом `then(..)` дважды для одного и того же промиса `p` иллюстрирует рассказанное ранее, то что промисы (если уже разрешены) остаются в том же состоянии разрешения (завершение или отказ) навсегда и могут быть позже исследованы столько раз, сколько нужно.
Всякий раз когда `p` разрешается, следующий шаг будет одним и тем же, и *сейчас*, и *позже*.
## Утиная типизация и наличие then
В краю промисов важной деталью является то, как узнать наверняка, что какое-то значение - подлинный промис или нет. Или более прямо, ведет ли себя это значение как промис?
Учитывая, что промисы создаются используя синтаксис `new Promise(..)`, вы можете подумать, что `p instanceof Promise` будет приемлемой проверкой. Но к сожалению есть ряд причин, указывающих что этого совершенно недостаточно.
Преимущественно, вы можете получить значение промиса от другого окна браузера (iframe и т.д.), у которого есть свой собственный промис, отличный от того, который в текущем окне/фрейме, и такая проверка на определение промиса была бы неудачной.
Более того, библиотека или фреймворк могут избрать путь распространения своих собственных промисов и не использовать нативную реализацию ES6 `Promise`. По сути, вы можете вполне успешно пользоваться промисами из библиотек в старых браузерах, у которых совсем нет промисов.
Когда мы будем обсуждать процессы разрешения промисов позже в этой главе, станет более очевидным почему ненативное, но выглядящее как промис значение будет все еще очень важно опознавать и употреблять. Но на текущий момент, просто поверьте на слово, что это важная часть головоломки.
Поэтому, было определено, что путем определения промиса (или чего-то, что ведет себя как промис) будет определение чего-либо, называемого "then-содержащим", как любой объект или функция, у которой есть метод `then(..)`. Предполагается, что любое такое значение является промисо-совместимым then-содержащим.
Общее термин для "проверок типа", которые делают предположения о "типе" значения на основании его формы (какие в нем есть свойства), называется "утиная типизация": "Если это выглядит как утка и крякает как утка, значит это должно быть утка" (см. раздел *Типы и грамматика* в этой серии книг). Таким образом утиная проверка на наличие then условно будет такой:
```js
if (
p !== null &&
(
typeof p === "object" ||
typeof p === "function"
) &&
typeof p.then === "function"
) {
// предположим, что это содержит then!
}
else {
// не содержит then
}
```
Тьфу! Оставляя в стороне тот факт, что эта логика немного уродливая для использования в различных местах, тут происходит кое-что поглубже и более проблематичное.
Если вы попробуете завершить промис из любого значения объекта/функции, у которых оказалась функция `then(..)`, но вы не хотели интерпретировать его как промис/then-содержащее значение, вам не повезло, потому что он будет автоматически распознан как then-содержащее значение и обработан по специальным правилам (см. позднее в этой главе).
Это верно даже если вы не осознавали, что у этого значения есть `then(..)`. Например:
```js
var o = { then: function(){} };
// сделать чтобы у `v` в качестве `[[Prototype]]` было `o`
var v = Object.create( o );
v.someStuff = "cool";
v.otherStuff = "not so cool";
v.hasOwnProperty( "then" ); // false
```
`v` совсем не выглядит как промис или then-содержащее значение. Это просто обычный объект с некоторыми свойствами. Возможно вы просто хотите передавать это значение везде как любой другой объект.
Но скрытно от вас, `v` также связано `[[Prototype]]` (см. *this и прототипы объектов* книгу в серии книг) с другим объектом `o`, в котором как оказалось есть `then(..)`. Таким образом проверка утиной типизации на then-содержащее значение подумает и предположит, что `v` - это then-содержащее значение. Ой-ей.
Тут даже всё может быть не так явно намеренным:
```js
Object.prototype.then = function(){};
Array.prototype.then = function(){};
var v1 = { hello: "world" };
var v2 = [ "Hello", "World" ];
```
Оба `v1` и `v2` будут определены как then-содержащие значения. Вы не можете контролировать или предсказать добавит ли какой-либо код случайно или злонамеренно `then(..)` в `Object.prototype`, `Array.prototype` или любой из других встроенных прототипов. И если то, что указано является функцией, которая не вызывает ни один из своих параметров как колбеки, то любой промис, разрешенный с таким значением просто незаметно повиснет навсегда! Безумие.
Звучит неправдоподобно или невероятно? Возможно.
Но не забывайте, что есть несколько хорошо известных не-промис библиотек, существовавших в сообществе до ES6, в кторым случайно оказался метод, названный `then(..)`. Некоторые из этих библиотек решили переименовать свои собственные методы, чтобы избежать коллизий (которые удручают!). Другие просто были отнесены к несчастливому статусу "несовместим с кодом, использующим промисы" в награду за их неспособность измениться, чтобы убраться с дороги.
По стандартам решили украсть ранее незарезервированное и совершенно универсально звучащее имя свойства `then`, что означает что ни одно значение (или любой из его делегатoв), прошлое, настоящее или будущее, не может иметь функцию `then(..)` намеренно или случайно, в противном случае это значение будут путать с then-содержащим в промис-системах, что вероятно повлечет за собой создание ошибок, которые будет действительно трудно отловить.
**Предупреждение:** Мне не нравится как мы закончили материал об утиной типизации then-содержащих значений для определения промисов. Были и другие варианты, такие как "брэндинг" или даже "анти-брэндинг"; то, что у нас было, казалось наихудшим компромиссом. Но это совсем не конец света. Then-содержащая утиная типизация может быть и полезной как мы увидим позже. Просто будьте осторожны, так как такая утиная типизация по then может быть опасна если она некорректно определяет что-то как промис, которое таковым не является.
## Доверие к промису
Сейчас мы увидели две сильные аналогии, которые объясняют различные аспекты того, что могут делать промисы для нашего асинхронного кода. Но если мы тут и остановимся, мы возможно упустим единственную важнейшую характеристику, которую предоставляет шаблон промисов: доверие.
В то время как аналогии *будущие значения* и *события завершения* в явном виде происходят в тех шаблонах кода которые мы изучили, будет не совсем очевидно почему или как промисы разработаны, чтобы решить все проблемы доверия *инверсии управления*, которые мы изложили в секции "Проблемы с доверием" главы 2. Но слегка покопавшись мы можем вскрыть некоторые важные гарантии, которые восстановят уверенность в асинхронном кодировании, которую разрушила глава 2!
Давайте начнем с рассмотрения проблем доверия при разработке в стиле одних только колбеков. Когда вы передаете колбек в функцию `foo(..)`, она может:
* Вызвать колбек слишком рано
* Вызвать колбек слишком поздно (или никогда)
* Вызвать колбек слишком мало раз или слишком много раз
* Провалить передачу в колбек любых необходимых окружения/параметров
* Проглотить любые ошибки/исключения, которые могут произойти
Характеристики промисов намеренно разработаны, чтобы обеспечить полезные, воспроизводимые ответы на все эти проблемы.
### Вызывая слишком рано
В первую очередь, эта проблема в том, могут ли проявиться в коде Залго-подобные эффекты (см. главу 2), где иногда задача завершается синхронно, а иногда - асинхронно, что может приводить к состоянию гонки.
Промисы по определению не могут быть подвержены этой проблеме, потому что даже сразу завершенный промис (типа `new Promise(function(resolve){ resolve(42); })`) нельзя *исследовать* синхронно.
То есть, когда вы вызываете `then(..)` у промиса, даже если промис уже был разрешен, колбек, который вы передаете в `then(..)` **всегда** будет вызван асинхронно (детальнее об этом см. "Задачи" в главе 1).
Больше не нужно вставлять свои собственные костыли с `setTimeout(..,0)`. Промисы не допускают Залго автоматически.
### Вызывая слишком поздно
Аналогично предыдущему пункту, колбеки наблюдения, зарегистрированные в `then(..)` промиса автоматически планируются к вызову когда вызван либо `resolve(..)`, либо `reject(..)` посредством кода создания промиса. Эти запланированные колбеки будут предсказуемо вызваны в следующий асинхронный момент (см. "Задачи" в главе 1).
Синхронное наблюдение тут невозможно, следовательно невозможно запустить синхронную цепочку задач таким образом, чтобы на практике "отложить" вызов другого колбека ожидаемым образом. То есть, когда промис разрешен, все зарегистрированные для`then(..)` колбеки будут по порядку вызваны, сразу же при следующей асинхронной возможности (снова, см. "Задачи" в главе 1) и ничто, что происходит внутри одного из этих колбеков не может повлиять или задержать вызов остальных колбеков.
Например:
```js
p.then( function(){
p.then( function(){
console.log( "C" );
} );
console.log( "A" );
} );
p.then( function(){
console.log( "B" );
} );
// A B C
```
Здесь, `"C"` не может прервать и предшествовать `"B"`, в силу того как промисам было определено работать.
#### Хитрости планировщика промисов
Важно отметить, впрочем, что есть масса нюансов планировщика, когда относительный порядок между колбеками, выстроенными в цепочки двух отдельных промисов, не является надежно предсказуемым.
Если два промиса `p1` и `p2` оба уже разрешены, то будет истиной, что `p1.then(..); p2.then(..)` закончится вызовов колбека(ов) для `p1` до колбеков для `p2`. Но есть некоторые хитрые случаи, когда это может быть и не так, такие как следующий:
```js
var p3 = new Promise( function(resolve,reject){
resolve( "B" );
} );
var p1 = new Promise( function(resolve,reject){
resolve( p3 );
} );
var p2 = new Promise( function(resolve,reject){
resolve( "A" );
} );
p1.then( function(v){
console.log( v );
} );
p2.then( function(v){
console.log( v );
} );
// A B <-- не B A как вы могли бы ожидать
```
Мы расскажем об этом позже, но как вы видите `p1` разрешен не с непосредственным значением, а с еще одним промисом `p3`, который сам разрешен со значением `"B"`. Указанное поведение - это *распаковать* `p3` внутри `p1`, но асинхронно, таким образом колбек(и) `p1` *позади* колбека(ов) `p2` в очереди асинхронных задач (см. главу 1).
Чтобы избежать таких хитрых кошмаров, вам никогда не следует полагаться на что-либо связанное с порядком/шедулингом колбеков в промисах. На самом деле, хорошей практикой будет не писать код таким образом, чтобы порядок многочисленных колбеков имел хоть какие-то значение. Избегайте этого если сможете.
### Колбек, который никогда не был вызван
Это - очень распространенная проблема. Она решаема несколькими путями с промисами.
Во-первых, ничто (ни даже JS-ошибка) не может повлиять на уведомление вас от промиса о своем разрешении (если он разрешен). Если вы указываете оба колбека как для завершения, так и для сбоя при создании промиса и промис разрешается,то один из этих двух колбеков всегда будет вызван.
Конечно, если ваши колбеки сами содержат JS-ошибки, вы можете не получить ожидаемый результат, но колбек и в самом деле будет вызван. Мы расскажем позже как получить уведомление об ошибке в своем колбеке, потому что даже эти ошибки не проглатываются.
Но что если сам промис никогда не будет разрешен тем или иным путем? Даже для такой ситуации у промисов есть ответ используя абстракцию более высокого порядка, называемую "гонка":
```js
// функция с промисом по тайм-ауту
function timeoutPromise(delay) {
return new Promise( function(resolve,reject){
setTimeout( function(){
reject( "Тайм-аут!" );
}, delay );
} );
}
// настройка тайм-аута для `foo()`
Promise.race( [
foo(), // попробовать вызвать `foo()`
timeoutPromise( 3000 ) // дать на это 3 секунды
] )
.then(
function(){
// `foo(..)` выполнился успешно и вовремя!
},
function(err){
// либо `foo()` завершилась неудачно, либо она
// не завершилась вовремя, так что проверьте
// `err` чтобы определить причину
}
);
```
Есть еще много нюансов, которые можно учесть при рассмотрении этого шаблона промисов с тайм-аутом, но мы вернемся к этом позже.
Важно, что мы можем гарантированно просигнализировать о результате `foo()`, чтобы предотвратить зависание нашей программы бесконечно.
### Вызывая слишком мало или много раз
По определению, *один* - это подходящее число раз, которое колбек должен быть вызван. Случай "слишком мало" будет означать ноль вызовов, что то же самое что и случай "никогда", который мы только что рассмотрели.
Случай "слишком много" легко объяснить. Промисы определены таким образом, что могут быть разрешены только один раз. Если по каким-либо причинам код создания промиса попытается вызвать `resolve(..)` или `reject(..)` несколько раз или попытается вызвать их обоих, то промис примет во внимание только первый вызов и молча проигнорирует любые последующие попытки.
Поскольку промис можно разрешить лишь раз, любые зарегистрированные колбеки `then(..)` будут вызваны только по разу (каждый).
Конечно, если зарегистрируете один и ото же колбек более одного раза, (т.е., `p.then(f); p.then(f);`), он будет вызван столько раз, сколько был зарегистрирован. Гарантия того, что функция ответа будет вызвана только раз не препятствует вам выстрелить себе в ногу.
### Сбой при передаче параметров/окружения
У промисов может быть не больше одного значения разрешения (завершение или отказ).
Если вы не разрешаете промис явно с конкретным значением, значение будет `undefined`, как и обычно в таких случаях в JS. Но если ли значение или нет, оно всегда будет передавно во все зарегистрированные (и корректные: завершение или отказ) колбеки, либо *сейчас*, или в будущем.
Что-то, о чем нужно знать: если вы вызываете `resolve(..)` или `reject(..)` с несколькими параметрами, все параметры, которые следуют за первым, будут молча проигнорированы. Хотя это и может выглядеть как нарушение гарантии, которую мы только что описали, это не совсем так потому что it это представляет собой недопустимое использование механизма промисов. Другие недопустимые случаи использования API (такие как вызов `resolve(..)` несколько раз) *защищены* похожим образом, таким образом поведение промисов тут будет консистентным (если не немного расстраивающим).
Если вы хотите передать несколько значений, вы должны обернуть их в еще одно одиночное значение, которое вы и передадите, такое как `array` (массив) или `object` (объект).
Что касается окружения, функции в JS всегда сохраняют свое замыкание области видимости, в которой они определены (см. *Область видимости и замыкания* в этой серии книг), так что у них, конечно, остается доступ к тому окружающему состоянию, которое вы предоставляете. Конечно, то же самое справедливо и для подхода с одними колбеками, поэтому это не какое-то особенная выгодная добавка от промисов, но это гарантия, на которую мы можем тем ни менее положиться.
### Проглатывание любых ошибок/исключений
В основе, это просто переформулирование предыдущего пункта. Если вы отклоняете промис с определенной *причиной* (т.е. сообщением об ошибке), то это значение будете передано в колбек(и) отказа.
Но здесь есть нечто гораздо большее. Если в любой точке создания промиса или при исследовании его разрешения произойдет исключение JS, такое как `TypeError` или `ReferenceError`, такое исключение будет захвачено и это заставит завершиться с отказом рассматриваемый промис.
Например:
```js
var p = new Promise( function(resolve,reject){
foo.bar(); // `foo` не определена поэтому ошибка!
resolve( 42 ); // никогда не достигнет этой точки :(
} );
p.then(
function fulfilled(){
// никогда не достигнет этой точки :(
},
function rejected(err){
// `err` будет объектом исключения `TypeError`
// произошедшим на строке `foo.bar()`.
}
);
```
JS-исключение, которое возникает от `foo.bar()`, становится отказом промиса, который вы можете захватить и обработать.
Это - важная деталь, потому что она успешно решает еще один потенциальный Залго-момент, который заключается в том, что эти ошибки повлечь синхронную реакцию тогда как не-ошибки будут асинхронными. Даже JS-исключения промисы превращают в асинхронное поведение, тем самым значительно уменьшая шансы появления состояния гонки.
Но что произойдет если промис завершен, а произошло JS-исключение во время обработки результата (в зарегистрированном колбеке `then(..)`)? Даже такие исключения не будут потеряны, но вы можете немного удивиться тому, как они обрабатываются, пока не вникните немного глубже в это:
```js
var p = new Promise( function(resolve,reject){
resolve( 42 );
} );
p.then(
function fulfilled(msg){
foo.bar();
console.log( msg ); // никогда не достигнет этой точки :(
},
function rejected(err){
// никогда не достигнет и этой точки :(
}
);
```
Подождите-ка, тут похоже, что исключение от `foo.bar()` и в самом деле проглочено. Без паники, оно не пропало. Но кое-что внутри не так, а именно то, что мы не смогли получить уведомление об этом. Вызов `p.then(..)` сам по себе возвращает другой промис и это *тот самый* промис, который будет отклонен с исключением `TypeError`.
Так почему же он не может просто вызвать обработчик ошибок, который мы там объявили? Похоже, что логичное объяснение лежит на поверхности. Но оно нарушило бы основополагающий принцип промисов - **неизменность** после разрешения. `p` уже была завершена со значением `42`, так что она не может быть позднее изменена на отказ только потому, что возникла ошибка в наблюдающей функции разрешения `p`.
Кроме нарушения принципа, такое поведение может нанести ущерб, если скажем было несколько зарегистрированных колбеков `then(..)` для промиса `p`, поскольку тогда некоторые будут вызваны, а другие - нет, и это было очень непрозрачно в плане причины почему так произошло.
### Надежный промис?
Есть одна последняя деталь, чтобы понять как установить доверие, основанное на шаблоне промисов.
Без сомнения вы заметили, что промисы не избавились от колбеков полностью. Они просто поменяли место, куда передается колбек. Вместо передачи колбека в `foo(..)`, мы получаем *нечто* (предположительно настоящий промис) обратно из `foo(..)`, и мы взамен передаем колбек в это *нечто*.
Но почему это было бы надежнее, чем просто колбеки сами по себе? Как мы можем быть уверены в точ, что *нечто*, что мы получаем в ответ на самом деле надежный промис? Не является ли всё это в сущности просто карточным домиком, где мы можем доверять только потому, что мы уже доверяем?
Одна из самых важных, но часто незаслуженно обойденных вниманием деталей промисов - это то, что у них есть решение также и этой проблемы. Включенное в нативную реализацию ES6 `Promise` - `Promise.resolve(..)`.
Если вы передаете непосредственное, не являющееся ни промисом, ни then-содержащим, значение в `Promise.resolve(..)`, вы получили промис, который завершен с этим значением. Другими словами, эти два промиса `p1` и `p2` будут вести себя практически идентично:
```js
var p1 = new Promise( function(resolve,reject){
resolve( 42 );
} );
var p2 = Promise.resolve( 42 );
```
Но если вы передадите настоящий промис в `Promise.resolve(..)`, вы просто получите тот же промис обратно:
```js
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( p1 );
p1 === p2; // true
```
Что еще более важно, если вы передаете не-промис then-содержащее значение в `Promise.resolve(..)`, оно попытается распаковать это значение, и распаковка будет продолжаться до тех пор, пока не будет извлечено конкретное окончательное не-промис значение.
Помните наше прошедшее обсуждение then-содержащих?
Рассмотрите:
```js
var p = {
then: function(cb) {
cb( 42 );
}
};
// это сработает, но только благодаря удаче
p
.then(
function fulfilled(val){
console.log( val ); // 42
},
function rejected(err){
// никогда не достигнет этой точки
}
);
```
Это `p` - then-содержащее, но это не настоящий промис. К счастью, оно разумное, как и большинство какие бывают. Но что если вы получите в ответ что-то, что выглядит как-то так:
```js
var p = {
then: function(cb,errcb) {
cb( 42 );
errcb( "злобный смех" );
}
};
p
.then(
function fulfilled(val){
console.log( val ); // 42
},
function rejected(err){
// ой, не должно было сработать
console.log( err ); // злобный смех
}
);
```
Это `p` - then-содержащее, но оно не ведет себя как хороший промис. Вредоносное ли оно? Или просто игнорирует то, как должны работать промисы? Это не важно, если честно. В любом случае, оно не надежно в том виде, как есть.
Тем ни менее, мы может передать любую их этих версий `p` в `Promise.resolve(..)`, и мы получим нормализованный, безопасный результат, который и ожидаем:
```js
Promise.resolve( p )
.then(
function fulfilled(val){
console.log( val ); // 42
},
function rejected(err){
// никогда не достигнет этой точки
}
);
```
`Promise.resolve(..)` примет любые then-содержащие аргументы, и распакует их в их не-then-содержащее значение. Но вы получите обратно из `Promise.resolve(..)` настоящий, подлинный промис, **тот, которому вы можете доверять**. Если то, что вы передали, уже является настоящим промисом, вы просто получите его же обратно, так что нет никаких недостатков в том, что фильтровать через `Promise.resolve(..)`, чтобы получить надежность.
Допустим мы вызываем `foo(..)` и не уверены, что можем доверять его возвращаемому значению в том, что оно является правильным промисом, но мы знаем, что оно как минимум then-содержащее. `Promise.resolve(..)` даст нам надежную обертку в виде промиса, которую можно использовать в цепочке:
```js
// не делайте так:
foo( 42 )
.then( function(v){
console.log( v );
} );
// вместо этого, делайте так:
Promise.resolve( foo( 42 ) )
.then( function(v){
console.log( v );
} );
```
**Примечание** Еще один выгодный побочный эффект оборачивания `Promise.resolve(..)` вокруг любого возвращаемого из функции значения (then-содержащей или нет) - это то, что это легкий путь к приведению этого вызова функции к правильно ведущей себя асинхронной задаче. Если `foo(42)` иногда возвращает непосредственное значение, а иногда промис, `Promise.resolve( foo(42) )` следит за тем, чтобы это всегда был в результате промис. И избегая Залго приводит к намного лучшему коду.
### Построенное доверие
Надеюсь, что предыдущая дискуссия теперь полностью "разрешает" (каламбур) в вашим мозгу факт, почему промис надежен, и более важно, почему это доверие так критично для построения надежного, поддерживаемого ПО.
Можете ли вы писать асинхронный код в JS без какой-либо надежности? Конечно можете. Мы, JS-разработчики, пишем асинхронный код не имея ничего кроме колбеков уже почти два десятилетия.
Но как только вы начинаете задавать вопросы просто о том, насколько вы можете доверять механизмам, на которые вы опираетесь, в самом деле быть предсказуемыми и надежными, вы начинаете осознавать, что у колбеков довольно шаткий фундамент доверия.
Промисы - это шаблон, который дополняет колбеки надежной семантикой, так что поведение становится более разумным и надежным. Сделав разинверсию *инверсии управления* колбеками, мы передаем управление надежной системе (промисам), которая была специально разработана, чтобы привнести здравый смысл в нашу асинхронность.
## Цепочечный поток
Мы уже намекали на это пару раз, но промисы - это не только механизм для одношаговой операции типа *это-затем-то*. Это строительный блок, конечно, но оказывается мы можем связать немного промисов вместе для представления последовательности асинхронных шагов.
Ключ к тому, чтобы это сработало, построен на двух видах поведения, присущих промисам:
* Каждый раз как вы вызываете `then(..)` у промиса, он создает и возвращает новый промис, с которым мы можем составить *цепочку*.
* Какое бы значение вы ни вернули из колбека завершения `then(..)` (первый параметр) - оно автоматически устанавливается как set как результата нормального завершения промиса *в цепочке* (из первого пункта).
Давайте сперва проиллюстрируем что это значит, а *затем* (then, туту игра слов) вы выведем как это помодет нам создавать асинхронные последовательности управления потоком. Рассмотрим следующее:
```js
var p = Promise.resolve( 21 );
var p2 = p.then( function(v){
console.log( v ); // 21
// завершить `p2` со значением `42`
return v * 2;
} );
// составляем цепочку с `p2`
p2.then( function(v){
console.log( v ); // 42
} );
```
Возвращая `v * 2` (i.e., `42`), мы завершаем промис `p2` с успешным результатом, который создал и вернул первый вызов `then(..)`. Когда происходит вызов `then(..)` у `p2`, он получает результат из выражения `return v * 2`. Конечно, `p2.then(..)` создает еще один промис, который мы можем сохранить в переменной `p3`.
Но немного раздражает необходимость создавать промежуточную переменную `p2` (или `p3`, и т.д.). К счастью, мы легко можем объединить их в цепочку:
```js
var p = Promise.resolve( 21 );
p
.then( function(v){
console.log( v ); // 21
// вернуть результат промиса в цепочке со значением `42`
return v * 2;
} )
// вот и промис в цепочке
.then( function(v){
console.log( v ); // 42
} );
```
Таким образом теперь первый `then(..)` - это первый шаг в асинхронной последовательности, а второй `then(..)` - это второй шаг. Это может продолжаться столь долго, сколь вам надо это расширять. Просто продолжайте цепочку от предыдущего `then(..)` автоматически созданным промисом.
Но здесь нет кое-чего. Что если мы хотим, чтобы шаг 2 ждал пока шаг 1 выполнит что-то асинхронное? Мы используем выражение `return` для возврата значения сразу, которое немедленно и завершает промис в цепочке.
Ключ к тому, чтобы заставить последовательность промисов быть истинно асинхронной на каждом шаге - это вспомнить как работает `Promise.resolve(..)` когда то, что вы передаете ему - это промис или then-содержащее вместо конечного значения. `Promise.resolve(..)` прямо возвращает полученный настоящий промис или распаковывает значение полученного then-содержащего и движется рекурсивно пока может распаковывать then-содержащие.
Такой же вид распаковки происходит если вы используете в `return` then-содержащее или промис из обработчика завершения (или отказа). Рассмотрим:
```js
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// создаем промис и возвращаем его
return new Promise( function(resolve,reject){
// завершение со значением `42`
resolve( v * 2 );
} );
} )
.then( function(v){
console.log( v ); // 42
} );
```
Несмотря на то, что мы обернули `42` в промис, который вернули, оно все еще распаковано и оказалось в качестве разрешения промиса в цепочке, такого как второй `then(..)`, который тем ни менее получил `42`. Если мы добавим асинхронность для этого оборачивающего промиса, все всё еще будет работать по-прежнему одинаково:
```js
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// создаем промис и возвращаем его
return new Promise( function(resolve,reject){
// добавляем асинхронность!
setTimeout( function(){
// завершение со значением `42`
resolve( v * 2 );
}, 100 );
} );
} )
.then( function(v){
// запускается после задержки 100мс на предыдущем шаге
console.log( v ); // 42
} );
```
Это невероятно мощно! Теперь мы можем строить последовательность со сколь угодно большим количеством асинхронных шагов и каждый шаг может задержать следующий шаг (или не задержать!), если необходимо.
Конечно, значение, передаваемое из шага в шаг в этих примерах, не обязательное. Если вы не вернете явное значение, будет предполагаться неявное значение `undefined` и промисы все еще будут в цепочке таким же образом. Разрешение каждого промиса, таким образом, является просто сигналом перейти к следующему шагу.
Для дальней иллюстрации цепочки, давайте обобщим создание промиса с задержкой (без вывода сообщений о завершении) в функцию, которую мы можем переиспользовать для нескольких шагов:
```js
function delay(time) {
return new Promise( function(resolve,reject){
setTimeout( resolve, time );
} );
}
delay( 100 ) // шаг 1
.then( function STEP2(){
console.log( "шаг 2 (после 100мс)" );
return delay( 200 );
} )
.then( function STEP3(){
console.log( "шаг 3 (после еще 200мс)" );
} )
.then( function STEP4(){
console.log( "шаг 4 (следующая задача)" );
return delay( 50 );
} )
.then( function STEP5(){
console.log( "шаг 5 (после еще 50мс)" );
} )
...
```
Вызов `delay(200)` создает промис, который завершится через 200мс, а затем вы вернем его из первого колбека завершения `then(..)`, который приведет ко второму промису от `then(..)`, чтобы подождать этот 200мс-промис.
**Примечание** Как было описано, технически есть два промиса в этом обмене: промис с 200мс-задержкой и промис в цепочке, к которому присоединяется второй `then(..)`. Но возможно вам будет проще мысленно объединить эти два промиса вместе, потому что механизм промисов автоматически объединим их состояния для вас. В этом отношении, вы можете думать о `return delay(200)` как о создании промиса, который заменяет ранее возвращенный промис в цепочке.
Хотя, если честно, последовательности задержек без передачи полезной нагрузки не очень полезный пример управления потоком промисов. Давайте взглянем на сценарий, который выглядит чуточку более практично.
Вместо таймеров, давайте рассмотрим возможность создания Ajax-запросов:
```js
// предположим есть функция `ajax( {url}, {callback} )`
// Ajax с учетом промисов
function request(url) {
return new Promise( function(resolve,reject){
// колбек `ajax(..)` должен стать нашей
// функцией `resolve(..)` промиса
ajax( url, resolve );
} );
}
```
Сначала мы определяем функцию `request(..)`, которая создает промис для представления завершения вызова `ajax(..)`:
```js
request( "http://some.url.1/" )
.then( function(response1){
return request( "http://some.url.2/?v=" + response1 );
} )
.then( function(response2){
console.log( response2 );
} );
```
**Примечание** Разработчики часто сталкиваются с ситуациями, когда они хотят получить асинхронное управление потоком промисов для функций, которые сами по себе не поддерживают промисы (как `ajax(..)` здесь, который ожидает на входе колбек). Хотя нативный механизм ES6 `Promise` не решает автоматически этот шаблон за нас, практически все промис-библиотеки *решают*. Они обычно называют этот процесс "lifting" (поднятие) или "promisifying" (промисифицирование) или вариации на тему. Мы вернемся к этой технике позже.
Используя `request(..)`, умеющий возвращать промис, мы неявно создаем первый шаг в нашей цепочке вызывая его с первым URL, и объединяем в цепочку этот возвращенный промис с первым `then(..)`.
Как только возвращается `response1`, мы используем это значение чтобы создать второй URL и выполнить второй вызов `request(..)`. Этот второй промис из `request(..)` - возвращен `return`ом таким образом, что третий шаг в нашем асинхронном контроле потока ждет завершения этого Ajax-вызова. Наконец, мы выводим `response2` как только он возвращен.
Цепочка промисов, которую мы создали - не только управление потоком, которое отражает многошаговую асинхронную последовательность, но и также действует как канал сообщений для передачи их от шага к шагу.
Что если что-то пойдет не так в одном из шагов цепочки промисов? Ошибка/исключение соединены со своим промисом, что означает, что есть возможность поймать такую ошибку в любом месте цепочки и такой захват действует как бы как "сброс" цепочки обратно к нормальному функционированию в этой точке:
```js
// шаг 1:
request( "http://some.url.1/" )
// шаг 2:
.then( function(response1){
foo.bar(); // undefined, ошибка!
// никогда не достигнет этой точки
return request( "http://some.url.2/?v=" + response1 );
} )
// шаг 3:
.then(
function fulfilled(response2){
// никогда не достигнет этой точки
},
// обработчик отказа, чтобы поймать ошибку
function rejected(err){
console.log( err ); // `TypeError` из-за ошибки на `foo.bar()`
return 42;
}
)
// шаг 4:
.then( function(msg){
console.log( msg ); // 42
} );
```
Когда происходит ошибка на шаге 2, обработчик отказа в шаге 3 ловит ее. Возвращаемое значение (`42` в этом примере кода), если таковое есть, из обработчика отказа завершает промис для следующего шага (4), так, чтобы цепочка вернулось обратно в состояние завершения.
**Примечание** Как мы уже говорили ранее, при возврате промиса из обработчика завершения, этот промис не обернут и может задержать следующий шаг. Это также верно при возврате промисов из обработчиков отказа, так что если `return 42` на шаге 3 вернет вместо этого промис, этот промис может задержать шаг 4. Выброшенное исключение внутри либо обработчика завершения, либо отказа в вызове `then(..)` приведет к тому, что следующий (в цепочке) промис будет немедленно отвергнут с этим же исключением.
Если вы вызовете `then(..)` у промиса и передадите только обработчик завершения в него, будет подставлен неявный обработчик отказов:
```js
var p = new Promise( function(resolve,reject){
reject( "Ой" );
} );
var p2 = p.then(
function fulfilled(){
// никогда не достигнет этой точки
}
// неявный обработчик отказа, если явно не указан или
// передано любое другое значение - не-функция
// function(err) {
// throw err;
// }
);
```
Как видите, неявный обработчик отказа просто повторно бросает ту же ошибку, что в итоге вынуждает `p2` (промис в цепочке) завершиться отказом с той же самой причиной в виде ошибки. По сути, это позволяет ошибке продолжить путешествовать по цепочке промисов до тех пор, пока не встретится явно заданный обработчик отказа.
**Примечание** Мы расскажем подробнее об обработке ошибок в промисах немного позже, потому что есть и другие нюансы, о которым стоит побеспокоиться.
Если в `then(..)` не передана валидная функция в качестве параметра обработчика завершения, также будет подставлен неявный обработчик:
```js
var p = Promise.resolve( 42 );
p.then(
// неявный обработчик завершения, если явно не указан или
// передано любое другое значение - не-функция
// function(v) {
// return v;
// }
null,
function rejected(err){
// никогда не достигнет этой точки
}
);
```
Как видите, обработчик завершения по умолчанию просто передает полученное значение на следующий шаг (промис).
**Примечание** Шаблон `then(null,function(err){ .. })`, обрабатывающий только отказы (если есть), но позволяющий пропускать далее завершения, имеет сокращенную форму в API: `catch(function(err){ .. })`. Мы рассмотрим `catch(..)` более полно в следующем разделе.
Давайте вкратце рассмотрим присущие промисам типы поведения, которые позволяют организовать цепочечное управление потоком:
* Вызов `then(..)` с одним промисом автоматически создает новый промис в качестве возвращаемого значения вызова.
* Внутри обработчиков завершения/отказа, если вы возвращаете значение или бросается исключение, новый возвращенный промис (который можно присоединить к цепочке) разрешается соответственно с тем же результатом.
* Если обработчик завершения или отказа возвращают промис, он не обернут, таким образом как бы он не разрешился, это станет разрешением промиса в цепочке, возвращенного из текущего `then(..)`.
Хотя цепочечное управление потоком полезное, возможно будет более точным представлять его как побочный эффект того, как промисы объединяются (составляются) вместе, нежели как основной функционал. Как мы подробно обсуждали уже несколько раз, промисы нормализуют асинхронность и скрывают состояние значения, зависимого от времени, и это *то*, что позволяет нам объединять их в цепочки таким удобным способом.
Определенно, последовательная выразительность цепочки (это-then-это-then-это...) - это большое улучшение по сравнению с запутанным клубком колбеков, как мы уже выяснили в главе 2. Но все еще есть изрядный объем шаблона (`then(..)` и `function(){ .. }`), через который нужно продираться. В следующей главе мы увидим значительно более приятный шаблон для выразительной организации последовательного управления потоком с помощью генераторов.
### Терминология: Разрешить (Resolve), Завершить (Fulfill) и Отвергнуть (Reject)
Существует небольшая путаница в терминах "разрешить (resolve)", "Завершить (fulfill)" и "отвергнуть (reject)", которую нам необходимо прояснить до того, как вы погрузитесь слишком глубоко в изучении промисов. Давайте сначала рассмотрим конструктор `Promise(..)`:
```js
var p = new Promise( function(X,Y){
// X() для завершения
// Y() для отказа
} );
```
Как видите, переданы два колбека (здесь помечены как `X` и `Y`). Первый *обычно* используется для отметки того, что промис завершен, а второй *всегда* помечает промис как отвергнутый. Но о чем это "обычно" и что это означает для точного именования этих параметров?
В конечном счете, это только ваш код и имена идентификаторов не интерпретируются JS-движком как что-то значимое, так что *технически* это не имеет значения; `foo(..)` и `bar(..)` одинаково функциональны. Но слова, которыми вы пользуетесь, могут затронуть не только как вы думаете о коде, но и как другие разработчики в вашей команде будут думать о нем. Думая неправильно о тщательно организованном асинхронном коде - это почти наверняка будет хуже, чем альтернативы из спагетти-колбеков.
Так что на самом деле имеет значение, как вы их называете.
Со вторым параметром легко определиться. Почти вся литература использует `reject(..)` (отвергнуть) как его имя и поскольку это в точности (и только это!) то, что он делает, это и есть очень хороший выбор для этого имени. Я бы настоятельно рекомендовал вам всегда использовать `reject(..)`.
Но вокруг первого параметра чуть больше неясностей, который в литературе о промисах часто обозначается `resolve(..)` (разрешить). Это слово очевидно связано с "resolution" (разрешение), которое и используется во всей литературе (включая эту книгу), чтобы описать установку конечного значения/состояния в промисе. Мы уже использовать "разрешить промис" несколько раз, чтобы обозначить либо завершение, или отвергнутый промис.
Но если этот параметр, по-видимому, используется для конкретно завершения промиса, почему бы не назвать его `fulfill(..)` (завершить) вместо `resolve(..)` (разрешить), чтобы быть более точным? Чтобы ответить на этот вопрос, давайте также взглянет на два метода `Promise` API:
```js
var fulfilledPr = Promise.resolve( 42 );
var rejectedPr = Promise.reject( "Ой" );
```
`Promise.resolve(..)` создает промис, который разрешен со переданным значением. В этом примере, `42` - это обычное, не-промис, не-then-содержащее значение, поэтому завершенный промис `fulfilledPr` создан для значения `42`. `Promise.reject("Ой")` создает отвергнутый промис `rejectedPr` для причины `"Ой"`.
Давайте теперь проиллюстрируем, почему слово "resolve" (разрешить) (такое как в `Promise.resolve(..)`) - является однозначным и более точным, если используется явно в контексте, который должен либо завершиться, либо закончиться отказом:
```js
var rejectedTh = {
then: function(resolved,rejected) {
rejected( "Ой" );
}
};
var rejectedPr = Promise.resolve( rejectedTh );
```
Как мы уже говорили ранее в этой главе, `Promise.resolve(..)` вернет полученный настоящий промис напрямую или распакует полученное then-содержащее. Если распаковка этого then-содержащего покажет отвергнутое состояние, то промис, который был возвращен из `Promise.resolve(..)` - по факту в том же самом отвергнутом состоянии.
Таким образом `Promise.resolve(..)` - это хорошее, точное название для метода API, потому что он может либо завершиться, либо будет отвергнутым.
Первый колбек-параметр конструктора `Promise(..)` распакует либо then-содержащее (идентично `Promise.resolve(..)`), либо настоящий промис:
```js
var rejectedPr = new Promise( function(resolve,reject){
// разрешить этот промис отвергнутым промисом
resolve( Promise.reject( "Ой" ) );
} );
rejectedPr.then(
function fulfilled(){
// никогда не достигнет этой точки
},
function rejected(err){
console.log( err ); // "Ой"
}
);
```
Теперь должно быть ясно, что `resolve(..)` - подходящее название для для первого колбек-параметра конструктора `Promise(..)`.
**Предупреждение:** Ранее упомянутый `reject(..)` **не** выполняет распаковку, как это делает `resolve(..)`. Если вы передадите промис или then-содержащее значение в `reject(..)`, то именно это значение нетронутым будет установлено как причина отказа. Последующий обработчик отказа получит настоящий промис/then-содержащее, которое вы передали в `reject(..)`, а не его внутреннее непосредственное значение.
Но теперь давайте обратим наше внимание на колбеки, переданные в `then(..)`. Как их следует назвывать (и в литературе, и в коде)? Я бы предложил `fulfilled(..)` (завершенный) и `rejected(..)` (отвергнутый):
```js
function fulfilled(msg) {
console.log( msg );
}
function rejected(err) {
console.error( err );
}
p.then(
fulfilled,
rejected
);
```
В случае первого параметра в `then(..)` - это однозначно всегда случай завершения, поэтому нет нужны для двойственной терминологии "resolve". В качестве примечания, спецификация ES6 использует `onFulfilled(..)` и `onRejected(..)`, чтобы обозначить эти два колбека, поэтому они являются точными терминами.
## Обработка ошибок
Мы уже видели несколько примеров того, как отказ промисов, либо намеренно через вызов `reject(..)`, либо случайно через исключение JS, позволяет более разумно обрабатывать ошибки при асинхронной разработке. Давайте вернемся назад и четко сформулируем некоторые детали, которые мы пропустили.
Самая естественная форма обработки ошибок для большинства разработчиков - это синхронная конструкция `try..catch`. К сожалению, она есть только в синхронной форме, поэтому она не поможет в шаблонах асинхронного кода:
```js
function foo() {
setTimeout( function(){
baz.bar();
}, 100 );
}
try {
foo();
// позднее выбросит глобальную ошибку из `baz.bar()`
}
catch (err) {
// никогда не достигнет этой точки
}
```
Было бы неплохо иметь в арсенале `try..catch`, но он не работает для асинхронных операций. То есть, если только нет какой-то дополнительной поддержки среды, к которой мы вернемся вместе с генераторами в главе 4.
В колбеках, появились некоторые стандарты для шаблонной обработки ошибок, особенно стиль "колбек ошибки идет первым":
```js
function foo(cb) {
setTimeout( function(){
try {
var x = baz.bar();
cb( null, x ); // успех!
}
catch (err) {
cb( err );
}
}, 100 );
}
foo( function(err,val){
if (err) {
console.error( err ); // облом :(
}
else {
console.log( val );
}
} );
```
**Примечание** `try..catch` тут работает только с той точки зрения, что вызов `baz.bar()` немедленно завершится или прервется со сбоем и при этом синхронно. Если `baz.bar()` сам являлся своей же асинхронной функцией продолжения, то любые асинхронные ошибки внутри него нельзя будет поймать.
Колбек, который мы передаем в `foo(..)`, ожидает получить сигнал об ошибке с помощью зарезервированного первого параметра `err`. Если присутствует, предполагается ошибка. Если нет, то предполагается завершение.
Такой тип обработки ошибок технически *поддерживает асинхронность*, но он совсем не способен к композиции. Множественные уровни колбеков в стиле "ошибка первая" сплетенные вместе с этими вездесущими операторами проверок `if` неизбежно приведет вас к опасностям, связанным с адом колбеков (см. главу 2).
Таким образом вы возвращаемся к обработке ошибок с помощью промисов, с передачей обработчика отказов в `then(..)`. Промисы не используют популярный стиль дизайна "колбек с первым параметром-ошибкой", а вместо этого используют стиль "раздельных колбеков", есть один колбек для завершения и другой - для отказа:
```js
var p = Promise.reject( "Ой" );
p.then(
function fulfilled(){
// никогда не достигнет этой точки
},
function rejected(err){
console.log( err ); // "Ой"
}
);
```
Несмотря на то, что этот шаблон обработки ошибок, на первый взгляд, имеет смысл, нюансы обработки ошибок в промисах зачастую гораздо сложнее полностью осознать.
Представьте:
```js
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// числа не содержат функцией как у строк,
// поэтому тут возникнет ошибка
console.log( msg.toLowerCase() );
},
function rejected(err){
// никогда не достигнет этой точки
}
);
```
Если `msg.toLowerCase()` законно выдает ошибку (и это действительно так!), почему же наш обработчик ошибок не вызван? Как мы объясняли ранее, так происходит потому, что *этот* обработчик ошибок - для промиса `p`, который уже был разрешен со значением `42`. Промис `p` - неизменяемый, поэтому единственный промис, который может получить ошибку, тот, что возвращается из `p.then(..)`, который мы в этом случае никак не ловим.
Это должно дать четкое представление о том, почему обработка ошибок в промисах подвержена ошибкам (каламбур). Слишком легко допустить, чтобы ошибки были "проглочены", так как это очень редко то, что вы задумывали изначально.
**Предупреждение:** Если вы используете API промисов неправильным путем и происходит ошибка, то это препятсвует правильному созданию промиса, в результате будет полученное сращу же исключение, но **не отвергнутый промис**. Некоторые примеры некорректного использования, который ломают создание промиса: `new Promise(null)`, `Promise.all()`, `Promise.race(42)`, и т.д.. Вы не сможете получить отвергнутый промис, если вы не используете API промисов достаточно корректно, чтобы на само деле создать в первую очередь сам промис!
### Яма отчаяния
Джефф Этвуд заметил несколько лет назад: языки программирования часто настроены настоены по умолчанию таким образом, что разработчки попадают в "яму отчаяния" (http://blog.codinghorror.com/falling-into-the-pit-of-success/), где за инциденты наказывают и что нужно больше стараться, чтобы все получилось. Он призвал нас вместо этого создавать "яму успеха," когда по умолчанию вы попадаете в ожидаемое (успешное) действие, и, следовательно, должны сильно постараться, чтобы потерпеть неудачу.
Обработка ошибок в промисах - несомненно, является дизайном "ямы отчаяния". По умолчанию, он предполагает, что вы хотите, чтобы любые ошибки были поглощены состоянием промиса и если вы забудете исследовать этот состояние, ошибка тихо томится/умирает в безвестности, обычно отчаяния.
Чтобы избежать ошибки из-за молчания забытого/заброшенного промиса, некоторые разработчики заявили, что "лучшей практикой" для цепочек промисов является всегда завершать цепочку заключительным `catch(..)`, например:
```js
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// числа не содержат функцией как у строк,
// поэтому тут возникнет ошибка
console.log( msg.toLowerCase() );
}
)
.catch( handleErrors );
```
Поскольку мы не передали обработчик отказов в `then(..)`, был подставлен обработчик по умолчанию, который просто передает ошибку следующему промису в цепочке. Таким образом, обе ошибки, идущие в `p`, и ошибки, которые появляются *после* `p` в его разрешении (как в `msg.toLowerCase()`) будут отфильтрованы до конечного `handleErrors(..)`.
Проблема решена, не так ли? Не так быстро!
Что случится, если `handleErrors(..)` сам содержит ошибку? Кто отловит ее? Остался еще один невыполненный промис: тот, который возвращает `catch(..)`, который мы не ловим и не регистрируем обработчик отказа для него.
Вы не можете просто приклеить другой `catch(..)` в конец этой цепочки, потому что он тоже может завершиться ошибкой. Последний шаг в любой цепочек промисов, какой бы он ни был, всегда будет содержать возможность, хоть и в меньшей степени, зависнуть с непойманной ошибкой, застрявшей внутри неотслеживаемого промиса.
Все еще звучит как невыполнимая головоломка?
### Обработка непойманного
Эту проблему нелегко решить полностью. Есть и другие способы достичь этого, которые, по мнению многих, являются *лучшими*.
Некоторые промис-библиотеки добавили методы для регистрации чего-то подобного "глобальному обработчику необработанных отказов", который бы вызывался вместо глобального выброса ошибки. Но их решение о том, как определить ошибку как "непойманную" - это иметь таймер произвольной длительности, скажем 3 секунды, запускаемый от момента отказа. Если промис отвергнут, но не было зарегистрировано ни одного обработчика ошибок до того, как будет запушен таймер, то предполагается, что вы будто и не регистрировали ни одного обработчика, поэтому ошибка "не поймана".
На практике, это хорошо работает для многих библиотек, поскольку большинство использованных подходов как правило не требуют значительной задержки между отказом промиса и началом наблюдения за ним. Но этот шаблон вызывает проблемы, потому что 3 секунды это очень произвольное время (пусть даже эмпирическое), а также потому, что действительно есть случаи когда вы хотите придержать отказ промиса на некоторый неопределенный период времени и вы на самом деле не хотите, чтобы ваш обработчик "непойманного" вызывался для всех этих ложных срабатываний (еще не обработанные "непойманные ошибки").
Другое более распространенное предложение заключается в том, что к промисам нужно добавить `done(..)`, что, по сути, отмечает цепочку промисов как "выполненную" (done). `done(..)` не создает и не возвращает промис, таким образом колбеки, переданные в `done(..)`, очевидно не подключены для извещения о проблемах в промис в цепочке, который не существует.
Итак, что происходит вместо этого? Она обрабатывается так, как вы обычно ожидаете в условиях не пойманной ошибки: любое исключение внутри обработчика отказа `done(..)` будет выброшено как глобальная непойманная ошибка (в консоль разработчика, по сути):
```js
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// у чисел нет функций как у строк,
// поэтому будет выброшена ошибка
console.log( msg.toLowerCase() );
}
)
.done( null, handleErrors );
// если `handleErrors(..)` породила свое собственное исключение, оно
// будет выброшено тут как глобальное
```
Это может показаться более привлекательным, чем бесконечная цепочка или произвольные тайм-ауты. Но самая большая проблема заключается в том, что это не является частью стандарта ES6, поэтому, как бы хорошо это ни звучало, в лучшем случае это еще долго не будет надежным и повсеместным решением.
Неужели мы просто застряли? Не совсем.
Браузеры имеют уникальную возможность, которой нет у нашего кода: они могут отслеживать и точно знать, когда любой объект становится ненужным и собирается сборщиком мусора. Таким образом, браузеры могут отслеживать объекты промисов и всякий раз, когда их собирает сборщик мусора, если они содержат отказ, браузер точно знает, что это была допустимая "непойманная ошибка", и поэтому может с уверенностью утверждать, что должен сообщить о ней в консоль разработчика.
**Примечание** На момент написания этой статьи, в обоих Chrome и Firefox есть ранние ранние попытки такого рода возможности "непойманный отказ", хотя поддержка в лучшем случае неполная.
Однако, если промис не был собран сборщиком мусора, что очень легко сделать случайно с помощью множества различных шаблонов разработки, осведомленность сборщика мусора браузера не поможет вам узнать и диагностировать, что у вас есть молча отвергнутый промис, находящийся рядом.
Есть ли другие альтернативы? Да.
### Яма успеха
Нижеследующее является лишь теоретическим, как промисы *могли бы* однажды изменить свое поведение. Я верю, что Я уверен, что это будет намного лучше, чем то, что мы имеем сейчас. И я думаю, что это изменение будет возможно даже в пост-ES6, потому что я не думаю, что это нарушит совместимость с ES6 промисами. Более того, это можно превратить в полифил, если вы будете осторожны. Давайте взглянем:
* Промисы могут по умолчанию сообщать (в консоль разработчика) о любом отказе в следующей задаче или тике цикла событий, если в этот момент для промиса не был зарегистрирован обработчик ошибок.
* Для случаев, когда вы хотите удерживать отвергнутый промис в таком состоянии на бесконечное количество времени до начала наблюдения за ним, вы могли бы вызвать `defer()`, который подавляет автоматическое уведомление об ошибках в этом промисе.
Если промис отвергнут, по умолчанию он шумно сообщает об этом факте в консоль разработчика (вместо тишины по умолчанию). Вы можете отказаться от такой информации, либо неявно (зарегистрировав обработчик ошибок до отказа), или явно (с помощью `defer()`). В любом случае, *вы* управляете ложными срабатываниями.
Рассмотрим:
```js
var p = Promise.reject( "Ой" ).defer();
// `foo(..)` промисоподобная
foo( 42 )
.then(
function fulfilled(){
return p;
},
function rejected(err){
// обработка ошибок в `foo(..)`
}
);
...
```
Когда мы создаем `p`, мы знаем, что мы собираемся подождать некоторое время, чтобы использовать/наблюдать за его отказом, поэтому мы вызываем `defer()` - таким образом, отсутствует глобальная отчетность. `defer()` просто возвращает тот же промис для возможности выстраивания цепочки.
Промис, возвращенный из `foo(..)`, получает обработчик ошибок, привязанный *сразу же*, поэтому она выходит "из игры" и никакой глобальной отчетности по этому поводу также не ведется.
А вот у промиса, возвращенного из вызова `then(..)`, нет ни `defer()`, ни присоединенного обработчика ошибок, поэтому если он завершается отказом (изнутри любого обработчика разрешения), то *он* будет сообщен в консоль разработчика как не пойманная ошибка.
**"Этот подход" - яма успеха.** По умолчанию, все ошибки либо обрабатываются, либо о них получаются уведомления, то, что почти все разработчики в почти всех случаях ожидали бы. Вы либо должны зарегистрировать обработчик, либо вы должны намеренно отказаться и указать, что вы намерены отложить обработку ошибок на *попозже*, вы берете дополнительную ответственность только в этом конкретном случае.
Единственная реальная опасность в этом подходе - если вы отложите (`defer()`) промис, а затем не сможете на деле вообще наблюдать/обработать его отказ.
Но вы должны были намеренно вызвать `defer()`, чтобы опуститься в эту яму отчаяния, изначально была яма успеха, поэтому мы мало что можем сделать, чтобы спасти вас от ваших собственных ошибок.
Я думаю, что все еще есть надежда на обработку ошибок промисов (пост-ES6). Я надеюсь, что власть предержащие переосмыслят ситуацию и рассмотрят эту альтернативу. Тем временем, вы можете реализовать это сами (непростое упражнение для читателя!) или использовать *более умную* библиотеку промисов, которая сделает это за вас!
**Примечание** Эта конкретная модель обработки ошибок/сообщений реализована в моей библиотеке абстракций над промисами *asynquence*, которую обсудим в приложении A этой книги.
## Шаблоны промисов
Мы уже увидено неявно шаблон последовательности в цепочках промисов (управление потоком это-затем-это-затем-то), но существует множество вариаций асинхронных шаблонов, которые мы можем построить как абстракции над промисами. Этим шаблоны служат для упрощения выражения асинхронного управления потоком, который помогает сделать наш код более более разумным и более поддерживаемым, даже в самых сложных частях наших программ.
Два таких шаблона кодируются непосредственно в нативную реализацию ES6 `Promise`, так что мы получаем их бесплатно, чтобы использовать как строительные блоки для других шаблонов.
### Promise.all([ .. ])
В асинхронной последовательности (цепочке промисов) только одна асинхронная задача координируется в любой момент времени, шаг 2 строго следует за шагом 1, а шаг 3 строго следует за шагом 2. А как насчет выполнения двух и более шагов одновременно (т.е. "параллельно")?
В классической терминологии программирования, "шлюз" - это механизм, который ожидает завершения двух или более параллельных/ одновременных задач, прежде чем продолжить работу. Не важно в каком порядке они завершатся, а важно только, что все они должны завершиться чтобы открыть шлюз и позволить потоку управлению потоком идти дальше.
В API промисов, мы называем этот шаблон `all([ .. ])`.
Скажем вы хотели сделать два Ajax-запроса в одно и то же время и дождаться окончания обоих, независимо от их порядка, до выполнения третьего Ajax-запроса. Рассмотрим:
```js
// `request(..)` - промис-совместимая Ajax-функция,
// примерно как та, что мы определяли ранее в главе
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.all( [p1,p2] )
.then( function(msgs){
// оба `p1` and `p2` завершатся успешно и передадут
// свои сообщения сюда
return request(
"http://some.url.3/?v=" + msgs.join(",")
);
} )
.then( function(msg){
console.log( msg );
} );
```
`Promise.all([ .. ])` ожидает один аргумент, `массив`, состоящий состоящий в целом из экземпляров промисов. Промис, возвращенный из вызова `Promise.all([ .. ])`, получит сообщение о завершении (`msgs` в этом примере кода), которое является `массивом` всех сообщений о завершении от переданных промисов, в том же порядке как они были переданы (независимо от порядка завершения).
**Примечание** Технически, `массив` значений, переданный в `Promise.all([ .. ])`, может содержать промисы, then-содержащие или даже непосредственные значения. Каждое значение в списке по сути, проходит через `Promise.resolve(..)`, чтобы убедиться, что ожидается настоящий промис, таким образом непосредственное значение будет просто приведено в промис для этого значения. Если `массив` пустой, основной промис немедленно завершается.
Основной промис, возвращенный из `Promise.all([ .. ])`, будет завершен только тогда и если все входящие в него промисы будут завершены. Если любой из этих промисов вместо этого отвергается, основной промис `Promise.all([ .. ])` сразу же отвергается, отбрасывая все результаты любых других промисов.
Помните о том, чтобы всегда присоединять обработчик отказа/ошибки к каждому промису, даже и особенно к тому, который возвращается из `Promise.all([ .. ])`.
### Promise.race([ .. ])
В то время как `Promise.all([ .. ])` координирует несколько обещаний одновременно и предполагает, что все они нужны для завершения, иногда вы хотите всего лишь получить ответ от "первого же промиса, пересекшего финишную линию", позволяя других промисам отпасть за ненадобностью.
Этот шаблон классически называют "задвижка" (latch), но в промисах он называется "гонка" (race).
**Предупреждение:** В то время как метафора "только первый, пересёкший финишную черту, выигрывает" хорошо соответствует поведению, к сожалению "гонка" - это своего рода нагруженный термин, потому что "состояния гонки" - обычно воспринимаются как ошибки в программах (см. главу 1). Не путайте `Promise.race([ .. ])` с "состоянием гонки" (race condition).
`Promise.race([ .. ])` также ожидает единственный аргумент в виде `массива`, содержащий один или более промисов, then-содержащих или непосредственных значений. Не имеет большого практического смысла иметь гонку с непосредственными значениями, потому что первое перечисленное значение очевидно выиграет, как в беге, где один бегун стартует с финиша!
Аналогично `Promise.all([ .. ])`, `Promise.race([ .. ])` завершится если и тогда, когда любое из разрешений промисов - успешное, и завершится отказом если и тогда, когда любое из разрешений промисов - это отказ.
**Предупреждение:** "гонка" требует по меньшей мере одного "бегуна", поэтому если вы передадите пустой `массив`, вместо немедленно разрешения, основной промис `race([..])` никогда не будет разрешен. Это программные "грабли"! ES6 должен был указать, что он либо выполняет, либо отклоняет, либо просто выбрасывает какую-то синхронную ошибку. К сожалению, из-за прецедента в библиотеках промисов, предшествующих ES6 `Promise`, им пришлось оставить эту недоработку, поэтому будьте осторожны и никогда не отправляйте пустой `массив`.
Давайте вернемся к нашему предыдущему примеру с параллельным Ajax, но в контексте гонки между `p1` и `p2`:
```js
// `request(..)` - это промис-совместимая функция,
// подобно той, что мы ранее определили в этой главе
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.race( [p1,p2] )
.then( function(msg){
// либо `p1`, либо `p2` выиграет гонку
return request(
"http://some.url.3/?v=" + msg
);
} )
.then( function(msg){
console.log( msg );
} );
```
Поскольку побеждает только один промис, значение завершения - это одно сообщение, а не `массив`, как это было в `Promise.all([ .. ])`.
#### Гонка тайм-аутов
Мы видели этот пример ранее, иллюстрирующий как `Promise.race([ .. ])` может использоваться для выражения шаблона "тайм-аут промиса":
```js
// `foo()` - функция, поддерживающая промисы
// `timeoutPromise(..)`, определенный ранее, аозвращает
// промис, который завершается отказом rejects после указанной задержки
// настроить тайм-аут для `foo()`
Promise.race( [
foo(), // попробовать вызвать `foo()`
timeoutPromise( 3000 ) // дать ему 3 секунды
] )
.then(
function(){
// `foo(..)` завершилась успешно и вовремя!
},
function(err){
// либо `foo()` завершился отказом, либо просто
// не успеет завершиться вовремя, поэтому загляните в
// `err`, чтобы узнать причину
}
);
```
Этот шаблон тайм-аута работает в большинстве случаев. Но есть некоторые нюансы, которые необходимо учитывать, и, честно говоря, они применимы к обоим `Promise.race([ .. ])` и `Promise.all([ .. ])` в равной степени.
#### "Finally"
Ключевой вопрос, который необходимо задать: "Что происходит с промисами, который который отбрасываются/игнорируются?" Мы задаем этот вопрос не с точки зрения производительности, они, как правило, попадают в сборку мусора как подходящие кандидаты для этого, а с поведенческий аспекта (побочные эффекты и т.д.). Промисы нельзя отменить, и не должны бы, поскольку это разрушит доверие к внешней неизменяемости, обсуждаемой в секции "Промис неотменяемый" позже в этой главе, поэтому их можно только молча игнорировать.
Но что если `foo()` из предыдущего примера резервирует какой-то ресурс для использования, но первым срабатывает тайм-аут и приводит к тому, что этот промис игнорируется? Есть ли в этом шаблоне что-нибудь, что с упреждением освобождает зарезервированный ресурс после истечения тайм-аута или иным образом отменяет любые побочные эффекты, которые он мог иметь? Что если всё, что вы хотели - это зафиксировать факт того, что `foo()` завершился по тайм-ауту?
Некоторые разработчики предлагают, что промису нужна регистрация колбека `finally(..)`, который вызывается всегда, когда промис разрешен, и позволяет вам и позволяет вам указать любую очистку, которая может потребоваться. На текущий момент такого нет в спецификации, но может появиться в ES7+. Подождем и посмотрим.
Это может выглядеть так:
```js
var p = Promise.resolve( 42 );
p.then( something )
.finally( cleanup )
.then( another )
.finally( cleanup );
```
**Примечание** В различных промис-библиотеках `finally(..)` все еще создает и возвращает новый промис (чтобы продолжать цепочку). Если бы функция `cleanup(..)` возвращала промис, его можно было бы соединить в цепочку, что означает, что у вас все еще могли бы быть проблемы с неразрешенными отказами, которые мы ранее обсуждали.
Тем временем, мы могли бы создать статическую вспомогательную функцию, которая позволит нам наблюдать (без вмешательства) за разрешением промиса:
```js
// защитная проверка в стиле безопасного полифила
if (!Promise.observe) {
Promise.observe = function(pr,cb) {
// стороннее наблюдение за разрешением `pr`
pr.then(
function fulfilled(msg){
// запланировать колбек асинхронно (в виде задачи)
Promise.resolve( msg ).then( cb );
},
function rejected(err){
// запланировать колбек асинхронно (в виде задачи)
Promise.resolve( err ).then( cb );
}
);
// вернуть оригинальный промис
return pr;
};
}
```
Вот как мы используем его в предыдущем примере с тайм-аутом:
```js
Promise.race( [
Promise.observe(
foo(), // попытка вызова `foo()`
function cleanup(msg){
// почистить за `foo()`, даже если она
// не завершилась после тайм-аута
}
),
timeoutPromise( 3000 ) // дать функции тайм-аут в 3 секунды
] )
```
Это вспомогательная функция `Promise.observe(..)` - просто иллюстрация того, как вы могли бы наблюдать за завершениями промисов без вмешательства в них. В других библиотеках промисов есть свои собственные решения. Независимо от того, как вы это сделаете, скорее всего, у вас будут места, где вы захотите убедиться, что ваши промисы не будут *просто* молча проигнорированы случайно.
### Вариации на тему all([ .. ]) и race([ .. ])
В то время как нативные ES6 промисы идут со встроенными `Promise.all([ .. ])` и `Promise.race([ .. ])`, есть несколько других часто используемых паттернов с вариациями этой семантики:
* `none([ .. ])` похож на `all([ .. ])`, но завершения и отказы меняются местами. Все промисы должны быть отвергнуты, отказы становятся значениями завершения, а значения завершения - наоборот.
* `any([ .. ])` похож на `all([ .. ])`, но она игнорирует любые отказы, поэтому нужно выполнить только один, а не *все*.
* `first([ .. ])` похож на гонку в сочетании с `any([ .. ])`, которая заключается в том, что она игнорирует любые отказы и завершается, как только завершается первый промис.
* `last([ .. ])` похож на `first([ .. ])`, но только побеждает самое последнее завершение.
Некоторые библиотеки абстракций над промисами обеспечивают такие функции, но вы также можете определить из сами использую механизмы промисов, `race([ .. ])` и `all([ .. ])`.
Например, вот как мы могли бы определить `first([ .. ])`:
```js
// защитная проверка в стиле безопасного полифила
if (!Promise.first) {
Promise.first = function(prs) {
return new Promise( function(resolve,reject){
// цикл по всем промисам
prs.forEach( function(pr){
// нормализовать значение
Promise.resolve( pr )
// кто завершится первым, тот и победил, и
// приводит к разрешению основного промиса
.then( resolve );
} );
} );
};
}
```
**Примечание** Такая реализация `first(..)` не завершается отказом если все ее промисы завершаются отказом; она просто зависает, подобно тому, как работает `Promise.race([])`. При необходимости, вы могли бы добавить дополнительную логику для отслеживания каждого отказа промисов и если все они отвергнуты, вызвать `reject()` для основного промиса. Оставим это как упражнение для читателя.
### Одновременные итерации
Иногда вы хотите проходить по списку промисов и выполнить некоторую задачу для них всех, так же, как это можно сделать с синхронными `array`s (например, `forEach(..)`, `map(..)`, `some(..)`, and `every(..)`). Если задача, которую нужно выполнить по отношению к каждому промису, является принципиально синхронной, это отлично работает, точно так же, как мы использовали `forEach(..)` в предыдущем отрывке кода.
Но если задачи принципиально асинхронные или могут/должны в противном случае выполняться одновременно, вы можете воспользоваться асинхронными версиями этих функций, предоставляемых многими библиотеками.
Например, давайте рассмотрим асинхронную функцию `map(..)`, которая принимает `массив` значений (могут быть промисами или чем-то еще), плюс функцию (задачу), для выполнения над каждым значением. `map(..)` сам по себе возвращает промис, чье значение завершения - `массив`, который хранит (в том же порядке) асинхронное значение завершения из каждой задачи:
```js
if (!Promise.map) {
Promise.map = function(vals,cb) {
// новый промис, который ждет все сопоставленные промисы
return Promise.all(
// примечание: обычная функция массива `map(..)`, превращает
// массив значений в массив промисов
vals.map( function(val){
// заменить `val` новым промисом, который
// разрешается после того, как `val` асинхронно отмаплена
return new Promise( function(resolve){
cb( val, resolve );
} );
} )
);
};
}
```
**Примечание** В этой реализации `map(..)` вы не можете сигнализировать об асинхронном отказе, но если происходит синхронное исключение/ошибка внутри колбека маппинга (`cb(..)`), основной промис, возвращающийся из the main `Promise.map(..)` будет отвергнут.
Давайте проиллюстрируем использование `map(..)` со списком промисов (вместо простых значений):
```js
var p1 = Promise.resolve( 21 );
var p2 = Promise.resolve( 42 );
var p3 = Promise.reject( "Ой" );
// удвоить значения в списке, даже если они в промисах
Promise.map( [p1,p2,p3], function(pr,done){
// убедиться, что само значение - это промис
Promise.resolve( pr )
.then(
// извлечь значение как `v`
function(v){
// отмапить значение завершения `v` в новое значение
done( v * 2 );
},
// или отмапить в сообщение отказа промиса
done
);
} )
.then( function(vals){
console.log( vals ); // [42,84,"Ой"]
} );
```
## Обзор API промисов
Давайте проведем обзор ES6 `Promise` API, которые мы уже в какой-то степени наблюдали в этой главе.
**Примечание** Следующий API является нативным только в ES6, но есть полифилы, совместимые со спецификацией (а не просто расширенные библиотеки промисов), которые могут определить `Promise` и все связанное с ним поведение, так что вы можете использовать нативные промисы даже в до-ES6 браузерах. Один такой полифил - это "Native Promise Only" (http://github.com/getify/native-promise-only), который написал я!
### Конструктор new Promise(..)
*Доступный конструктор* `Promise(..)` должен использоваться с `new` и в него нужно передать колбек-функцию, которая вызывается синхронно/немедленно. Это функция передается в в два колбека, которые действуют как возможности для разрешения промиса. Мы обычно называем их `resolve(..)` и `reject(..)`:
```js
var p = new Promise( function(resolve,reject){
// `resolve(..)` чтобы разрешить/завершить промис
// `reject(..)` чтобы отвергнуть промис
} );
```
`reject(..)` просто отвергает промис, а `resolve(..)` может либо завершить промис или отвергнуть его, в зависимости от того, что передано на вход. Если в `resolve(..)` передано непосредственное, не-промис, не-then-содержащее значение, то промис завершается с этим значением.
Но если в `resolve(..)` передается настоящий промис или then-содержащее значение, то это значение будет рекурсивно распаковано, и какое бы ни было его окончательное разрешение/состояние - оно будет принято промисом.
### Promise.resolve(..) и Promise.reject(..)
Краткий вариант для создания уже отвергнутого промиса - `Promise.reject(..)`, таким образом эти два промиса равнозначны:
```js
var p1 = new Promise( function(resolve,reject){
reject( "Ой" );
} );
var p2 = Promise.reject( "Ой" );
```
`Promise.resolve(..)` обычно используется для создания уже завершенного промиса примерно также как `Promise.reject(..)`. Однако, `Promise.resolve(..)` также распаковывает then-содержащие значения (как уже неоднократно обсуждалось). В этом случае, возвращенный промис принимает окончательное разрешение then-содержащего, которое вы передали, которое может быть либо завершением, либо отказом:
```js
var fulfilledTh = {
then: function(cb) { cb( 42 ); }
};
var rejectedTh = {
then: function(cb,errCb) {
errCb( "Ой" );
}
};
var p1 = Promise.resolve( fulfilledTh );
var p2 = Promise.resolve( rejectedTh );
// `p1` станет завершенным промисом
// `p2` станет отвергнутым промисом
```
И помните, `Promise.resolve(..)` ничего не дает, если то, что вы передаете ему, является настоящим промисом, она просто непосредственно вернет это значение. Поэтому нет никаких затрат на вызов `Promise.resolve(..)` со значениями чью природу вы не знаете, если они уже оказались настоящими промисами.
### then(..) и catch(..)
Каждый экземпляр промиса (но **не** пространства имен `Promise` API) содержит методы `then(..)` и `catch(..)`, которые позволяют зарегистрировать обработчики завершения и отказа для этого промиса. Как только промис разрешен, будет вызван один из этих обработчиков, но не оба, и он всегда будет вызваться асинхронно (см. "Задачи" в главе 1).
`then(..)` принимает один или два параметра, первый - для колбека завершения, а второй - для колбека отказа. Если какой-либо из параметров будет опущен или будет передано значение не-функция, то подставляется колбек по умолчанию соответственно. Колбек завершения по умолчанию просто передает сообщение дальше, в то время как колбек отказа по умолчанию просто заново выбрасывает (распространяет дальше) полученную причину ошибки (propagates).
`catch(..)` принимает только колбека отказа как параметр и автоматически подставляет обработчик завершения по умолчанию, как только что упоминалось. Другими словами, это - эквивалент `then(null,..)`:
```js
p.then( fulfilled );
p.then( fulfilled, rejected );
p.catch( rejected ); // или `p.then( null, rejected )`
```
`then(..)` и `catch(..)` также создают и возвращают новый промис, который можно использовать для выражения управления потоком цепочки промисов. Если у колбеков завершения или отказа уже есть выброшенное исключение, то возвращаемый промис будет отвергнут. Если какой-либо из колбеков вернет непосредственное, не-промис, не-then-содержащее значение, то это значение будет установлено как завершение возвращенного промиса. Если обработчик завершения специально возвращает промис или then-содержащее значение, то это значение распаковывается и становится разрешением возвращенного промиса.
### Promise.all([ .. ]) и Promise.race([ .. ])
Статические вспомогательные функции `Promise.all([ .. ])` и `Promise.race([ .. ])` в ES6 `Promise` API обе создают промис как свое возвращаемое значение. Разрешение этого промиса целиком управляется массивом промисов, который вы передаете на вход.
Для `Promise.all([ .. ])` все промисы, которые вы передаете, должны завершиться, чтобы возвращаемый промис также завершился. Если какой-либо из промисов будет отвергнут, главный возвращаемый промис будет также немедленно отвергнут (отбрасывая результаты любых других промисов). При завершении вы получаете `массив` всех переданных в промисы значений завершения. При отказе, вы получаете только значение причины отказа первого промиса. Этот шаблон классически называется "шлюз (gate)": все должны прибыть до того, как откроется шлюз.
Для `Promise.race([ .. ])`, только первый промис с разрешением (завершение или отказ) "выигрывает", и какое бы ни было его разрешение, оно и становится разрешением возвращаемого промиса. Этот шаблон классически называется "задвижка (latch)": первый, кто откроет задвижку - проходит. Представьте:
```js
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( "Привет, мир" );
var p3 = Promise.reject( "Ой" );
Promise.race( [p1,p2,p3] )
.then( function(msg){
console.log( msg ); // 42
} );
Promise.all( [p1,p2,p3] )
.catch( function(err){
console.error( err ); // "Ой"
} );
Promise.all( [p1,p2] )
.then( function(msgs){
console.log( msgs ); // [42,"Привет, мир"]
} );
```
**Предупреждение:** Будьте осторожны! Если в `Promise.all([ .. ])` будет передает пустой массив, то функция завершится немедленно, а вот `Promise.race([ .. ])` повиснет навсегда и никогда не разрешится.
ES6 `Promise` API довольно простое и понятное. Оно, по крайней мере, достаточно хорош, чтобы обслуживать самые простые случаи асинхронной работы и это хорошее место для начала перестройки вашего кода из ада колбеков в нечто лучшее.
Но существует целый ряд асинхронных сложностей, которые часто требуются приложениям и для которых промисы сами по себе будут ограничены в решении. В следующем разделе мы погрузимся в эти ограничения как мотивацию для создания библиотек промисов.
## Ограничения промисов
Многие детали, которые мы обсудим в этом разделе, уже упоминались в этой главе, но мы просто рассмотрим эти ограничения отдельно.
### Обработка ошибок последовательностей
Мы подробно рассмотрели обработку ошибок с помощью промисов в начале этой главы. Ограничения в том, как разработаны промисы, в частности, как они связываются в цепочки, создают очень легкую ловушку, когда ошибка в цепочке промисов может быть молча проигнорирована случайно.
Но есть еще кое-что, что следует учитывать с ошибками промисов. Поскольку цепочка промисов - это не что иное, как входящие в нее промисы, соединенные вместе, нет никакой сущности, чтобы ссылаться на всю цепочку как на единое *нечто*, что означает, что нет внешнего способа наблюдения за возможными ошибками.
Если вы конструируете цепочку промисов, в которой нет обработки ошибок, любая ошибка где бы то ни было в цепочке будет распространяться бесконечно вниз по цепочке, пока не будет замечена (регистрацией обработчика отказов на каком-либо шаге). Так что, в этом конкретном случае, иметь ссылку на *последний* промис в цепочке будет достаточно (`p` в последующем примере кода), потому что вы можете зарегистрировать там обработчик отказов, и он будет уведомлен о любых распространяемых ошибках:
```js
// `foo(..)`, `STEP2(..)` и `STEP3(..)` -
// обе функции поддерживают промисы
var p = foo( 42 )
.then( STEP2 )
.then( STEP3 );
```
Хотя это может показаться хитрым и запутанным, `p` здесь не указывает на первый промис в цепочке (тот, что получен из вызова `foo(42)`), а вместо этого из последнего промиса, того, который возвращается из вызова `then(STEP3)`.
Кроме того, ни один шаг в цепочке промисов не выполняет собственную обработку ошибок. Это означает, что вы могли бы зарегистрировать обработчик ошибок отказа для `p` и он получит уведомление если возникнет какая-нибудь ошибка в цепочке:
```
p.catch( handleErrors );
```
Но если любой этап цепочки фактически выполняет собственную обработку ошибок (возможно скрыто/абстрагировано от того, что вы можете увидеть), ваш `handleErrors(..)` не получит уведомлений. Возможно это и будет то, что вы хотите, это же был, в конце концов, "обработанный отказ", но это также может и *не* быть тем, что вы хотели. Полное отсутствие возможности получать уведомления (об "уже обработанных" ошибках отказа) является ограничением, которое сужает возможности в некоторых сценариях использования.
По сути, это то же самое ограничение, которое существует для блока `try..catch`, который может поймать исключение и просто проглотить его. Таким образом, это не является ограничением **специфичным для промисов**, но это *то*, для чего мы могли бы захотеть наличия обходного пути.
К сожалению, во многих случаях не сохраняются ссылки на промежуточные шаги в последовательности цепочки промисов, поэтому без таких ссылок вы не сможете подключить обработчики ошибок для надежного наблюдения за ошибками.
### Единственное значение
У промисов по определению есть только одно значение завершения или одна причина отказа. В простых примерах, это не так уж и важно, но в более сложных сценариях вы можете счесть это ограничением.
Типичный совет - создать обертку для значений (такую как `объект` или `массив`), чтобы хранить эти многочисленные сообщения. Это решение работает, но это может быть довольно неудобно и утомительно - упаковывать и распаковывать ваши сообщения на каждом шаге вашей цепочки промисов.
#### Разделение значений
Иногда вы можете воспринять это как сигнал о том, что вы можете/должны разложить проблему на два или более промисов.
Представьте, что у вас есть функция `foo(..)`, которая обеспечивает два значения (`x` и `y`) асинхронно:
```js
function getY(x) {
return new Promise( function(resolve,reject){
setTimeout( function(){
resolve( (3 * x) - 1 );
}, 100 );
} );
}
function foo(bar,baz) {
var x = bar * baz;
return getY( x )
.then( function(y){
// упаковать оба значения в контейнер
return [x,y];
} );
}
foo( 10, 20 )
.then( function(msgs){
var x = msgs[0];
var y = msgs[1];
console.log( x, y ); // 200 599
} );
```
Сперва, давайте переставим то. что возвращает `foo(..)` чтобы нам не пришлось упаковывать `x` и `y` в единственное значение `массива`, чтобы для передачи через один промис. Вместо этого мы можем обернуть кажое значение в свой собственный промис:
```js
function foo(bar,baz) {
var x = bar * baz;
// вернуть оба промиса
return [
Promise.resolve( x ),
getY( x )
];
}
Promise.all(
foo( 10, 20 )
)
.then( function(msgs){
var x = msgs[0];
var y = msgs[1];
console.log( x, y );
} );
```
Действительно ли `массив` промисов лучше, чем `массив` значений, переданных через один промис? Синтаксически это не является большим улучшением.
Но этот подход больше отвечает теории дизайна промисов. Теперь легче в будущем рефакторить, чтобы разделить вычисление `x` и `y` на отдельные функции. Это Гораздо чище и гибче позволить вызывающему коду решать, как согласовать эти два промиса используя тут `Promise.all([ .. ])`, но, конечно, не единственный вариант, а не абстрагироваться от таких деталей внутри `foo(..)`.
#### Распаковка/разбиение аргументов
Присвоения `var x = ..` и `var y = ..` - все еще неудобные накладные расходы. Мы можем использовать некоторые функциональные хитрости (благодарим за предоставленную информацию Реджинальда Брейтуэйта (Reginald Braithwaite), @raganwald в твиттере) во вспомогательной функции:
```js
function spread(fn) {
return Function.apply.bind( fn, null );
}
Promise.all(
foo( 10, 20 )
)
.then(
spread( function(x,y){
console.log( x, y ); // 200 599
} )
)
```
Это немного лучше! Конечно, вы можете встроить функциональную магию, чтобы избежать дополнительную функцию:
```js
Promise.all(
foo( 10, 20 )
)
.then( Function.apply.bind(
function(x,y){
console.log( x, y ); // 200 599
},
null
) );
```
Эти трюки могут быть простыми, но у ES6 есть еще более лучший ответ для нас: деструктуризация. Форма присвоения с деструктуризацией массива выглядит как-то так:
```js
Promise.all(
foo( 10, 20 )
)
.then( function(msgs){
var [x,y] = msgs;
console.log( x, y ); // 200 599
} );
```
Но лучше всего, ES6 предлагает формат деструктуризации параметра-массива:
```js
Promise.all(
foo( 10, 20 )
)
.then( function([x,y]){
console.log( x, y ); // 200 599
} );
```
Теперь мы придерживаемся мантры "одно значение на промис", но сократили до минимума наш вспомогательный шаблон!
**Примечание** Для получения дополнительной информации о форматах деструктуризации в ES6 см. книгу *За пределами ES6* этой серии.
### Единственное разрешение
Одним из наиболее характерных свойств промисов является то, что промис может быть разрешен только один раз (завершение или отказ). Для многих случаев использования асинхронного кода, вы получаете значение только один раз, поэтому это работает прекрасно.
Но есть также множество асинхронных случаев, которые вписываются в другую модель - те, которые больше похожи на события и/или потоки данных. На первый взгляд неясно, насколько хорошо промисы могут вписаться в такие сценарии использования, если это вообще возможно. Без значительной абстракции поверх промисов, они будут совершенно не подходить для обработки множественного разрешения значений.
Представьте себе сценарий, в котором вы хотите запустить последовательность асинхронных шагов в ответ на стимул (например, событие), который может произойти несколько раз, например, нажатие кнопки.
Это, вероятно, не будет работать так, как вы хотите:
```js
// `click(..)` привязывает событие `"click"` к элементу DOM
// `request(..)` это ранее определенный Ajax-запрос с поддержкой промисов
var p = new Promise( function(resolve,reject){
click( "#mybtn", resolve );
} );
p.then( function(evt){
var btnID = evt.currentTarget.id;
return request( "http://some.url.1/?id=" + btnID );
} )
.then( function(text){
console.log( text );
} );
```
Приведенное здесь поведение работает только в том случае, если ваше приложение требует, чтобы кнопка была нажата только один раз. Если кнопка нажата второй раз, промис `p` уже был разрешен, поэтому второй вызов `resolve(..)` будет проигнорирован.
Вместо этого вам, вероятно, придется изменить парадигму, создавая совершенно новую цепочку промисов для каждого срабатывания события:
```js
click( "#mybtn", function(evt){
var btnID = evt.currentTarget.id;
request( "http://some.url.1/?id=" + btnID )
.then( function(text){
console.log( text );
} );
} );
```
Этот подход *работает* в том смысле, что для каждого события `"click"` на кнопке будет запускаться совершенно новая последовательность промисов.
Но помимо безобразия, связанного с необходимостью определять всю цепочку промисов внутри обработчика события, эта конструкция в некоторых отношениях нарушает идею разделения обязанностей/возможностей (SoC). Вполне возможно, что вы захотите определить обработчик события в другом месте вашего кода, а не там, где вы определяете *ответ* на событие (цепочка промисов). Это довольно неудобно делать в этом шаблоне без вспомогательных механизмов..
**Примечание** Другой способ сформулировать это ограничение заключается в том, что было бы неплохо, если бы мы могли создать некую "наблюдаемую штуку", на которую мы могли бы подписать цепочку промисов. Существуют библиотеки, которые создали такие абстракции (такие как RxJS -- http://rxjs.codeplex.com/), но абстракции могут показаться настолько тяжелыми, что вы больше не можете видеть природу промисов. Такая тяжелая абстракция заставляет задуматься о таких важных вопросах, как: являются ли (вне промисов) эти механизмы настолько *надежными*, насколько сами промисы были разработаны для этого. Мы вернемся к шаблону "Наблюдаемый" в Приложении B.
### Инерция
Одним из конкретных препятствий для начала использования промисов в вашем собственном коде является весь существующий код, который еще не поддерживает промисы. Если у вас много кода, основанного на колбеках, гораздо проще просто продолжать кодировать в том же стиле.
"Кодовая база в движении (с колбеками) будет оставаться в движении (с колбеками), если не будут приняты меры со стороны умного, знающего промисы разработчика."
Промисы предлагают другую парадигму, и поэтому подход к коду может быть разным - от просто немного другого до, в некоторых случаях, радикально другого. Вы должны быть целенаправленными в этом, потому что промисы не будут просто естественным образом вытекать из тех же старых способов выполнения кода, которые до сих пор хорошо вам служили.
Рассмотрим следующий сценарий, основанный на колбеке:
```js
function foo(x,y,cb) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
cb
);
}
foo( 11, 31, function(err,text) {
if (err) {
console.error( err );
}
else {
console.log( text );
}
} );
```
Очевидно ли сразу, каковы первые шаги по преобразованию этого кода, основанного на колбеках, в код, ориентированный на промисы? Зависит от вашего опыта. Чем больше у вас будет практики, тем естественнее оно будет ощущаться. Но, безусловно, Промисы не только в том, что на этикетке написано, как именно это сделать - универсального ответа не существует, так что ответственность лежит на вас.
Как мы уже писали ранее, нам определенно требуется Ajax-функция, которая поддерживает промисы вместо колбеков, которую мы могли бы назвать `request(..)`. Вы можете сделать свою собственную, как мы уже делали. Но накладные расходы, связанные с необходимостью вручную определять промис-совместимые обертки для каждой функции, основанной на колбеках, снижают вероятность того, что вы вообще решите перейти на промис-совместимое кодирование.
Промисы не дают прямого ответа на это ограничение. Однако, большинство библиотек промисов предлагают вспомогательные средства. Но даже без библиотеки, представьте вспомогательный код подобный этому:
```js
// защитная проверка в стиле безопасного полифила
if (!Promise.wrap) {
Promise.wrap = function(fn) {
return function() {
var args = [].slice.call( arguments );
return new Promise( function(resolve,reject){
fn.apply(
null,
args.concat( function(err,v){
if (err) {
reject( err );
}
else {
resolve( v );
}
} )
);
} );
};
};
}
```
Хорошо, это больше, чем просто маленькая тривиальная утилита. Однако, хотя это может выглядеть немного пугающе, все не так плохо, как вы думаете. Она принимает функцию, которая ожидает колбек в стиле ошибка-первым-аргументом, как свой последний параметр и возвращает новый, который автоматически создает промис в качестве возвращаемого значения и подставляет колбек за вас, присоединяя к завершению/отказу промиса.
Вместо того, чтобы тратить много времени на обсуждение того, *как* эта вспомогательная функция `Promise.wrap(..)` работает, давайте просто взглянем на то, как мы ее используем:
```js
var request = Promise.wrap( ajax );
request( "http://some.url.1/" )
.then( .. )
..
```
Ого, это было довольно просто!
`Promise.wrap(..)` **не** создает промис. Она создает функцию, которая создаст промисы. В каком-то смысле, промисо-генерирующая функция может рассматриваться как "фабрика промисов". Я предлагаю "promisory" в качестве названия для такой вещи ("Promise" + "factory" (промис + фабрика)).
Процесс обертывания функции, ожидающей колбек, в функцию с поддержкой промисов иногда называют "поднятием" (lifting) или "промисификацией" (promisifying). Но, похоже, нет стандартного термина для того, как следует называть результирующую функцию, кроме как "поднятая функция", поэтому мне больше нравится "промисофабрика (promisory)", поскольку я считаю его более описательным.
**Примечание** Promisory - это не выдуманный термин. Это реальное слово, и его определение означает "содержать или передать промис". Именно это и делают эти функции, так что получается довольно идеальное терминологическое соответствие!
Таким образом, `Promise.wrap(ajax)` создает промисофабрику для `ajax(..)`, который мы назвали `request(..)` и эта промисофабрика создает промисы для Ajax-ответов.
Если бы все функции уже были бы промисофабриками, нам бы не пришлось создавать из самим, так что лишний шаг - это немного досадно. Но, по крайней мере, шаблон упаковки (обычно) повторяемый, поэтому мы можем поместить его во вспомогательную функцию `Promise.wrap(...)`, как показано на примере, чтобы облегчить кодирование промисов.
Итак, возвращаясь к нашему предыдущему примеру, нам нужна промисофабрика для обоих `ajax(..)` и `foo(..)`:
```js
// создать промисофабрику для `ajax(..)`
var request = Promise.wrap( ajax );
// отрефакторить `foo(..)`, но оставить его снаружи
// на основе колбека для совместимости с другими
// частями кода пока что, и использовать промис из
// `request(..)` только внутри.
function foo(x,y,cb) {
request(
"http://some.url.1/?x=" + x + "&y=" + y
)
.then(
function fulfilled(text){
cb( null, text );
},
cb
);
}
// теперь, для целей данного кода, сделаем
// промисофабрику для `foo(..)`
var betterFoo = Promise.wrap( foo );
// и используем эту промисофабрику
betterFoo( 11, 31 )
.then(
function fulfilled(text){
console.log( text );
},
function rejected(err){
console.error( err );
}
);
```
Конечно, в то время как мы рефакторим `foo(..)`, чтобы использовать нашу новую промисофабрику `request(..)`, мы могли бы просто превратить саму `foo(..)` в промисофабрику, вместо того, чтобы оставлять основу из колбеков и необходимость создания и использования последующей промисофабрики `betterFoo(..)`. Это решение зависит только от того. надо ли оставлять `foo(..)` колбеко-совместимой с другими частями кода или нет.
Представим:
```js
// `foo(..)` теперь является промисофабрикой, поскольку она
// делегирует работу промисофабрике `request(..)`
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
foo( 11, 31 )
.then( .. )
..
```
В то время как ES6 промисы не оснащены нативно вспомогательными функциями для такого обертывания с помощью промисофабрик, многие библиотеки предоставляют их, либо вы можете сами сделать свои собственные. В любом случае, это конкретное ограничение промисов можно устранить без особых проблем. (конечно, по сравнению с муками ада обратных вызовов!).
### Неотменяемый промис
Как только вы создадите промис и зарегистрируете для него обработчик завершения и/или отказа, не будет ничего снаружи, что вы могли бы сделать, чтобы остановить это движение, если произойдет что-то еще, что сделает эту задачу неактуальной.
**Примечание** Многие библиотеки абстракций над промисами предоставляют возможности для отмены промисов, но это ужасная идея! Многие разработчики хотели бы, чтобы промисы были изначально спроектированы с возможностью внешней отмены, но проблема в том, что это позволило бы одному потребителю/наблюдателю промиса влиять на способность другого потребителя наблюдать тот же промис. Это нарушает достоверность (внешнюю неизменяемость) будущего значения и, более того, является воплощением анти-паттерна "действие на расстоянии" ("action at a distance" (http://en.wikipedia.org/wiki/Action_at_a_distance_%28computer_programming%29)). Каким бы полезным он ни казался, на самом деле он приведет вас к тому же кошмару, что и колбеки.
Рассмотрим наш сценарий с тайм-аутом промиса, описанный ранее:
```js
var p = foo( 42 );
Promise.race( [
p,
timeoutPromise( 3000 )
] )
.then(
doSomething,
handleError
);
p.then( function(){
// все равно происходит даже в случае тайм-аута :(
} );
```
"Тайм-аут" был внешним по отношению к `p`, поэтому само `p` выполняется дальше, чего мы, вероятно, не хотим.
Одним из вариантов может быть инвазивное определение колбеков разрешения:
```js
var OK = true;
var p = foo( 42 );
Promise.race( [
p,
timeoutPromise( 3000 )
.catch( function(err){
OK = false;
throw err;
} )
] )
.then(
doSomething,
handleError
);
p.then( function(){
if (OK) {
// выполняется только если не было тайм-аута! :)
}
} );
```
Это ужасно. Это работает, но далеко от идеала. В общем случае следует стараться избегать таких сценариев.
Но если не можете этого избежать, Уродливость этого решения должна быть подсказкой, что *отмена* - это функциональность, которая находится на более высоком уровне абстракции, поверх промисов. Я бы рекомендовал обращаться за помощью к библиотекам абстракций над промисами, а не разрабатывать их самостоятельно.
**Примечание** Моя библиотека абстракций над промисами *asynquence* предоставляет именно такую абстракцию и возможность сделать `abort()` для последовательности, все они будут рассмотрены в Приложении А.
Одиночный промис на самом деле не является механизмом управления потоком (по крайней мере, в сильно значимом смысле), а это именно то, к чему относится *отмена*; поэтому отмена промиса будет выглядеть неловко.
В отличие от этого, цепочка промисов, взятых в совокупности (то, что я люблю называть "последовательность") - *является* выражением управления потоком, и поэтому уместно, чтобы отмена была определена на этом уровне абстракции.
Ни один отдельный промис не должен быть отменяемым, но разумно, чтобы отменяемой была *последовательность*, поскольку вы не передаете последовательность как единое неизменяемое значение, как вы делаете это с обещанием.
### Производительность промисов
Данное ограничение является одновременно и простым, и сложным.
Сравнивая, сколько частей двигается в базовой цепочке асинхронных задач, основанных на колбеках, с цепочкой промисов, можно заметить, что в промисах происходит гораздо больше операций, а значит, они, естественно, по крайней мере немного медленнее. Вспомните простой список гарантий доверия, которые предоставляют промисы, по сравнению со специальным кодом решения, который пришлось бы накладывать поверх колбеков для достижения того же уровня защиты.
Больше работы, больше защитных мер, что означает, что обещания *медленнее* по сравнению с чистыми, ненадежными колбеками. Это очевидно и, вероятно, просто для восприятия.
Но насколько медленнее? Ну... На этот вопрос, на самом деле, невероятно сложно ответить всесторонне.
Честно говоря, это своего рода сравнение "яблок с апельсинами", так что, вероятно, это неправильный вопрос. На самом деле следует сравнить, насколько специализированная система колбеков со всеми теми же защитами, внедренными вручную, быстрее, чем реализация промисов.
Если у промисов и есть обоснованное ограничение производительности, то оно заключается в том, что они не дают возможности выбирать, какие средства защиты надежности вам нужны или не нужны - вы получаете их все и всегда.
Тем не менее, если мы признаем, что промис в целом *немного медленнее*, чем его эквивалент в виде не-промис и ненадежного колбека, предполагая, что есть места, где вы можете оправдать отсутствие надежности - означает ли это, что промиса следует избегать повсеместно, как будто все ваше приложение управляется только самым быстрым кодом, какой только может быть?
Если ваш код действительно такой, то **подходит ли JavaScript для таких задач?** JavaScript может быть оптимизирован для выполнения приложений с очень высокой производительностью (см. главу 5 и главу 6). Но действительно ли уместно зацикливаться на крошечных компромиссах производительности с промисами в свете всех преимуществ, которые они дают?
Еще одна небольшая проблема заключается в том, что Promises делают *все* асинхронным, а это значит, что некоторые немедленно (синхронно) завершенные шаги все равно откладывают выполнение следующего шага до Задачи (см. главу 1). Это означает, что последовательность задач промиса может выполняться чуть медленнее, чем та же самая последовательность с колбеками.
Конечно, вопрос заключается в следующем: стоят ли эти потенциальные снижения крошечных долей производительности всех остальных преимуществ промисов, которые мы изложили в этой главе?
Я считаю, что практически во всех случаях, когда производительность промисов кажется вам достаточно низкой, чтобы начать беспокоиться, в действительности это антипаттерн - оптимизировать преимущества надежности и комбинируемости промисов, избегая их полностью.
Вместо этого вам следует по умолчанию использовать их во всей кодовой базе, а затем профилировать и анализировать ключевые (критические) места вашего приложения. Являются ли промисы *действительно* узким местом, или они просто теоретически замедляют работу? Только *после этого*, вооружившись реальными контрольными показателями (см. главу 6), можно ответственно и разумно исключить промисы именно в тех критических областях, которые были выявлены.
Промисы немного медленнее, но в обмен на это вы получаете много надежности, не-Залго предсказуемости, и встроенную способность к композиции. Может быть, ограничением является не их производительность, а ваше недостаточное восприятие их преимуществ?
## Обзор
Обещания - это круто. Используйте их. Они решают проблемы *инверсии управления*, которые мучают нас в коде, основанном только на колбеках.
Они не избавляются от колбеков, а просто перепоручают их организацию надежному механизму-посреднику, который находится между нами и другой утилитой.
Цепочки промисов также начинают решать (хотя, конечно, не идеально) проблему лучшего способа последовательного выражения потока асинхронного кода, что помогает нашему мозгу лучше планировать и поддерживать асинхронный JS-код. Мы увидим еще более удачное решение *этой* проблемы в следующей главе!
================================================
FILE: async & performance/ch4.md
================================================
# You Don't Know JS: Async & Performance
# Chapter 4: Generators
In Chapter 2, we identified two key drawbacks to expressing async flow control with callbacks:
* Callback-based async doesn't fit how our brain plans out steps of a task.
* Callbacks aren't trustable or composable because of *inversion of control*.
In Chapter 3, we detailed how Promises uninvert the *inversion of control* of callbacks, restoring trustability/composability.
Now we turn our attention to expressing async flow control in a sequential, synchronous-looking fashion. The "magic" that makes it possible is ES6 **generators**.
## Breaking Run-to-Completion
In Chapter 1, we explained an expectation that JS developers almost universally rely on in their code: once a function starts executing, it runs until it completes, and no other code can interrupt and run in between.
As bizarre as it may seem, ES6 introduces a new type of function that does not behave with the run-to-completion behavior. This new type of function is called a "generator."
To understand the implications, let's consider this example:
```js
var x = 1;
function foo() {
x++;
bar(); // <-- what about this line?
console.log( "x:", x );
}
function bar() {
x++;
}
foo(); // x: 3
```
In this example, we know for sure that `bar()` runs in between `x++` and `console.log(x)`. But what if `bar()` wasn't there? Obviously the result would be `2` instead of `3`.
Now let's twist your brain. What if `bar()` wasn't present, but it could still somehow run between the `x++` and `console.log(x)` statements? How would that be possible?
In **preemptive** multithreaded languages, it would essentially be possible for `bar()` to "interrupt" and run at exactly the right moment between those two statements. But JS is not preemptive, nor is it (currently) multithreaded. And yet, a **cooperative** form of this "interruption" (concurrency) is possible, if `foo()` itself could somehow indicate a "pause" at that part in the code.
**Note:** I use the word "cooperative" not only because of the connection to classical concurrency terminology (see Chapter 1), but because as you'll see in the next snippet, the ES6 syntax for indicating a pause point in code is `yield` -- suggesting a politely *cooperative* yielding of control.
Here's the ES6 code to accomplish such cooperative concurrency:
```js
var x = 1;
function *foo() {
x++;
yield; // pause!
console.log( "x:", x );
}
function bar() {
x++;
}
```
**Note:** You will likely see most other JS documentation/code that will format a generator declaration as `function* foo() { .. }` instead of as I've done here with `function *foo() { .. }` -- the only difference being the stylistic positioning of the `*`. The two forms are functionally/syntactically identical, as is a third `function*foo() { .. }` (no space) form. There are arguments for both styles, but I basically prefer `function *foo..` because it then matches when I reference a generator in writing with `*foo()`. If I said only `foo()`, you wouldn't know as clearly if I was talking about a generator or a regular function. It's purely a stylistic preference.
Now, how can we run the code in that previous snippet such that `bar()` executes at the point of the `yield` inside of `*foo()`?
```js
// construct an iterator `it` to control the generator
var it = foo();
// start `foo()` here!
it.next();
x; // 2
bar();
x; // 3
it.next(); // x: 3
```
OK, there's quite a bit of new and potentially confusing stuff in those two code snippets, so we've got plenty to wade through. But before we explain the different mechanics/syntax with ES6 generators, let's walk through the behavior flow:
1. The `it = foo()` operation does *not* execute the `*foo()` generator yet, but it merely constructs an *iterator* that will control its execution. More on *iterators* in a bit.
2. The first `it.next()` starts the `*foo()` generator, and runs the `x++` on the first line of `*foo()`.
3. `*foo()` pauses at the `yield` statement, at which point that first `it.next()` call finishes. At the moment, `*foo()` is still running and active, but it's in a paused state.
4. We inspect the value of `x`, and it's now `2`.
5. We call `bar()`, which increments `x` again with `x++`.
6. We inspect the value of `x` again, and it's now `3`.
7. The final `it.next()` call resumes the `*foo()` generator from where it was paused, and runs the `console.log(..)` statement, which uses the current value of `x` of `3`.
Clearly, `*foo()` started, but did *not* run-to-completion -- it paused at the `yield`. We resumed `*foo()` later, and let it finish, but that wasn't even required.
So, a generator is a special kind of function that can start and stop one or more times, and doesn't necessarily ever have to finish. While it won't be terribly obvious yet why that's so powerful, as we go throughout the rest of this chapter, that will be one of the fundamental building blocks we use to construct generators-as-async-flow-control as a pattern for our code.
### Input and Output
A generator function is a special function with the new processing model we just alluded to. But it's still a function, which means it still has some basic tenets that haven't changed -- namely, that it still accepts arguments (aka "input"), and that it can still return a value (aka "output"):
```js
function *foo(x,y) {
return x * y;
}
var it = foo( 6, 7 );
var res = it.next();
res.value; // 42
```
We pass in the arguments `6` and `7` to `*foo(..)` as the parameters `x` and `y`, respectively. And `*foo(..)` returns the value `42` back to the calling code.
We now see a difference with how the generator is invoked compared to a normal function. `foo(6,7)` obviously looks familiar. But subtly, the `*foo(..)` generator hasn't actually run yet as it would have with a function.
Instead, we're just creating an *iterator* object, which we assign to the variable `it`, to control the `*foo(..)` generator. Then we call `it.next()`, which instructs the `*foo(..)` generator to advance from its current location, stopping either at the next `yield` or end of the generator.
The result of that `next(..)` call is an object with a `value` property on it holding whatever value (if anything) was returned from `*foo(..)`. In other words, `yield` caused a value to be sent out from the generator during the middle of its execution, kind of like an intermediate `return`.
Again, it won't be obvious yet why we need this whole indirect *iterator* object to control the generator. We'll get there, I *promise*.
#### Iteration Messaging
In addition to generators accepting arguments and having return values, there's even more powerful and compelling input/output messaging capability built into them, via `yield` and `next(..)`.
Consider:
```js
function *foo(x) {
var y = x * (yield);
return y;
}
var it = foo( 6 );
// start `foo(..)`
it.next();
var res = it.next( 7 );
res.value; // 42
```
First, we pass in `6` as the parameter `x`. Then we call `it.next()`, and it starts up `*foo(..)`.
Inside `*foo(..)`, the `var y = x ..` statement starts to be processed, but then it runs across a `yield` expression. At that point, it pauses `*foo(..)` (in the middle of the assignment statement!), and essentially requests the calling code to provide a result value for the `yield` expression. Next, we call `it.next( 7 )`, which is passing the `7` value back in to *be* that result of the paused `yield` expression.
So, at this point, the assignment statement is essentially `var y = 6 * 7`. Now, `return y` returns that `42` value back as the result of the `it.next( 7 )` call.
Notice something very important but also easily confusing, even to seasoned JS developers: depending on your perspective, there's a mismatch between the `yield` and the `next(..)` call. In general, you're going to have one more `next(..)` call than you have `yield` statements -- the preceding snippet has one `yield` and two `next(..)` calls.
Why the mismatch?
Because the first `next(..)` always starts a generator, and runs to the first `yield`. But it's the second `next(..)` call that fulfills the first paused `yield` expression, and the third `next(..)` would fulfill the second `yield`, and so on.
##### Tale of Two Questions
Actually, which code you're thinking about primarily will affect whether there's a perceived mismatch or not.
Consider only the generator code:
```js
var y = x * (yield);
return y;
```
This **first** `yield` is basically *asking a question*: "What value should I insert here?"
Who's going to answer that question? Well, the **first** `next()` has already run to get the generator up to this point, so obviously *it* can't answer the question. So, the **second** `next(..)` call must answer the question *posed* by the **first** `yield`.
See the mismatch -- second-to-first?
But let's flip our perspective. Let's look at it not from the generator's point of view, but from the iterator's point of view.
To properly illustrate this perspective, we also need to explain that messages can go in both directions -- `yield ..` as an expression can send out messages in response to `next(..)` calls, and `next(..)` can send values to a paused `yield` expression. Consider this slightly adjusted code:
```js
function *foo(x) {
var y = x * (yield "Hello"); // <-- yield a value!
return y;
}
var it = foo( 6 );
var res = it.next(); // first `next()`, don't pass anything
res.value; // "Hello"
res = it.next( 7 ); // pass `7` to waiting `yield`
res.value; // 42
```
`yield ..` and `next(..)` pair together as a two-way message passing system **during the execution of the generator**.
So, looking only at the *iterator* code:
```js
var res = it.next(); // first `next()`, don't pass anything
res.value; // "Hello"
res = it.next( 7 ); // pass `7` to waiting `yield`
res.value; // 42
```
**Note:** We don't pass a value to the first `next()` call, and that's on purpose. Only a paused `yield` could accept such a value passed by a `next(..)`, and at the beginning of the generator when we call the first `next()`, there **is no paused `yield`** to accept such a value. The specification and all compliant browsers just silently **discard** anything passed to the first `next()`. It's still a bad idea to pass a value, as you're just creating silently "failing" code that's confusing. So, always start a generator with an argument-free `next()`.
The first `next()` call (with nothing passed to it) is basically *asking a question*: "What *next* value does the `*foo(..)` generator have to give me?" And who answers this question? The first `yield "hello"` expression.
See? No mismatch there.
Depending on *who* you think about asking the question, there is either a mismatch between the `yield` and `next(..)` calls, or not.
But wait! There's still an extra `next()` compared to the number of `yield` statements. So, that final `it.next(7)` call is again asking the question about what *next* value the generator will produce. But there's no more `yield` statements left to answer, is there? So who answers?
The `return` statement answers the question!
And if there **is no `return`** in your generator -- `return` is certainly not any more required in generators than in regular functions -- there's always an assumed/implicit `return;` (aka `return undefined;`), which serves the purpose of default answering the question *posed* by the final `it.next(7)` call.
These questions and answers -- the two-way message passing with `yield` and `next(..)` -- are quite powerful, but it's not obvious at all how these mechanisms are connected to async flow control. We're getting there!
### Multiple Iterators
It may appear from the syntactic usage that when you use an *iterator* to control a generator, you're controlling the declared generator function itself. But there's a subtlety that's easy to miss: each time you construct an *iterator*, you are implicitly constructing an instance of the generator which that *iterator* will control.
You can have multiple instances of the same generator running at the same time, and they can even interact:
```js
function *foo() {
var x = yield 2;
z++;
var y = yield (x * z);
console.log( x, y, z );
}
var z = 1;
var it1 = foo();
var it2 = foo();
var val1 = it1.next().value; // 2 <-- yield 2
var val2 = it2.next().value; // 2 <-- yield 2
val1 = it1.next( val2 * 10 ).value; // 40 <-- x:20, z:2
val2 = it2.next( val1 * 5 ).value; // 600 <-- x:200, z:3
it1.next( val2 / 2 ); // y:300
// 20 300 3
it2.next( val1 / 4 ); // y:10
// 200 10 3
```
**Warning:** The most common usage of multiple instances of the same generator running concurrently is not such interactions, but when the generator is producing its own values without input, perhaps from some independently connected resource. We'll talk more about value production in the next section.
Let's briefly walk through the processing:
1. Both instances of `*foo()` are started at the same time, and both `next()` calls reveal a `value` of `2` from the `yield 2` statements, respectively.
2. `val2 * 10` is `2 * 10`, which is sent into the first generator instance `it1`, so that `x` gets value `20`. `z` is incremented from `1` to `2`, and then `20 * 2` is `yield`ed out, setting `val1` to `40`.
3. `val1 * 5` is `40 * 5`, which is sent into the second generator instance `it2`, so that `x` gets value `200`. `z` is incremented again, from `2` to `3`, and then `200 * 3` is `yield`ed out, setting `val2` to `600`.
4. `val2 / 2` is `600 / 2`, which is sent into the first generator instance `it1`, so that `y` gets value `300`, then printing out `20 300 3` for its `x y z` values, respectively.
5. `val1 / 4` is `40 / 4`, which is sent into the second generator instance `it2`, so that `y` gets value `10`, then printing out `200 10 3` for its `x y z` values, respectively.
That's a "fun" example to run through in your mind. Did you keep it straight?
#### Interleaving
Recall this scenario from the "Run-to-completion" section of Chapter 1:
```js
var a = 1;
var b = 2;
function foo() {
a++;
b = b * a;
a = b + 3;
}
function bar() {
b--;
a = 8 + b;
b = a * 2;
}
```
With normal JS functions, of course either `foo()` can run completely first, or `bar()` can run completely first, but `foo()` cannot interleave its individual statements with `bar()`. So, there are only two possible outcomes to the preceding program.
However, with generators, clearly interleaving (even in the middle of statements!) is possible:
```js
var a = 1;
var b = 2;
function *foo() {
a++;
yield;
b = b * a;
a = (yield b) + 3;
}
function *bar() {
b--;
yield;
a = (yield 8) + b;
b = a * (yield 2);
}
```
Depending on what respective order the *iterators* controlling `*foo()` and `*bar()` are called, the preceding program could produce several different results. In other words, we can actually illustrate (in a sort of fake-ish way) the theoretical "threaded race conditions" circumstances discussed in Chapter 1, by interleaving the two generator interations over the same shared variables.
First, let's make a helper called `step(..)` that controls an *iterator*:
```js
function step(gen) {
var it = gen();
var last;
return function() {
// whatever is `yield`ed out, just
// send it right back in the next time!
last = it.next( last ).value;
};
}
```
`step(..)` initializes a generator to create its `it` *iterator*, then returns a function which, when called, advances the *iterator* by one step. Additionally, the previously `yield`ed out value is sent right back in at the *next* step. So, `yield 8` will just become `8` and `yield b` will just be `b` (whatever it was at the time of `yield`).
Now, just for fun, let's experiment to see the effects of interleaving these different chunks of `*foo()` and `*bar()`. We'll start with the boring base case, making sure `*foo()` totally finishes before `*bar()` (just like we did in Chapter 1):
```js
// make sure to reset `a` and `b`
a = 1;
b = 2;
var s1 = step( foo );
var s2 = step( bar );
// run `*foo()` completely first
s1();
s1();
s1();
// now run `*bar()`
s2();
s2();
s2();
s2();
console.log( a, b ); // 11 22
```
The end result is `11` and `22`, just as it was in the Chapter 1 version. Now let's mix up the interleaving ordering and see how it changes the final values of `a` and `b`:
```js
// make sure to reset `a` and `b`
a = 1;
b = 2;
var s1 = step( foo );
var s2 = step( bar );
s2(); // b--;
s2(); // yield 8
s1(); // a++;
s2(); // a = 8 + b;
// yield 2
s1(); // b = b * a;
// yield b
s1(); // a = b + 3;
s2(); // b = a * 2;
```
Before I tell you the results, can you figure out what `a` and `b` are after the preceding program? No cheating!
```js
console.log( a, b ); // 12 18
```
**Note:** As an exercise for the reader, try to see how many other combinations of results you can get back rearranging the order of the `s1()` and `s2()` calls. Don't forget you'll always need three `s1()` calls and four `s2()` calls. Recall the discussion earlier about matching `next()` with `yield` for the reasons why.
You almost certainly won't want to intentionally create *this* level of interleaving confusion, as it creates incredibly difficult to understand code. But the exercise is interesting and instructive to understand more about how multiple generators can run concurrently in the same shared scope, because there will be places where this capability is quite useful.
We'll discuss generator concurrency in more detail at the end of this chapter.
## Generator'ing Values
In the previous section, we mentioned an interesting use for generators, as a way to produce values. This is **not** the main focus in this chapter, but we'd be remiss if we didn't cover the basics, especially because this use case is essentially the origin of the name: generators.
We're going to take a slight diversion into the topic of *iterators* for a bit, but we'll circle back to how they relate to generators and using a generator to *generate* values.
### Producers and Iterators
Imagine you're producing a series of values where each value has a definable relationship to the previous value. To do this, you're going to need a stateful producer that remembers the last value it gave out.
You can implement something like that straightforwardly using a function closure (see the *Scope & Closures* title of this series):
```js
var gimmeSomething = (function(){
var nextVal;
return function(){
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
return nextVal;
};
})();
gimmeSomething(); // 1
gimmeSomething(); // 9
gimmeSomething(); // 33
gimmeSomething(); // 105
```
**Note:** The `nextVal` computation logic here could have been simplified, but conceptually, we don't want to calculate the *next value* (aka `nextVal`) until the *next* `gimmeSomething()` call happens, because in general that could be a resource-leaky design for producers of more persistent or resource-limited values than simple `number`s.
Generating an arbitrary number series isn't a terribly realistic example. But what if you were generating records from a data source? You could imagine much the same code.
In fact, this task is a very common design pattern, usually solved by iterators. An *iterator* is a well-defined interface for stepping through a series of values from a producer. The JS interface for iterators, as it is in most languages, is to call `next()` each time you want the next value from the producer.
We could implement the standard *iterator* interface for our number series producer:
```js
var something = (function(){
var nextVal;
return {
// needed for `for..of` loops
[Symbol.iterator]: function(){ return this; },
// standard iterator interface method
next: function(){
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
return { done:false, value:nextVal };
}
};
})();
something.next().value; // 1
something.next().value; // 9
something.next().value; // 33
something.next().value; // 105
```
**Note:** We'll explain why we need the `[Symbol.iterator]: ..` part of this code snippet in the "Iterables" section. Syntactically though, two ES6 features are at play. First, the `[ .. ]` syntax is called a *computed property name* (see the *this & Object Prototypes* title of this series). It's a way in an object literal definition to specify an expression and use the result of that expression as the name for the property. Next, `Symbol.iterator` is one of ES6's predefined special `Symbol` values (see the *ES6 & Beyond* title of this book series).
The `next()` call returns an object with two properties: `done` is a `boolean` value signaling the *iterator's* complete status; `value` holds the iteration value.
ES6 also adds the `for..of` loop, which means that a standard *iterator* can automatically be consumed with native loop syntax:
```js
for (var v of something) {
console.log( v );
// don't let the loop run forever!
if (v > 500) {
break;
}
}
// 1 9 33 105 321 969
```
**Note:** Because our `something` *iterator* always returns `done:false`, this `for..of` loop would run forever, which is why we put the `break` conditional in. It's totally OK for iterators to be never-ending, but there are also cases where the *iterator* will run over a finite set of values and eventually return a `done:true`.
The `for..of` loop automatically calls `next()` for each iteration -- it doesn't pass any values in to the `next()` -- and it will automatically terminate on receiving a `done:true`. It's quite handy for looping over a set of data.
Of course, you could manually loop over iterators, calling `next()` and checking for the `done:true` condition to know when to stop:
```js
for (
var ret;
(ret = something.next()) && !ret.done;
) {
console.log( ret.value );
// don't let the loop run forever!
if (ret.value > 500) {
break;
}
}
// 1 9 33 105 321 969
```
**Note:** This manual `for` approach is certainly uglier than the ES6 `for..of` loop syntax, but its advantage is that it affords you the opportunity to pass in values to the `next(..)` calls if necessary.
In addition to making your own *iterators*, many built-in data structures in JS (as of ES6), like `array`s, also have default *iterators*:
```js
var a = [1,3,5,7,9];
for (var v of a) {
console.log( v );
}
// 1 3 5 7 9
```
The `for..of` loop asks `a` for its *iterator*, and automatically uses it to iterate over `a`'s values.
**Note:** It may seem a strange omission by ES6, but regular `object`s intentionally do not come with a default *iterator* the way `array`s do. The reasons go deeper than we will cover here. If all you want is to iterate over the properties of an object (with no particular guarantee of ordering), `Object.keys(..)` returns an `array`, which can then be used like `for (var k of Object.keys(obj)) { ..`. Such a `for..of` loop over an object's keys would be similar to a `for..in` loop, except that `Object.keys(..)` does not include properties from the `[[Prototype]]` chain while `for..in` does (see the *this & Object Prototypes* title of this series).
### Iterables
The `something` object in our running example is called an *iterator*, as it has the `next()` method on its interface. But a closely related term is *iterable*, which is an `object` that **contains** an *iterator* that can iterate over its values.
As of ES6, the way to retrieve an *iterator* from an *iterable* is that the *iterable* must have a function on it, with the name being the special ES6 symbol value `Symbol.iterator`. When this function is called, it returns an *iterator*. Though not required, generally each call should return a fresh new *iterator*.
`a` in the previous snippet is an *iterable*. The `for..of` loop automatically calls its `Symbol.iterator` function to construct an *iterator*. But we could of course call the function manually, and use the *iterator* it returns:
```js
var a = [1,3,5,7,9];
var it = a[Symbol.iterator]();
it.next().value; // 1
it.next().value; // 3
it.next().value; // 5
..
```
In the previous code listing that defined `something`, you may have noticed this line:
```js
[Symbol.iterator]: function(){ return this; }
```
That little bit of confusing code is making the `something` value -- the interface of the `something` *iterator* -- also an *iterable*; it's now both an *iterable* and an *iterator*. Then, we pass `something` to the `for..of` loop:
```js
for (var v of something) {
..
}
```
The `for..of` loop expects `something` to be an *iterable*, so it looks for and calls its `Symbol.iterator` function. We defined that function to simply `return this`, so it just gives itself back, and the `for..of` loop is none the wiser.
### Generator Iterator
Let's turn our attention back to generators, in the context of *iterators*. A generator can be treated as a producer of values that we extract one at a time through an *iterator* interface's `next()` calls.
So, a generator itself is not technically an *iterable*, though it's very similar -- when you execute the generator, you get an *iterator* back:
```js
function *foo(){ .. }
var it = foo();
```
We can implement the `something` infinite number series producer from earlier with a generator, like this:
```js
function *something() {
var nextVal;
while (true) {
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
yield nextVal;
}
}
```
**Note:** A `while..true` loop would normally be a very bad thing to include in a real JS program, at least if it doesn't have a `break` or `return` in it, as it would likely run forever, synchronously, and block/lock-up the browser UI. However, in a generator, such a loop is generally totally OK if it has a `yield` in it, as the generator will pause at each iteration, `yield`ing back to the main program and/or to the event loop queue. To put it glibly, "generators put the `while..true` back in JS programming!"
That's a fair bit cleaner and simpler, right? Because the generator pauses at each `yield`, the state (scope) of the function `*something()` is kept around, meaning there's no need for the closure boilerplate to preserve variable state across calls.
Not only is it simpler code -- we don't have to make our own *iterator* interface -- it actually is more reason-able code, because it more clearly expresses the intent. For example, the `while..true` loop tells us the generator is intended to run forever -- to keep *generating* values as long as we keep asking for them.
And now we can use our shiny new `*something()` generator with a `for..of` loop, and you'll see it works basically identically:
```js
for (var v of something()) {
console.log( v );
// don't let the loop run forever!
if (v > 500) {
break;
}
}
// 1 9 33 105 321 969
```
But don't skip over `for (var v of something()) ..`! We didn't just reference `something` as a value like in earlier examples, but instead called the `*something()` generator to get its *iterator* for the `for..of` loop to use.
If you're paying close attention, two questions may arise from this interaction between the generator and the loop:
* Why couldn't we say `for (var v of something) ..`? Because `something` here is a generator, which is not an *iterable*. We have to call `something()` to construct a producer for the `for..of` loop to iterate over.
* The `something()` call produces an *iterator*, but the `for..of` loop wants an *iterable*, right? Yep. The generator's *iterator* also has a `Symbol.iterator` function on it, which basically does a `return this`, just like the `something` *iterable* we defined earlier. In other words, a generator's *iterator* is also an *iterable*!
#### Stopping the Generator
In the previous example, it would appear the *iterator* instance for the `*something()` generator was basically left in a suspended state forever after the `break` in the loop was called.
But there's a hidden behavior that takes care of that for you. "Abnormal completion" (i.e., "early termination") of the `for..of` loop -- generally caused by a `break`, `return`, or an uncaught exception -- sends a signal to the generator's *iterator* for it to terminate.
**Note:** Technically, the `for..of` loop also sends this signal to the *iterator* at the normal completion of the loop. For a generator, that's essentially a moot operation, as the generator's *iterator* had to complete first so the `for..of` loop completed. However, custom *iterators* might desire to receive this additional signal from `for..of` loop consumers.
While a `for..of` loop will automatically send this signal, you may wish to send the signal manually to an *iterator*; you do this by calling `return(..)`.
If you specify a `try..finally` clause inside the generator, it will always be run even when the generator is externally completed. This is useful if you need to clean up resources (database connections, etc.):
```js
function *something() {
try {
var nextVal;
while (true) {
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
yield nextVal;
}
}
// cleanup clause
finally {
console.log( "cleaning up!" );
}
}
```
The earlier example with `break` in the `for..of` loop will trigger the `finally` clause. But you could instead manually terminate the generator's *iterator* instance from the outside with `return(..)`:
```js
var it = something();
for (var v of it) {
console.log( v );
// don't let the loop run forever!
if (v > 500) {
console.log(
// complete the generator's iterator
it.return( "Hello World" ).value
);
// no `break` needed here
}
}
// 1 9 33 105 321 969
// cleaning up!
// Hello World
```
When we call `it.return(..)`, it immediately terminates the generator, which of course runs the `finally` clause. Also, it sets the returned `value` to whatever you passed in to `return(..)`, which is how `"Hello World"` comes right back out. We also don't need to include a `break` now because the generator's *iterator* is set to `done:true`, so the `for..of` loop will terminate on its next iteration.
Generators owe their namesake mostly to this *consuming produced values* use. But again, that's just one of the uses for generators, and frankly not even the main one we're concerned with in the context of this book.
But now that we more fully understand some of the mechanics of how they work, we can *next* turn our attention to how generators apply to async concurrency.
## Iterating Generators Asynchronously
What do generators have to do with async coding patterns, fixing problems with callbacks, and the like? Let's get to answering that important question.
We should revisit one of our scenarios from Chapter 3. Let's recall the callback approach:
```js
function foo(x,y,cb) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
cb
);
}
foo( 11, 31, function(err,text) {
if (err) {
console.error( err );
}
else {
console.log( text );
}
} );
```
If we wanted to express this same task flow control with a generator, we could do:
```js
function foo(x,y) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
function(err,data){
if (err) {
// throw an error into `*main()`
it.throw( err );
}
else {
// resume `*main()` with received `data`
it.next( data );
}
}
);
}
function *main() {
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
var it = main();
// start it all up!
it.next();
```
At first glance, this snippet is longer, and perhaps a little more complex looking, than the callback snippet before it. But don't let that impression get you off track. The generator snippet is actually **much** better! But there's a lot going on for us to explain.
First, let's look at this part of the code, which is the most important:
```js
var text = yield foo( 11, 31 );
console.log( text );
```
Think about how that code works for a moment. We're calling a normal function `foo(..)` and we're apparently able to get back the `text` from the Ajax call, even though it's asynchronous.
How is that possible? If you recall the beginning of Chapter 1, we had almost identical code:
```js
var data = ajax( "..url 1.." );
console.log( data );
```
And that code didn't work! Can you spot the difference? It's the `yield` used in a generator.
That's the magic! That's what allows us to have what appears to be blocking, synchronous code, but it doesn't actually block the whole program; it only pauses/blocks the code in the generator itself.
In `yield foo(11,31)`, first the `foo(11,31)` call is made, which returns nothing (aka `undefined`), so we're making a call to request data, but we're actually then doing `yield undefined`. That's OK, because the code is not currently relying on a `yield`ed value to do anything interesting. We'll revisit this point later in the chapter.
We're not using `yield` in a message passing sense here, only in a flow control sense to pause/block. Actually, it will have message passing, but only in one direction, after the generator is resumed.
So, the generator pauses at the `yield`, essentially asking the question, "what value should I return to assign to the variable `text`?" Who's going to answer that question?
Look at `foo(..)`. If the Ajax request is successful, we call:
```js
it.next( data );
```
That's resuming the generator with the response data, which means that our paused `yield` expression receives that value directly, and then as it restarts the generator code, that value gets assigned to the local variable `text`.
Pretty cool, huh?
Take a step back and consider the implications. We have totally synchronous-looking code inside the generator (other than the `yield` keyword itself), but hidden behind the scenes, inside of `foo(..)`, the operations can complete asynchronously.
**That's huge!** That's a nearly perfect solution to our previously stated problem with callbacks not being able to express asynchrony in a sequential, synchronous fashion that our brains can relate to.
In essence, we are abstracting the asynchrony away as an implementation detail, so that we can reason synchronously/sequentially about our flow control: "Make an Ajax request, and when it finishes print out the response." And of course, we just expressed two steps in the flow control, but this same capability extends without bounds, to let us express however many steps we need to.
**Tip:** This is such an important realization, just go back and read the last three paragraphs again to let it sink in!
### Synchronous Error Handling
But the preceding generator code has even more goodness to *yield* to us. Let's turn our attention to the `try..catch` inside the generator:
```js
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
```
How does this work? The `foo(..)` call is asynchronously completing, and doesn't `try..catch` fail to catch asynchronous errors, as we looked at in Chapter 3?
We already saw how the `yield` lets the assignment statement pause to wait for `foo(..)` to finish, so that the completed response can be assigned to `text`. The awesome part is that this `yield` pausing *also* allows the generator to `catch` an error. We throw that error into the generator with this part of the earlier code listing:
```js
if (err) {
// throw an error into `*main()`
it.throw( err );
}
```
The `yield`-pause nature of generators means that not only do we get synchronous-looking `return` values from async function calls, but we can also synchronously `catch` errors from those async function calls!
So we've seen we can throw errors *into* a generator, but what about throwing errors *out of* a generator? Exactly as you'd expect:
```js
function *main() {
var x = yield "Hello World";
yield x.toLowerCase(); // cause an exception!
}
var it = main();
it.next().value; // Hello World
try {
it.next( 42 );
}
catch (err) {
console.error( err ); // TypeError
}
```
Of course, we could have manually thrown an error with `throw ..` instead of causing an exception.
We can even `catch` the same error that we `throw(..)` into the generator, essentially giving the generator a chance to handle it but if it doesn't, the *iterator* code must handle it:
```js
function *main() {
var x = yield "Hello World";
// never gets here
console.log( x );
}
var it = main();
it.next();
try {
// will `*main()` handle this error? we'll see!
it.throw( "Oops" );
}
catch (err) {
// nope, didn't handle it!
console.error( err ); // Oops
}
```
Synchronous-looking error handling (via `try..catch`) with async code is a huge win for readability and reason-ability.
## Generators + Promises
In our previous discussion, we showed how generators can be iterated asynchronously, which is a huge step forward in sequential reason-ability over the spaghetti mess of callbacks. But we lost something very important: the trustability and composability of Promises (see Chapter 3)!
Don't worry -- we can get that back. The best of all worlds in ES6 is to combine generators (synchronous-looking async code) with Promises (trustable and composable).
But how?
Recall from Chapter 3 the Promise-based approach to our running Ajax example:
```js
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
foo( 11, 31 )
.then(
function(text){
console.log( text );
},
function(err){
console.error( err );
}
);
```
In our earlier generator code for the running Ajax example, `foo(..)` returned nothing (`undefined`), and our *iterator* control code didn't care about that `yield`ed value.
But here the Promise-aware `foo(..)` returns a promise after making the Ajax call. That suggests that we could construct a promise with `foo(..)` and then `yield` it from the generator, and then the *iterator* control code would receive that promise.
But what should the *iterator* do with the promise?
It should listen for the promise to resolve (fulfillment or rejection), and then either resume the generator with the fulfillment message or throw an error into the generator with the rejection reason.
Let me repeat that, because it's so important. The natural way to get the most out of Promises and generators is **to `yield` a Promise**, and wire that Promise to control the generator's *iterator*.
Let's give it a try! First, we'll put the Promise-aware `foo(..)` together with the generator `*main()`:
```js
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
function *main() {
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
```
The most powerful revelation in this refactor is that the code inside `*main()` **did not have to change at all!** Inside the generator, whatever values are `yield`ed out is just an opaque implementation detail, so we're not even aware it's happening, nor do we need to worry about it.
But how are we going to run `*main()` now? We still have some of the implementation plumbing work to do, to receive and wire up the `yield`ed promise so that it resumes the generator upon resolution. We'll start by trying that manually:
```js
var it = main();
var p = it.next().value;
// wait for the `p` promise to resolve
p.then(
function(text){
it.next( text );
},
function(err){
it.throw( err );
}
);
```
Actually, that wasn't so painful at all, was it?
This snippet should look very similar to what we did earlier with the manually wired generator controlled by the error-first callback. Instead of an `if (err) { it.throw..`, the promise already splits fulfillment (success) and rejection (failure) for us, but otherwise the *iterator* control is identical.
Now, we've glossed over some important details.
Most importantly, we took advantage of the fact that we knew that `*main()` only had one Promise-aware step in it. What if we wanted to be able to Promise-drive a generator no matter how many steps it has? We certainly don't want to manually write out the Promise chain differently for each generator! What would be much nicer is if there was a way to repeat (aka "loop" over) the iteration control, and each time a Promise comes out, wait on its resolution before continuing.
Also, what if the generator throws out an error (intentionally or accidentally) during the `it.next(..)` call? Should we quit, or should we `catch` it and send it right back in? Similarly, what if we `it.throw(..)` a Promise rejection into the generator, but it's not handled, and comes right back out?
### Promise-Aware Generator Runner
The more you start to explore this path, the more you realize, "wow, it'd be great if there was just some utility to do it for me." And you're absolutely correct. This is such an important pattern, and you don't want to get it wrong (or exhaust yourself repeating it over and over), so your best bet is to use a utility that is specifically designed to *run* Promise-`yield`ing generators in the manner we've illustrated.
Several Promise abstraction libraries provide just such a utility, including my *asynquence* library and its `runner(..)`, which will be discussed in Appendix A of this book.
But for the sake of learning and illustration, let's just define our own standalone utility that we'll call `run(..)`:
```js
// thanks to Benjamin Gruenbaum (@benjamingr on GitHub) for
// big improvements here!
function run(gen) {
var args = [].slice.call( arguments, 1), it;
// initialize the generator in the current context
it = gen.apply( this, args );
// return a promise for the generator completing
return Promise.resolve()
.then( function handleNext(value){
// run to the next yielded value
var next = it.next( value );
return (function handleResult(next){
// generator has completed running?
if (next.done) {
return next.value;
}
// otherwise keep going
else {
return Promise.resolve( next.value )
.then(
// resume the async loop on
// success, sending the resolved
// value back into the generator
handleNext,
// if `value` is a rejected
// promise, propagate error back
// into the generator for its own
// error handling
function handleErr(err) {
return Promise.resolve(
it.throw( err )
)
.then( handleResult );
}
);
}
})(next);
} );
}
```
As you can see, it's a quite a bit more complex than you'd probably want to author yourself, and you especially wouldn't want to repeat this code for each generator you use. So, a utility/library helper is definitely the way to go. Nevertheless, I encourage you to spend a few minutes studying that code listing to get a better sense of how to manage the generator+Promise negotiation.
How would you use `run(..)` with `*main()` in our *running* Ajax example?
```js
function *main() {
// ..
}
run( main );
```
That's it! The way we wired `run(..)`, it will automatically advance the generator you pass to it, asynchronously until completion.
**Note:** The `run(..)` we defined returns a promise which is wired to resolve once the generator is complete, or receive an uncaught exception if the generator doesn't handle it. We don't show that capability here, but we'll come back to it later in the chapter.
#### ES7: `async` and `await`?
The preceding pattern -- generators yielding Promises that then control the generator's *iterator* to advance it to completion -- is such a powerful and useful approach, it would be nicer if we could do it without the clutter of the library utility helper (aka `run(..)`).
There's probably good news on that front. At the time of this writing, there's early but strong support for a proposal for more syntactic addition in this realm for the post-ES6, ES7-ish timeframe. Obviously, it's too early to guarantee the details, but there's a pretty decent chance it will shake out similar to the following:
```js
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
async function main() {
try {
var text = await foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
main();
```
As you can see, there's no `run(..)` call (meaning no need for a library utility!) to invoke and drive `main()` -- it's just called as a normal function. Also, `main()` isn't declared as a generator function anymore; it's a new kind of function: `async function`. And finally, instead of `yield`ing a Promise, we `await` for it to resolve.
The `async function` automatically knows what to do if you `await` a Promise -- it will pause the function (just like with generators) until the Promise resolves. We didn't illustrate it in this snippet, but calling an async function like `main()` automatically returns a promise that's resolved whenever the function finishes completely.
**Tip:** The `async` / `await` syntax should look very familiar to readers with experience in C#, because it's basically identical.
The proposal essentially codifies support for the pattern we've already derived, into a syntactic mechanism: combining Promises with sync-looking flow control code. That's the best of both worlds combined, to effectively address practically all of the major concerns we outlined with callbacks.
The mere fact that such a ES7-ish proposal already exists and has early support and enthusiasm is a major vote of confidence in the future importance of this async pattern.
### Promise Concurrency in Generators
So far, all we've demonstrated is a single-step async flow with Promises+generators. But real-world code will often have many async steps.
If you're not careful, the sync-looking style of generators may lull you into complacency with how you structure your async concurrency, leading to suboptimal performance patterns. So we want to spend a little time exploring the options.
Imagine a scenario where you need to fetch data from two different sources, then combine those responses to make a third request, and finally print out the last response. We explored a similar scenario with Promises in Chapter 3, but let's reconsider it in the context of generators.
Your first instinct might be something like:
```js
function *foo() {
var r1 = yield request( "http://some.url.1" );
var r2 = yield request( "http://some.url.2" );
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log( r3 );
}
// use previously defined `run(..)` utility
run( foo );
```
This code will work, but in the specifics of our scenario, it's not optimal. Can you spot why?
Because the `r1` and `r2` requests can -- and for performance reasons, *should* -- run concurrently, but in this code they will run sequentially; the `"http://some.url.2"` URL isn't Ajax fetched until after the `"http://some.url.1"` request is finished. These two requests are independent, so the better performance approach would likely be to have them run at the same time.
But how exactly would you do that with a generator and `yield`? We know that `yield` is only a single pause point in the code, so you can't really do two pauses at the same time.
The most natural and effective answer is to base the async flow on Promises, specifically on their capability to manage state in a time-independent fashion (see "Future Value" in Chapter 3).
The simplest approach:
```js
function *foo() {
// make both requests "in parallel"
var p1 = request( "http://some.url.1" );
var p2 = request( "http://some.url.2" );
// wait until both promises resolve
var r1 = yield p1;
var r2 = yield p2;
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log( r3 );
}
// use previously defined `run(..)` utility
run( foo );
```
Why is this different from the previous snippet? Look at where the `yield` is and is not. `p1` and `p2` are promises for Ajax requests made concurrently (aka "in parallel"). It doesn't matter which one finishes first, because promises will hold onto their resolved state for as long as necessary.
Then we use two subsequent `yield` statements to wait for and retrieve the resolutions from the promises (into `r1` and `r2`, respectively). If `p1` resolves first, the `yield p1` resumes first then waits on the `yield p2` to resume. If `p2` resolves first, it will just patiently hold onto that resolution value until asked, but the `yield p1` will hold on first, until `p1` resolves.
Either way, both `p1` and `p2` will run concurrently, and both have to finish, in either order, before the `r3 = yield request..` Ajax request will be made.
If that flow control processing model sounds familiar, it's basically the same as what we identified in Chapter 3 as the "gate" pattern, enabled by the `Promise.all([ .. ])` utility. So, we could also express the flow control like this:
```js
function *foo() {
// make both requests "in parallel," and
// wait until both promises resolve
var results = yield Promise.all( [
request( "http://some.url.1" ),
request( "http://some.url.2" )
] );
var r1 = results[0];
var r2 = results[1];
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log( r3 );
}
// use previously defined `run(..)` utility
run( foo );
```
**Note:** As we discussed in Chapter 3, we can even use ES6 destructuring assignment to simplify the `var r1 = .. var r2 = ..` assignments, with `var [r1,r2] = results`.
In other words, all of the concurrency capabilities of Promises are available to us in the generator+Promise approach. So in any place where you need more than sequential this-then-that async flow control steps, Promises are likely your best bet.
#### Promises, Hidden
As a word of stylistic caution, be careful about how much Promise logic you include **inside your generators**. The whole point of using generators for asynchrony in the way we've described is to create simple, sequential, sync-looking code, and to hide as much of the details of asynchrony away from that code as possible.
For example, this might be a cleaner approach:
```js
// note: normal function, not generator
function bar(url1,url2) {
return Promise.all( [
request( url1 ),
request( url2 )
] );
}
function *foo() {
// hide the Promise-based concurrency details
// inside `bar(..)`
var results = yield bar(
"http://some.url.1",
"http://some.url.2"
);
var r1 = results[0];
var r2 = results[1];
var r3 = yield request(
"http://some.url.3/?v=" + r1 + "," + r2
);
console.log( r3 );
}
// use previously defined `run(..)` utility
run( foo );
```
Inside `*foo()`, it's cleaner and clearer that all we're doing is just asking `bar(..)` to get us some `results`, and we'll `yield`-wait on that to happen. We don't have to care that under the covers a `Promise.all([ .. ])` Promise composition will be used to make that happen.
**We treat asynchrony, and indeed Promises, as an implementation detail.**
Hiding your Promise logic inside a function that you merely call from your generator is especially useful if you're going to do a sophisticated series flow-control. For example:
```js
function bar() {
Promise.all( [
baz( .. )
.then( .. ),
Promise.race( [ .. ] )
] )
.then( .. )
}
```
That kind of logic is sometimes required, and if you dump it directly inside your generator(s), you've defeated most of the reason why you would want to use generators in the first place. We *should* intentionally abstract such details away from our generator code so that they don't clutter up the higher level task expression.
Beyond creating code that is both functional and performant, you should also strive to make code that is as reason-able and maintainable as possible.
**Note:** Abstraction is not *always* a healthy thing for programming -- many times it can increase complexity in exchange for terseness. But in this case, I believe it's much healthier for your generator+Promise async code than the alternatives. As with all such advice, though, pay attention to your specific situations and make proper decisions for you and your team.
## Generator Delegation
In the previous section, we showed calling regular functions from inside a generator, and how that remains a useful technique for abstracting away implementation details (like async Promise flow). But the main drawback of using a normal function for this task is that it has to behave by the normal function rules, which means it cannot pause itself with `yield` like a generator can.
It may then occur to you that you might try to call one generator from another generator, using our `run(..)` helper, such as:
```js
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
// "delegating" to `*foo()` via `run(..)`
var r3 = yield run( foo );
console.log( r3 );
}
run( bar );
```
We run `*foo()` inside of `*bar()` by using our `run(..)` utility again. We take advantage here of the fact that the `run(..)` we defined earlier returns a promise which is resolved when its generator is run to completion (or errors out), so if we `yield` out to a `run(..)` instance the promise from another `run(..)` call, it automatically pauses `*bar()` until `*foo()` finishes.
But there's an even better way to integrate calling `*foo()` into `*bar()`, and it's called `yield`-delegation. The special syntax for `yield`-delegation is: `yield * __` (notice the extra `*`). Before we see it work in our previous example, let's look at a simpler scenario:
```js
function *foo() {
console.log( "`*foo()` starting" );
yield 3;
yield 4;
console.log( "`*foo()` finished" );
}
function *bar() {
yield 1;
yield 2;
yield *foo(); // `yield`-delegation!
yield 5;
}
var it = bar();
it.next().value; // 1
it.next().value; // 2
it.next().value; // `*foo()` starting
// 3
it.next().value; // 4
it.next().value; // `*foo()` finished
// 5
```
**Note:** Similar to a note earlier in the chapter where I explained why I prefer `function *foo() ..` instead of `function* foo() ..`, I also prefer -- differing from most other documentation on the topic -- to say `yield *foo()` instead of `yield* foo()`. The placement of the `*` is purely stylistic and up to your best judgment. But I find the consistency of styling attractive.
How does the `yield *foo()` delegation work?
First, calling `foo()` creates an *iterator* exactly as we've already seen. Then, `yield *` delegates/transfers the *iterator* instance control (of the present `*bar()` generator) over to this other `*foo()` *iterator*.
So, the first two `it.next()` calls are controlling `*bar()`, but when we make the third `it.next()` call, now `*foo()` starts up, and now we're controlling `*foo()` instead of `*bar()`. That's why it's called delegation -- `*bar()` delegated its iteration control to `*foo()`.
As soon as the `it` *iterator* control exhausts the entire `*foo()` *iterator*, it automatically returns to controlling `*bar()`.
So now back to the previous example with the three sequential Ajax requests:
```js
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
// "delegating" to `*foo()` via `yield*`
var r3 = yield *foo();
console.log( r3 );
}
run( bar );
```
The only difference between this snippet and the version used earlier is the use of `yield *foo()` instead of the previous `yield run(foo)`.
**Note:** `yield *` yields iteration control, not generator control; when you invoke the `*foo()` generator, you're now `yield`-delegating to its *iterator*. But you can actually `yield`-delegate to any *iterable*; `yield *[1,2,3]` would consume the default *iterator* for the `[1,2,3]` array value.
### Why Delegation?
The purpose of `yield`-delegation is mostly code organization, and in that way is symmetrical with normal function calling.
Imagine two modules that respectively provide methods `foo()` and `bar()`, where `bar()` calls `foo()`. The reason the two are separate is generally because the proper organization of code for the program calls for them to be in separate functions. For example, there may be cases where `foo()` is called standalone, and other places where `bar()` calls `foo()`.
For all these exact same reasons, keeping generators separate aids in program readability, maintenance, and debuggability. In that respect, `yield *` is a syntactic shortcut for manually iterating over the steps of `*foo()` while inside of `*bar()`.
Such manual approach would be especially complex if the steps in `*foo()` were asynchronous, which is why you'd probably need to use that `run(..)` utility to do it. And as we've shown, `yield *foo()` eliminates the need for a sub-instance of the `run(..)` utility (like `run(foo)`).
### Delegating Messages
You may wonder how this `yield`-delegation works not just with *iterator* control but with the two-way message passing. Carefully follow the flow of messages in and out, through the `yield`-delegation:
```js
function *foo() {
console.log( "inside `*foo()`:", yield "B" );
console.log( "inside `*foo()`:", yield "C" );
return "D";
}
function *bar() {
console.log( "inside `*bar()`:", yield "A" );
// `yield`-delegation!
console.log( "inside `*bar()`:", yield *foo() );
console.log( "inside `*bar()`:", yield "E" );
return "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B
console.log( "outside:", it.next( 2 ).value );
// inside `*foo()`: 2
// outside: C
console.log( "outside:", it.next( 3 ).value );
// inside `*foo()`: 3
// inside `*bar()`: D
// outside: E
console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: 4
// outside: F
```
Pay particular attention to the processing steps after the `it.next(3)` call:
1. The `3` value is passed (through the `yield`-delegation in `*bar()`) into the waiting `yield "C"` expression inside of `*foo()`.
2. `*foo()` then calls `return "D"`, but this value doesn't get returned all the way back to the outside `it.next(3)` call.
3. Instead, the `"D"` value is sent as the result of the waiting `yield *foo()` expression inside of `*bar()` -- this `yield`-delegation expression has essentially been paused while all of `*foo()` was exhausted. So `"D"` ends up inside of `*bar()` for it to print out.
4. `yield "E"` is called inside of `*bar()`, and the `"E"` value is yielded to the outside as the result of the `it.next(3)` call.
From the perspective of the external *iterator* (`it`), it doesn't appear any differently between controlling the initial generator or a delegated one.
In fact, `yield`-delegation doesn't even have to be directed to another generator; it can just be directed to a non-generator, general *iterable*. For example:
```js
function *bar() {
console.log( "inside `*bar()`:", yield "A" );
// `yield`-delegation to a non-generator!
console.log( "inside `*bar()`:", yield *[ "B", "C", "D" ] );
console.log( "inside `*bar()`:", yield "E" );
return "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B
console.log( "outside:", it.next( 2 ).value );
// outside: C
console.log( "outside:", it.next( 3 ).value );
// outside: D
console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: undefined
// outside: E
console.log( "outside:", it.next( 5 ).value );
// inside `*bar()`: 5
// outside: F
```
Notice the differences in where the messages were received/reported between this example and the one previous.
Most strikingly, the default `array` *iterator* doesn't care about any messages sent in via `next(..)` calls, so the values `2`, `3`, and `4` are essentially ignored. Also, because that *iterator* has no explicit `return` value (unlike the previously used `*foo()`), the `yield *` expression gets an `undefined` when it finishes.
#### Exceptions Delegated, Too!
In the same way that `yield`-delegation transparently passes messages through in both directions, errors/exceptions also pass in both directions:
```js
function *foo() {
try {
yield "B";
}
catch (err) {
console.log( "error caught inside `*foo()`:", err );
}
yield "C";
throw "D";
}
function *bar() {
yield "A";
try {
yield *foo();
}
catch (err) {
console.log( "error caught inside `*bar()`:", err );
}
yield "E";
yield *baz();
// note: can't get here!
yield "G";
}
function *baz() {
throw "F";
}
var it = bar();
console.log( "outside:", it.next().value );
// outside: A
console.log( "outside:", it.next( 1 ).value );
// outside: B
console.log( "outside:", it.throw( 2 ).value );
// error caught inside `*foo()`: 2
// outside: C
console.log( "outside:", it.next( 3 ).value );
// error caught inside `*bar()`: D
// outside: E
try {
console.log( "outside:", it.next( 4 ).value );
}
catch (err) {
console.log( "error caught outside:", err );
}
// error caught outside: F
```
Some things to note from this snippet:
1. When we call `it.throw(2)`, it sends the error message `2` into `*bar()`, which delegates that to `*foo()`, which then `catch`es it and handles it gracefully. Then, the `yield "C"` sends `"C"` back out as the return `value` from the `it.throw(2)` call.
2. The `"D"` value that's next `throw`n from inside `*foo()` propagates out to `*bar()`, which `catch`es it and handles it gracefully. Then the `yield "E"` sends `"E"` back out as the return `value` from the `it.next(3)` call.
3. Next, the exception `throw`n from `*baz()` isn't caught in `*bar()` -- though we did `catch` it outside -- so both `*baz()` and `*bar()` are set to a completed state. After this snippet, you would not be able to get the `"G"` value out with any subsequent `next(..)` call(s) -- they will just return `undefined` for `value`.
### Delegating Asynchrony
Let's finally get back to our earlier `yield`-delegation example with the multiple sequential Ajax requests:
```js
function *foo() {
var r2 = yield request( "http://some.url.2" );
var r3 = yield request( "http://some.url.3/?v=" + r2 );
return r3;
}
function *bar() {
var r1 = yield request( "http://some.url.1" );
var r3 = yield *foo();
console.log( r3 );
}
run( bar );
```
Instead of calling `yield run(foo)` inside of `*bar()`, we just call `yield *foo()`.
In the previous version of this example, the Promise mechanism (controlled by `run(..)`) was used to transport the value from `return r3` in `*foo()` to the local variable `r3` inside `*bar()`. Now, that value is just returned back directly via the `yield *` mechanics.
Otherwise, the behavior is pretty much identical.
### Delegating "Recursion"
Of course, `yield`-delegation can keep following as many delegation steps as you wire up. You could even use `yield`-delegation for async-capable generator "recursion" -- a generator `yield`-delegating to itself:
```js
function *foo(val) {
if (val > 1) {
// generator recursion
val = yield *foo( val - 1 );
}
return yield request( "http://some.url/?v=" + val );
}
function *bar() {
var r1 = yield *foo( 3 );
console.log( r1 );
}
run( bar );
```
**Note:** Our `run(..)` utility could have been called with `run( foo, 3 )`, because it supports additional parameters being passed along to the initialization of the generator. However, we used a parameter-free `*bar()` here to highlight the flexibility of `yield *`.
What processing steps follow from that code? Hang on, this is going to be quite intricate to describe in detail:
1. `run(bar)` starts up the `*bar()` generator.
2. `foo(3)` creates an *iterator* for `*foo(..)` and passes `3` as its `val` parameter.
3. Because `3 > 1`, `foo(2)` creates another *iterator* and passes in `2` as its `val` parameter.
4. Because `2 > 1`, `foo(1)` creates yet another *iterator* and passes in `1` as its `val` parameter.
5. `1 > 1` is `false`, so we next call `request(..)` with the `1` value, and get a promise back for that first Ajax call.
6. That promise is `yield`ed out, which comes back to the `*foo(2)` generator instance.
7. The `yield *` passes that promise back out to the `*foo(3)` generator instance. Another `yield *` passes the promise out to the `*bar()` generator instance. And yet again another `yield *` passes the promise out to the `run(..)` utility, which will wait on that promise (for the first Ajax request) to proceed.
8. When the promise resolves, its fulfillment message is sent to resume `*bar()`, which passes through the `yield *` into the `*foo(3)` instance, which then passes through the `yield *` to the `*foo(2)` generator instance, which then passes through the `yield *` to the normal `yield` that's waiting in the `*foo(3)` generator instance.
9. That first call's Ajax response is now immediately `return`ed from the `*foo(3)` generator instance, which sends that value back as the result of the `yield *` expression in the `*foo(2)` instance, and assigned to its local `val` variable.
10. Inside `*foo(2)`, a second Ajax request is made with `request(..)`, whose promise is `yield`ed back to the `*foo(1)` instance, and then `yield *` propagates all the way out to `run(..)` (step 7 again). When the promise resolves, the second Ajax response propagates all the way back into the `*foo(2)` generator instance, and is assigned to its local `val` variable.
11. Finally, the third Ajax request is made with `request(..)`, its promise goes out to `run(..)`, and then its resolution value comes all the way back, which is then `return`ed so that it comes back to the waiting `yield *` expression in `*bar()`.
Phew! A lot of crazy mental juggling, huh? You might want to read through that a few more times, and then go grab a snack to clear your head!
## Generator Concurrency
As we discussed in both Chapter 1 and earlier in this chapter, two simultaneously running "processes" can cooperatively interleave their operations, and many times this can *yield* (pun intended) very powerful asynchrony expressions.
Frankly, our earlier examples of concurrency interleaving of multiple generators showed how to make it really confusing. But we hinted that there's places where this capability is quite useful.
Recall a scenario we looked at in Chapter 1, where two different simultaneous Ajax response handlers needed to coordinate with each other to make sure that the data communication was not a race condition. We slotted the responses into the `res` array like this:
```js
function response(data) {
if (data.url == "http://some.url.1") {
res[0] = data;
}
else if (data.url == "http://some.url.2") {
res[1] = data;
}
}
```
But how can we use multiple generators concurrently for this scenario?
```js
// `request(..)` is a Promise-aware Ajax utility
var res = [];
function *reqData(url) {
res.push(
yield request( url )
);
}
```
**Note:** We're going to use two instances of the `*reqData(..)` generator here, but there's no difference to running a single instance of two different generators; both approaches are reasoned about identically. We'll see two different generators coordinating in just a bit.
Instead of having to manually sort out `res[0]` and `res[1]` assignments, we'll use coordinated ordering so that `res.push(..)` properly slots the values in the expected and predictable order. The expressed logic thus should feel a bit cleaner.
But how will we actually orchestrate this interaction? First, let's just do it manually, with Promises:
```js
var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );
var p1 = it1.next().value;
var p2 = it2.next().value;
p1
.then( function(data){
it1.next( data );
return p2;
} )
.then( function(data){
it2.next( data );
} );
```
`*reqData(..)`'s two instances are both started to make their Ajax requests, then paused with `yield`. Then we choose to resume the first instance when `p1` resolves, and then `p2`'s resolution will restart the second instance. In this way, we use Promise orchestration to ensure that `res[0]` will have the first response and `res[1]` will have the second response.
But frankly, this is awfully manual, and it doesn't really let the generators orchestrate themselves, which is where the true power can lie. Let's try it a different way:
```js
// `request(..)` is a Promise-aware Ajax utility
var res = [];
function *reqData(url) {
var data = yield request( url );
// transfer control
yield;
res.push( data );
}
var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );
var p1 = it1.next().value;
var p2 = it2.next().value;
p1.then( function(data){
it1.next( data );
} );
p2.then( function(data){
it2.next( data );
} );
Promise.all( [p1,p2] )
.then( function(){
it1.next();
it2.next();
} );
```
OK, this is a bit better (though still manual!), because now the two instances of `*reqData(..)` run truly concurrently, and (at least for the first part) independently.
In the previous snippet, the second instance was not given its data until after the first instance was totally finished. But here, both instances receive their data as soon as their respective responses come back, and then each instance does another `yield` for control transfer purposes. We then choose what order to resume them in the `Promise.all([ .. ])` handler.
What may not be as obvious is that this approach hints at an easier form for a reusable utility, because of the symmetry. We can do even better. Let's imagine using a utility called `runAll(..)`:
```js
// `request(..)` is a Promise-aware Ajax utility
var res = [];
runAll(
function*(){
var p1 = request( "http://some.url.1" );
// transfer control
yield;
res.push( yield p1 );
},
function*(){
var p2 = request( "http://some.url.2" );
// transfer control
yield;
res.push( yield p2 );
}
);
```
**Note:** We're not including a code listing for `runAll(..)` as it is not only long enough to bog down the text, but is an extension of the logic we've already implemented in `run(..)` earlier. So, as a good supplementary exercise for the reader, try your hand at evolving the code from `run(..)` to work like the imagined `runAll(..)`. Also, my *asynquence* library provides a previously mentioned `runner(..)` utility with this kind of capability already built in, and will be discussed in Appendix A of this book.
Here's how the processing inside `runAll(..)` would operate:
1. The first generator gets a promise for the first Ajax response from `"http://some.url.1"`, then `yield`s control back to the `runAll(..)` utility.
2. The second generator runs and does the same for `"http://some.url.2"`, `yield`ing control back to the `runAll(..)` utility.
3. The first generator resumes, and then `yield`s out its promise `p1`. The `runAll(..)` utility does the same in this case as our previous `run(..)`, in that it waits on that promise to resolve, then resumes the same generator (no control transfer!). When `p1` resolves, `runAll(..)` resumes the first generator again with that resolution value, and then `res[0]` is given its value. When the first generator then finishes, that's an implicit transfer of control.
4. The second generator resumes, `yield`s out its promise `p2`, and waits for it to resolve. Once it does, `runAll(..)` resumes the second generator with that value, and `res[1]` is set.
In this running example, we use an outer variable called `res` to store the results of the two different Ajax responses -- that's our concurrency coordination making that possible.
But it might be quite helpful to further extend `runAll(..)` to provide an inner variable space for the multiple generator instances to *share*, such as an empty object we'll call `data` below. Also, it could take non-Promise values that are `yield`ed and hand them off to the next generator.
Consider:
```js
// `request(..)` is a Promise-aware Ajax utility
runAll(
function*(data){
data.res = [];
// transfer control (and message pass)
var url1 = yield "http://some.url.2";
var p1 = request( url1 ); // "http://some.url.1"
// transfer control
yield;
data.res.push( yield p1 );
},
function*(data){
// transfer control (and message pass)
var url2 = yield "http://some.url.1";
var p2 = request( url2 ); // "http://some.url.2"
// transfer control
yield;
data.res.push( yield p2 );
}
);
```
In this formulation, the two generators are not just coordinating control transfer, but actually communicating with each other, both through `data.res` and the `yield`ed messages that trade `url1` and `url2` values. That's incredibly powerful!
Such realization also serves as a conceptual base for a more sophisticated asynchrony technique called CSP (Communicating Sequential Processes), which we will cover in Appendix B of this book.
## Thunks
So far, we've made the assumption that `yield`ing a Promise from a generator -- and having that Promise resume the generator via a helper utility like `run(..)` -- was the best possible way to manage asynchrony with generators. To be clear, it is.
But we skipped over another pattern that has some mildly widespread adoption, so in the interest of completeness we'll take a brief look at it.
In general computer science, there's an old pre-JS concept called a "thunk." Without getting bogged down in the historical nature, a narrow expression of a thunk in JS is a function that -- without any parameters -- is wired to call another function.
In other words, you wrap a function definition around function call -- with any parameters it needs -- to *defer* the execution of that call, and that wrapping function is a thunk. When you later execute the thunk, you end up calling the original function.
For example:
```js
function foo(x,y) {
return x + y;
}
function fooThunk() {
return foo( 3, 4 );
}
// later
console.log( fooThunk() ); // 7
```
So, a synchronous thunk is pretty straightforward. But what about an async thunk? We can essentially extend the narrow thunk definition to include it receiving a callback.
Consider:
```js
function foo(x,y,cb) {
setTimeout( function(){
cb( x + y );
}, 1000 );
}
function fooThunk(cb) {
foo( 3, 4, cb );
}
// later
fooThunk( function(sum){
console.log( sum ); // 7
} );
```
As you can see, `fooThunk(..)` only expects a `cb(..)` parameter, as it already has values `3` and `4` (for `x` and `y`, respectively) pre-specified and ready to pass to `foo(..)`. A thunk is just waiting around patiently for the last piece it needs to do its job: the callback.
You don't want to make thunks manually, though. So, let's invent a utility that does this wrapping for us.
Consider:
```js
function thunkify(fn) {
var args = [].slice.call( arguments, 1 );
return function(cb) {
args.push( cb );
return fn.apply( null, args );
};
}
var fooThunk = thunkify( foo, 3, 4 );
// later
fooThunk( function(sum) {
console.log( sum ); // 7
} );
```
**Tip:** Here we assume that the original (`foo(..)`) function signature expects its callback in the last position, with any other parameters coming before it. This is a pretty ubiquitous "standard" for async JS function standards. You might call it "callback-last style." If for some reason you had a need to handle "callback-first style" signatures, you would just make a utility that used `args.unshift(..)` instead of `args.push(..)`.
The preceding formulation of `thunkify(..)` takes both the `foo(..)` function reference, and any parameters it needs, and returns back the thunk itself (`fooThunk(..)`). However, that's not the typical approach you'll find to thunks in JS.
Instead of `thunkify(..)` making the thunk itself, typically -- if not perplexingly -- the `thunkify(..)` utility would produce a function that produces thunks.
Uhhhh... yeah.
Consider:
```js
function thunkify(fn) {
return function() {
var args = [].slice.call( arguments );
return function(cb) {
args.push( cb );
return fn.apply( null, args );
};
};
}
```
The main difference here is the extra `return function() { .. }` layer. Here's how its usage differs:
```js
var whatIsThis = thunkify( foo );
var fooThunk = whatIsThis( 3, 4 );
// later
fooThunk( function(sum) {
console.log( sum ); // 7
} );
```
Obviously, the big question this snippet implies is what is `whatIsThis` properly called? It's not the thunk, it's the thing that will produce thunks from `foo(..)` calls. It's kind of like a "factory" for "thunks." There doesn't seem to be any kind of standard agreement for naming such a thing.
So, my proposal is "thunkory" ("thunk" + "factory"). So, `thunkify(..)` produces a thunkory, and a thunkory produces thunks. That reasoning is symmetric to my proposal for "promisory" in Chapter 3:
```js
var fooThunkory = thunkify( foo );
var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );
// later
fooThunk1( function(sum) {
console.log( sum ); // 7
} );
fooThunk2( function(sum) {
console.log( sum ); // 11
} );
```
**Note:** The running `foo(..)` example expects a style of callback that's not "error-first style." Of course, "error-first style" is much more common. If `foo(..)` had some sort of legitimate error-producing expectation, we could change it to expect and use an error-first callback. None of the subsequent `thunkify(..)` machinery cares what style of callback is assumed. The only difference in usage would be `fooThunk1(function(err,sum){..`.
Exposing the thunkory method -- instead of how the earlier `thunkify(..)` hides this intermediary step -- may seem like unnecessary complication. But in general, it's quite useful to make thunkories at the beginning of your program to wrap existing API methods, and then be able to pass around and call those thunkories when you need thunks. The two distinct steps preserve a cleaner separation of capability.
To illustrate:
```js
// cleaner:
var fooThunkory = thunkify( foo );
var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );
// instead of:
var fooThunk1 = thunkify( foo, 3, 4 );
var fooThunk2 = thunkify( foo, 5, 6 );
```
Regardless of whether you like to deal with the thunkories explicitly or not, the usage of thunks `fooThunk1(..)` and `fooThunk2(..)` remains the same.
### s/promise/thunk/
So what's all this thunk stuff have to do with generators?
Comparing thunks to promises generally: they're not directly interchangable as they're not equivalent in behavior. Promises are vastly more capable and trustable than bare thunks.
But in another sense, they both can be seen as a request for a value, which may be async in its answering.
Recall from Chapter 3 we defined a utility for promisifying a function, which we called `Promise.wrap(..)` -- we could have called it `promisify(..)`, too! This Promise-wrapping utility doesn't produce Promises; it produces promisories that in turn produce Promises. This is completely symmetric to the thunkories and thunks presently being discussed.
To illustrate the symmetry, let's first alter the running `foo(..)` example from earlier to assume an "error-first style" callback:
```js
function foo(x,y,cb) {
setTimeout( function(){
// assume `cb(..)` as "error-first style"
cb( null, x + y );
}, 1000 );
}
```
Now, we'll compare using `thunkify(..)` and `promisify(..)` (aka `Promise.wrap(..)` from Chapter 3):
```js
// symmetrical: constructing the question asker
var fooThunkory = thunkify( foo );
var fooPromisory = promisify( foo );
// symmetrical: asking the question
var fooThunk = fooThunkory( 3, 4 );
var fooPromise = fooPromisory( 3, 4 );
// get the thunk answer
fooThunk( function(err,sum){
if (err) {
console.error( err );
}
else {
console.log( sum ); // 7
}
} );
// get the promise answer
fooPromise
.then(
function(sum){
console.log( sum ); // 7
},
function(err){
console.error( err );
}
);
```
Both the thunkory and the promisory are essentially asking a question (for a value), and respectively the thunk `fooThunk` and promise `fooPromise` represent the future answers to that question. Presented in that light, the symmetry is clear.
With that perspective in mind, we can see that generators which `yield` Promises for asynchrony could instead `yield` thunks for asynchrony. All we'd need is a smarter `run(..)` utility (like from before) that can not only look for and wire up to a `yield`ed Promise but also to provide a callback to a `yield`ed thunk.
Consider:
```js
function *foo() {
var val = yield request( "http://some.url.1" );
console.log( val );
}
run( foo );
```
In this example, `request(..)` could either be a promisory that returns a promise, or a thunkory that returns a thunk. From the perspective of what's going on inside the generator code logic, we don't care about that implementation detail, which is quite powerful!
So, `request(..)` could be either:
```js
// promisory `request(..)` (see Chapter 3)
var request = Promise.wrap( ajax );
// vs.
// thunkory `request(..)`
var request = thunkify( ajax );
```
Finally, as a thunk-aware patch to our earlier `run(..)` utility, we would need logic like this:
```js
// ..
// did we receive a thunk back?
else if (typeof next.value == "function") {
return new Promise( function(resolve,reject){
// call the thunk with an error-first callback
next.value( function(err,msg) {
if (err) {
reject( err );
}
else {
resolve( msg );
}
} );
} )
.then(
handleNext,
function handleErr(err) {
return Promise.resolve(
it.throw( err )
)
.then( handleResult );
}
);
}
```
Now, our generators can either call promisories to `yield` Promises, or call thunkories to `yield` thunks, and in either case, `run(..)` would handle that value and use it to wait for the completion to resume the generator.
Symmetry wise, these two approaches look identical. However, we should point out that's true only from the perspective of Promises or thunks representing the future value continuation of a generator.
From the larger perspective, thunks do not in and of themselves have hardly any of the trustability or composability guarantees that Promises are designed with. Using a thunk as a stand-in for a Promise in this particular generator asynchrony pattern is workable but should be seen as less than ideal when compared to all the benefits that Promises offer (see Chapter 3).
If you have the option, prefer `yield pr` rather than `yield th`. But there's nothing wrong with having a `run(..)` utility which can handle both value types.
**Note:** The `runner(..)` utility in my *asynquence* library, which will be discussed in Appendix A, handles `yield`s of Promises, thunks and *asynquence* sequences.
## Pre-ES6 Generators
You're hopefully convinced now that generators are a very important addition to the async programming toolbox. But it's a new syntax in ES6, which means you can't just polyfill generators like you can Promises (which are just a new API). So what can we do to bring generators to our browser JS if we don't have the luxury of ignoring pre-ES6 browsers?
For all new syntax extensions in ES6, there are tools -- the most common term for them is transpilers, for trans-compilers -- which can take your ES6 syntax and transform it into equivalent (but obviously uglier!) pre-ES6 code. So, generators can be transpiled into code that will have the same behavior but work in ES5 and below.
But how? The "magic" of `yield` doesn't obviously sound like code that's easy to transpile. We actually hinted at a solution in our earlier discussion of closure-based *iterators*.
### Manual Transformation
Before we discuss the transpilers, let's derive how manual transpilation would work in the case of generators. This isn't just an academic exercise, because doing so will actually help further reinforce how they work.
Consider:
```js
// `request(..)` is a Promise-aware Ajax utility
function *foo(url) {
try {
console.log( "requesting:", url );
var val = yield request( url );
console.log( val );
}
catch (err) {
console.log( "Oops:", err );
return false;
}
}
var it = foo( "http://some.url.1" );
```
The first thing to observe is that we'll still need a normal `foo()` function that can be called, and it will still need to return an *iterator*. So, let's sketch out the non-generator transformation:
```js
function foo(url) {
// ..
// make and return an iterator
return {
next: function(v) {
// ..
},
throw: function(e) {
// ..
}
};
}
var it = foo( "http://some.url.1" );
```
The next thing to observe is that a generator does its "magic" by suspending its scope/state, but we can emulate that with function closure (see the *Scope & Closures* title of this series). To understand how to write such code, we'll first annotate different parts of our generator with state values:
```js
// `request(..)` is a Promise-aware Ajax utility
function *foo(url) {
// STATE *1*
try {
console.log( "requesting:", url );
var TMP1 = request( url );
// STATE *2*
var val = yield TMP1;
console.log( val );
}
catch (err) {
// STATE *3*
console.log( "Oops:", err );
return false;
}
}
```
**Note:** For more accurate illustration, we split up the `val = yield request..` statement into two parts, using the temporary `TMP1` variable. `request(..)` happens in state `*1*`, and the assignment of its completion value to `val` happens in state `*2*`. We'll get rid of that intermediate `TMP1` when we convert the code to its non-generator equivalent.
In other words, `*1*` is the beginning state, `*2*` is the state if the `request(..)` succeeds, and `*3*` is the state if the `request(..)` fails. You can probably imagine how any extra `yield` steps would just be encoded as extra states.
Back to our transpiled generator, let's define a variable `state` in the closure we can use to keep track of the state:
```js
function foo(url) {
// manage generator state
var state;
// ..
}
```
Now, let's define an inner function called `process(..)` inside the closure which handles each state, using a `switch` statement:
```js
// `request(..)` is a Promise-aware Ajax utility
function foo(url) {
// manage generator state
var state;
// generator-wide variable declarations
var val;
function process(v) {
switch (state) {
case 1:
console.log( "requesting:", url );
return request( url );
case 2:
val = v;
console.log( val );
return;
case 3:
var err = v;
console.log( "Oops:", err );
return false;
}
}
// ..
}
```
Each state in our generator is represented by its own `case` in the `switch` statement. `process(..)` will be called each time we need to process a new state. We'll come back to how that works in just a moment.
For any generator-wide variable declarations (`val`), we move those to a `var` declaration outside of `process(..)` so they can survive multiple calls to `process(..)`. But the "block scoped" `err` variable is only needed for the `*3*` state, so we leave it in place.
In state `*1*`, instead of `yield request(..)`, we did `return request(..)`. In terminal state `*2*`, there was no explicit `return`, so we just do a `return;` which is the same as `return undefined`. In terminal state `*3*`, there was a `return false`, so we preserve that.
Now we need to define the code in the *iterator* functions so they call `process(..)` appropriately:
```js
function foo(url) {
// manage generator state
var state;
// generator-wide variable declarations
var val;
function process(v) {
switch (state) {
case 1:
console.log( "requesting:", url );
return request( url );
case 2:
val = v;
console.log( val );
return;
case 3:
var err = v;
console.log( "Oops:", err );
return false;
}
}
// make and return an iterator
return {
next: function(v) {
// initial state
if (!state) {
state = 1;
return {
done: false,
value: process()
};
}
// yield resumed successfully
else if (state == 1) {
state = 2;
return {
done: true,
value: process( v )
};
}
// generator already completed
else {
return {
done: true,
value: undefined
};
}
},
"throw": function(e) {
// the only explicit error handling is in
// state *1*
if (state == 1) {
state = 3;
return {
done: true,
value: process( e )
};
}
// otherwise, an error won't be handled,
// so just throw it right back out
else {
throw e;
}
}
};
}
```
How does this code work?
1. The first call to the *iterator*'s `next()` call would move the generator from the uninitialized state to state `1`, and then call `process()` to handle that state. The return value from `request(..)`, which is the promise for the Ajax response, is returned back as the `value` property from the `next()` call.
2. If the Ajax request succeeds, the second call to `next(..)` should send in the Ajax response value, which moves our state to `2`. `process(..)` is again called (this time with the passed in Ajax response value), and the `value` property returned from `next(..)` will be `undefined`.
3. However, if the Ajax request fails, `throw(..)` should be called with the error, which would move the state from `1` to `3` (instead of `2`). Again `process(..)` is called, this time with the error value. That `case` returns `false`, which is set as the `value` property returned from the `throw(..)` call.
From the outside -- that is, interacting only with the *iterator* -- this `foo(..)` normal function works pretty much the same as the `*foo(..)` generator would have worked. So we've effectively "transpiled" our ES6 generator to pre-ES6 compatibility!
We could then manually instantiate our generator and control its iterator -- calling `var it = foo("..")` and `it.next(..)` and such -- or better, we could pass it to our previously defined `run(..)` utility as `run(foo,"..")`.
### Automatic Transpilation
The preceding exercise of manually deriving a transformation of our ES6 generator to pre-ES6 equivalent teaches us how generators work conceptually. But that transformation was really intricate and very non-portable to other generators in our code. It would be quite impractical to do this work by hand, and would completely obviate all the benefit of generators.
But luckily, several tools already exist that can automatically convert ES6 generators to things like what we derived in the previous section. Not only do they do the heavy lifting work for us, but they also handle several complications that we glossed over.
One such tool is regenerator (https://facebook.github.io/regenerator/), from the smart folks at Facebook.
If we use regenerator to transpile our previous generator, here's the code produced (at the time of this writing):
```js
// `request(..)` is a Promise-aware Ajax utility
var foo = regeneratorRuntime.mark(function foo(url) {
var val;
return regeneratorRuntime.wrap(function foo$(context$1$0) {
while (1) switch (context$1$0.prev = context$1$0.next) {
case 0:
context$1$0.prev = 0;
console.log( "requesting:", url );
context$1$0.next = 4;
return request( url );
case 4:
val = context$1$0.sent;
console.log( val );
context$1$0.next = 12;
break;
case 8:
context$1$0.prev = 8;
context$1$0.t0 = context$1$0.catch(0);
console.log("Oops:", context$1$0.t0);
return context$1$0.abrupt("return", false);
case 12:
case "end":
return context$1$0.stop();
}
}, foo, this, [[0, 8]]);
});
```
There's some obvious similarities here to our manual derivation, such as the `switch` / `case` statements, and we even see `val` pulled out of the closure just as we did.
Of course, one trade-off is that regenerator's transpilation requires a helper library `regeneratorRuntime` that holds all the reusable logic for managing a general generator / *iterator*. A lot of that boilerplate looks different than our version, but even then, the concepts can be seen, like with `context$1$0.next = 4` keeping track of the next state for the generator.
The main takeaway is that generators are not restricted to only being useful in ES6+ environments. Once you understand the concepts, you can employ them throughout your code, and use tools to transform the code to be compatible with older environments.
This is more work than just using a `Promise` API polyfill for pre-ES6 Promises, but the effort is totally worth it, because generators are so much better at expressing async flow control in a reason-able, sensible, synchronous-looking, sequential fashion.
Once you get hooked on generators, you'll never want to go back to the hell of async spaghetti callbacks!
## Review
Generators are a new ES6 function type that does not run-to-completion like normal functions. Instead, the generator can be paused in mid-completion (entirely preserving its state), and it can later be resumed from where it left off.
This pause/resume interchange is cooperative rather than preemptive, which means that the generator has the sole capability to pause itself, using the `yield` keyword, and yet the *iterator* that controls the generator has the sole capability (via `next(..)`) to resume the generator.
The `yield` / `next(..)` duality is not just a control mechanism, it's actually a two-way message passing mechanism. A `yield ..` expression essentially pauses waiting for a value, and the next `next(..)` call passes a value (or implicit `undefined`) back to that paused `yield` expression.
The key benefit of generators related to async flow control is that the code inside a generator expresses a sequence of steps for the task in a naturally sync/sequential fashion. The trick is that we essentially hide potential asynchrony behind the `yield` keyword -- moving the asynchrony to the code where the generator's *iterator* is controlled.
In other words, generators preserve a sequential, synchronous, blocking code pattern for async code, which lets our brains reason about the code much more naturally, addressing one of the two key drawbacks of callback-based async.
================================================
FILE: async & performance/ch5.md
================================================
# You Don't Know JS: Async & Performance
# Глава 5: Производительность программы
До сих пор эта книга была посвящена тому, как эффективнее использовать паттерны асинхронности. Но мы не рассматривали напрямую, почему асинхронность действительно важна для JS. Самая очевидная и явная причина - это **производительность**.
Например, если вам нужно выполнить два независимых Ajax-запроса, но вам нужно дождаться их завершения перед выполнением следующей задачи, у вас есть два варианта моделирования этого взаимодействия: последовательный и параллельный.
Вы можете сделать первый запрос и ждать начала второго запроса, пока не завершится первый. Или, как мы уже видели на примере обещаний и генераторов, вы можете выполнить оба запроса "параллельно" и выразить "ворота" для ожидания выполнения обоих запросов, прежде чем двигаться дальше.
Очевидно, что последний вариант, как правило, будет более производительным, чем первый. А лучшая производительность обычно приводит к лучшему пользовательскому опыту
Возможно даже, что асинхронность (чередующийся параллелизм) может улучшить только восприятие производительности, даже если на выполнение программы в целом уходит столько же времени. Восприятие производительности пользователем не менее -- если не более -- важно, чем реальная измеримая производительность.
Теперь мы хотим выйти за рамки локализованных паттернов асинхронности и поговорить о некоторых деталях производительности на уровне программы.
**Примечание:** Вам могут быть интересны вопросы микропроизводительности, например, что быстрее - `a++` или `++a`. Мы рассмотрим такие детали производительности в следующей главе "Бенчмаркинг и тюнинг".
## Web Workers
Если у вас есть задачи с интенсивной обработкой данных, но вы не хотите, чтобы они выполнялись в главном потоке (что может замедлить работу браузера/интерфейса), вы могли бы пожелать, чтобы JavaScript работал в многопоточном режиме.
В главе 1 мы подробно рассказывали о том, что JavaScript является однопоточным. И это по-прежнему верно. Но один поток - не единственный способ организовать выполнение программы.
Представьте, что вы разделили свою программу на две части, запустили одну из них в основном потоке пользовательского интерфейса, а другую - в совершенно отдельном потоке.
Какие проблемы возникнут при такой архитектуре?
Например, вы бы хотели знать, означает ли выполнение в отдельном потоке, что он работает параллельно (в системах с несколькими процессорами/ядрами), так что долго выполняющийся процесс во втором потоке не будет блокировать основной поток программы. В противном случае "виртуальные потоки" не принесут особой пользы по сравнению с тем, что мы уже имеем в JS с асинхронным параллелизмом.
Кроме того, вам нужно знать, имеют ли эти две части программы доступ к одной и той же общей области видимости/ресурсам. Если да, то возникают все вопросы, с которыми сталкиваются многопоточные языки (Java, C++ и т. д.), например, необходимость кооперативной или вытесняющей блокировки (мьютексы(взаимное исключение) и т. д.). Это очень много дополнительной работы, и к ней не стоит относиться легкомысленно.
Кроме того, вы бы хотели знать, как эти две части могут "общаться", если они не могут делиться областью видимости/ресурсами.
Всё это - отличные вопросы для рассмотрения, поскольку мы изучаем функцию, добавленную в веб-платформу примерно во времена HTML5, под названием "Web Workers". Это функция браузера (он же хост-окружение), и на самом деле она почти никак не связана с самим языком JS. То есть в JavaScript на данный момент нет никаких функций, поддерживающих многопоточное выполнение.
Но такая среда, как ваш браузер, может легко предоставить несколько экземпляров движка JavaScript, каждый со своим потоком, и позволить вам запускать разные программы в каждом из потоков. Каждый из этих отдельных потоковых частей вашей программы называется "(Web) Worker". Такой тип параллелизма называется "параллелизмом задач (Task parallelism)", поскольку акцент делается на разделении фрагментов вашей программы для параллельного выполнения.
Из вашей основной JS-программы (или другого Worker) вы создаете Worker следующим образом:
```js
var w1 = new Worker( "http://some.url.1/mycoolworker.js" );
```
URL должен указывать на местоположение JS-файла (не HTML-страницы!), который предназначен для загрузки в Worker. После этого браузер запустит отдельный поток и позволит этому файлу выполняться в нем как независимой программе.
**Примечание:** Тип Worker, созданный с помощью такого URL, называется "Dedicated Worker". Но вместо URL-адреса внешнего файла вы можете создать "Inline Worker", предоставив Blob URL-адрес (еще одна возможность HTML5); по сути, это inline-файл, хранящийся в одном (двоичном) значении. Однако Blob'ы выходят за рамки того, что мы будем обсуждать здесь.
Workers не имеют общей области видимости или ресурсов друг с другом или с основной программой - это вывело бы на первый план все кошмары многопоточного программирования - но вместо этого их связывает простой механизм обмена сообщениями о событиях.
Объект `w1` Worker является слушателем событий и триггером, который позволяет вам подписываться на события, посылаемые Worker, а также посылать события Worker.
Вот как прослушивать события (если точнее, фиксированное событие `"message"`):
```js
w1.addEventListener( "message", function(evt){
// evt.data
} );
```
И вы можете отправить событие `"message"` в Worker:
```js
w1.postMessage( "something cool to say" );
```
Внутри Worker отправка событий полностью симметрична:
```js
// "mycoolworker.js"
addEventListener( "message", function(evt){
// evt.data
} );
postMessage( "a really cool reply" );
```
Обратите внимание, что выделенный Worker находится в отношениях один-к-одному с программой, которая его создала. То есть событие ``message`` не нуждается в устранении неоднозначности, потому что мы уверены, что оно могло произойти только в результате этой связи "один к одному" - либо от Worker, либо от главной страницы.
Обычно приложение главной страницы создает Worker'ы, но при необходимости Worker может создавать свои собственные дочерние Worker(ы) - так называемые subworkers. Иногда полезно делегировать такие детали некоему "главному" Worker'у, который порождает других Worker'ов для обработки части задачи. К сожалению, на момент написания этой статьи Chrome все еще не поддерживает subworkers, в то время как Firefox поддерживает.
Чтобы немедленно убить Worker из программы, которая его создала, вызовите `terminate()` для объекта Worker (как `w1` в предыдущих сниппетах). Резкое завершение потока Worker не дает ему возможности закончить свою работу или очистить ресурсы. Это похоже на то, как если бы вы закрыли вкладку браузера, чтобы убить страницу.
Если в браузере есть две или более страниц (или несколько вкладок с одной и той же страницей!), которые пытаются создать Worker из одного и того же URL-адреса файла, то в итоге они окажутся совершенно отдельными Worker'ами. Вскоре мы обсудим способ "совместного использования" Worker'ов.
**Примечание:** Может показаться, что вредоносная или невежественная JS-программа может легко провести атаку на отказ в обслуживании системы (denial-of-service attack), породив сотни Worker'ов, каждый из которых, казалось бы, имеет свой собственный поток. Хотя это правда, что есть некоторая гарантия того, что Worker окажется в отдельном потоке, эта гарантия не безгранична. Система вольна решать, сколько реальных потоков/процессоров/ядер она действительно хочет создать. Невозможно предсказать или гарантировать, к какому количеству потоков вы получите доступ, хотя многие люди полагают, что их будет не меньше, чем количество доступных процессоров/ядер. Я думаю, что самое безопасное предположение - это наличие хотя бы одного другого потока, кроме основного потока пользовательского интерфейса, но это не более того.
### Worker Environment
Внутри Worker у вас нет доступа ни к одному из ресурсов основной программы. Это означает, что вы не можете получить доступ ни к глобальным переменным, ни к DOM страницы, ни к другим ресурсам. Помните: это совершенно отдельный поток.
Однако вы можете выполнять сетевые операции (Ajax, WebSockets) и устанавливать таймеры. Также Worker имеет доступ к собственной копии нескольких важных глобальных переменных/функций, включая `navigator`, `location`, `JSON` и `applicationCache`.
Вы также можете загружать в Worker дополнительные JS-скрипты, используя `importScripts(...)`:
```js
// inside the Worker
importScripts( "foo.js", "bar.js" );
```
Эти скрипты загружаются синхронно, что означает, что вызов `importScripts(...)` будет блокировать выполнение остальной части Worker'а, пока файл(ы) не закончит(ат) загрузку и выполнение.
**Примечание:** Также обсуждался вопрос о раскрытии API `