diff --git a/1-js/10-es-modern/17-generator/anon.png b/1-js/10-es-modern/17-generator/anon.png
new file mode 100644
index 00000000..a1675d2f
Binary files /dev/null and b/1-js/10-es-modern/17-generator/anon.png differ
diff --git a/1-js/10-es-modern/17-generator/article.md b/1-js/10-es-modern/17-generator/article.md
index 1922c1fc..644155d2 100644
--- a/1-js/10-es-modern/17-generator/article.md
+++ b/1-js/10-es-modern/17-generator/article.md
@@ -1,5 +1,5 @@
-# Генераторы [todo]
+# Генераторы
Генераторы -- новый вид функций в современном JavaScript. Они отличаются от обычных тем, что могут приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять его позже, в произвольный момент времени.
@@ -261,9 +261,10 @@ for(let code of generateAlphaNum()) {
alert(str); // 0..9A..Za..z
```
-Код выше по поведению полностью идентичен варианту с `yield*`.
+Код выше по поведению полностью идентичен варианту с `yield*`. При этом, конечно, переменные вложенного генератора не попадают во внешний, "делегирование" только выводит результаты во внешний поток.
+
+Композиция -- это естественное встраивание одного генератора в поток другого. При композиции значения из вложенного генератора выдаются "по мере готовности". Поэтому она будет работать даже если поток данных из вложенного генератора оказался бесконечным или ожидает какого-либо условия для завершения.
-Композиция -- это естественное встраивание одного генератора в поток другого. В частности, если поток данных из вложенного генератора оказался бесконечным (или ожидает какого-либо условия для завершения), то с точки зрения композиции это вполне нормально.
## yield -- дорога в обе стороны
@@ -327,7 +328,7 @@ function* gen() {
alert(ask1); // 4
- let ask2 = yield "А сколько будет 3 * 3?"
+ let ask2 = yield "3 * 3?"
alert(ask2); // 9
}
@@ -353,11 +354,12 @@ alert( generator.next(9).done ); // true
Третий `next(9)` передаёт `9` в генератор как результат второго `yield` и возобновляет выполнение, которое завершается окончанием функции, так что `done: true`.
-Получается "пинг-понг": каждый `next(value)` передаёт в генератор значение, которое становится результатом текущего `yield`, возобновляет выполнение и получает выражение из следующего `yield`.
+Получается "пинг-понг": каждый `next(value)` передаёт в генератор значение, которое становится результатом текущего `yield`, возобновляет выполнение и получает выражение из следующего `yield`. Исключением является первый вызов `next`, который не может передать значение в генератор, т.к. ещё не было ни одного `yield`.
+
+На рисунке ниже изображены шаги 3-4 из примера:
-Исключением является первый вызов `next`, который не может передать значение в генератор, т.к. ещё не было ни одного `yield`.
## generator.throw
@@ -366,8 +368,6 @@ alert( generator.next(9).done ); // true
...Но "вернуть" можно не только результат, но и ошибку!
-Как и любая операция, `yield` может завершиться со значением, либо сгенерировать исключение. И то и то -- разновидность результата.
-
Для того, чтобы передать в `yield` ошибку, используется вызов `generator.throw(err)`. При этом на строке с `yield` возникает исключение.
Например, в коде ниже обращение к внешнему коду `yield "Сколько будет 2 + 2"` завершится с ошибкой:
@@ -380,9 +380,9 @@ alert( generator.next(9).done ); // true
function* gen() {
try {
// в этой строке возникнет ошибка
- let result = yield "Сколько будет 2 + 2?";
+ let result = yield "Сколько будет 2 + 2?"; // (**)
- alert("не сработает, так как ошибка выпадет в try..catch");
+ alert("выше будет исключение ^^^");
} catch(e) {
alert(e); // выведет ошибку
}
@@ -397,9 +397,9 @@ generator.throw(new Error("ответ не найден в моей базе д
*/!*
```
-"Вброшенная" в строке `(*)` ошибка возникает в строке с `yield` и обрабатывается как обычно. В примере выше она перехватывается `try..catch` и выводится.
+"Вброшенная" в строке `(*)` ошибка возникает в строке с `yield` `(**)`. Далее она обрабатывается как обычно. В примере выше она перехватывается `try..catch` и выводится.
-Если её не перехватить, то она "выпадет" из генератора. По стеку ближайший вызов, который инициировал выполнение -- это строка с `.throw`. Можно перехватить её там, как и продемонстрировано в примере ниже:
+Если ошибку не перехватить, то она "выпадет" из генератора. По стеку ближайший вызов, который инициировал выполнение -- это строка с `.throw`. Можно перехватить её там, как и продемонстрировано в примере ниже:
```js
@@ -426,7 +426,7 @@ try {
Если же ошибка и там не перехвачена, то дальше -- как обычно, либо `try..catch` снаружи, либо она "повалит" скрипт.
-# Плоский асинхронный код
+## Плоский асинхронный код
Одна из основных областей применения генераторов -- написание "плоского" асинхронного кода.
@@ -493,9 +493,13 @@ execute( showUserAvatar() );
Вместе с тем, это -- всего лишь набросок, чтобы было понятно, как такая функция в принципе работает. Есть уже готовые реализации, обладающие большим количеством возможностей.
-Одна из самых известных -- это библиотека [co](https://github.com/tj/co).
+Одна из самых известных -- это библиотека [co](https://github.com/tj/co), которую мы рассмотрим далее.
-Её нужно подключить (через `npm` или https://cdnjs.cloudflare.com/ajax/libs/co/4.1.0/index.min.js), после чего запускаем её с функцией-генератором, вот так:
+## Библиотека "co"
+
+Библиотека `co`, как и `execute` в примере выше, получает генератор и выполняет его.
+
+Начнём сразу с примера, а потом -- детали и полезные возможности:
```js
//+ run
@@ -513,7 +517,8 @@ co(function*() {
})
```
-Библиотека `co`, как и `executor` выше, выполняет генератор, получает из него промисы и возвращает обратно их результаты. В примере выше `function*()` делает `yield` промиса с `setTimeout`, который через секунду возвращает `1`.
+
+Предполагается, что библиотека `co` подключена к странице , например, отсюда: [](http://cdnjs.com/libraries/co/). В примере выше `function*()` делает `yield` промиса с `setTimeout`, который через секунду возвращает `1`.
Вызов `co(…)` возвращает промис с результатом генератора. Если в примере выше `function*()` что-то возвратит, то это можно будет получить через `.then` в результате `co`:
@@ -561,19 +566,17 @@ co(function*() {
[/warn]
-**Библиотека `co` может работать не только с генераторами.**
-
-Есть несколько видов значений, которые умеет обрабатывать `co`:
+Библиотека `co` умеет выполнять не только промисы. Есть несколько видов значений, которые можно `yield`, и их обработает `co`:
-- Объект-генератор -- выполняется описанным выше способом.
+- Промис.
+- Объект-генератор.
- Функция-генератор `function*()` -- `co` её выполнит, затем выполнит полученный генератор.
-- Промис -- `co` вернёт его (на самом деле новый промис, но он вернёт этот).
-- Функция с единственным аргументом вида `function(callback)` -- библиотека `co` её запустит со своей функцией-`callback` и будет ожидать, что при ошибке она вызовет `callback(err)`, а при успешном выполнении -- `callback(null, result)`, то есть в первом аргументе -- будет ошибка (если есть), а втором -- результат (если нет ошибки). После чего результат будет передан в генератор.
+- Функция с единственным аргументом вида `function(callback)` -- библиотека `co` её запустит со своей функцией-`callback` и будет ожидать, что при ошибке она вызовет `callback(err)`, а при успешном выполнении -- `callback(null, result)`. То есть, в первом аргументе -- будет ошибка (если есть), а втором -- результат (если нет ошибки). После чего результат будет передан в генератор.
- Массив или объект из вышеперечисленного. При этом все задачи будут выполнены параллельно, и результат, в той же структуре, будет выдан наружу.
-В примере ниже происходит `yield` всех этих видов значений:
+В примере ниже происходит `yield` всех этих видов значений. Библиотека `co` обеспечивает их выполнение и возврат результата в генератор:
```js
//+ run
@@ -596,7 +599,7 @@ co(function*() {
result = yield Promise.resolve(3); // промис
result = yield function(callback) { // function(callback)
- callback(null, 4);
+ setTimeout(() => callback(null, 4), 1000);
};
@@ -613,18 +616,28 @@ co(function*() {
});
```
-Библиотека `co` обрабатывает результаты рекурсивно. То есть, если в результате `yield` получается генератор, то он тоже выполняется библиотекой `co`, и так далее.
+[smart header="Устаревший `yield function(callback)`"]
+Отдельно заметим вариант с `yield function(callback)`. Такие функции (с единственным-аргументом callback'ом) называют "thunk".
-Звучит это сложнее, чем на самом деле. Практическое следствие простое -- мы можем использовать `yield` во вложенных вызовах функций.
+Функция обязана выполниться и вызвать (асинхронно) либо `callback(err)` с ошибкой, либо `callback(null, result)` с результатом.
-Например:
+Этот способ использования является устаревшим, так как там, где можно использовать `yield function(callback)`, можно использовать и промисы. При этом промисы мощнее. Но в старом коде его ещё можно встретить.
+[/smart]
+
+
+Посмотрим пример посложнее, с композицией генераторов:
```js
//+ run
'use strict';
+co(function*() {
+ let result = yield* gen();
+ alert(result); // hello
+});
+
function* gen() {
- return yield gen2();
+ return yield* gen2();
}
function* gen2() {
@@ -633,17 +646,25 @@ function* gen2() {
);
return result;
}
+```
+
+Это -- отличный вариант для библиотеки `co`. Композиция `yield* gen()` вызывает `gen()` в потоке внешнего генератора. Аналогично делает и `yield* gen()`.
+
+Поэтому `yield new Promise` из `gen2()` попадает напрямую в библиотеку `co`, как еслы бы он был сделан так:
+
+```js
+//+ run
+'use strict';
co(function*() {
- let result = yield gen();
+ // gen() и затем gen2 встраиваются во внешний генератор
+ let result = yield new Promise(
+ resolve => setTimeout(resolve, 1000, 'hello')
+ );
alert(result); // hello
});
```
-В примере выше: первый `yield` возвращет генератор `gen()`, при его выполнении `yield'ится` генератор `gen2()`, внутри которого `yield'ится` промис, который завершается с `"hello"`.
-
-Библиотека `co` при этом -- только на самом верхнем уровне.
-
Пример `showUserAvatar()` можно переписать с использованием `co` вот так:
```js
@@ -687,13 +708,11 @@ function* showUserAvatar() {
let avatarUrl;
-*!*
try {
avatarUrl = yield* fetchAvatar('/article/generator/user.json');
} catch(e) {
avatarUrl = '/article/generator/anon.png';
}
-*/!*
let img = new Image();
img.src = avatarUrl;
@@ -710,15 +729,155 @@ function* showUserAvatar() {
co(showUserAvatar);
```
-Заметим, что для перехвата ошибок при получении аватара используется `try..catch` вокруг `yield* fetchAvatar`. Несмотря на то, что операции -- асинхронные, мы можем использовать обычный `try..catch`. И это очень удобно!
+Заметим, что для перехвата ошибок при получении аватара используется `try..catch` вокруг `yield* fetchAvatar`:
-TODO
+```js
+try {
+ avatarUrl = yield* fetchAvatar('/article/generator/user.json');
+} catch(e) {
+ avatarUrl = '/article/generator/anon.png';
+}
+```
+Это -- одно из главных удобств использования генераторов. Несмотря на то, что операции `fetch` -- асинхронные, мы можем использовать обычный `try..catch` для обработки ошибок в них.
+## Для генераторов -- только yield*
+Библиотека `co` технически позволяет писать код так:
+```js
+let user = yield fetchUser(url);
+// вместо
+// let user = yield* fetchUser(url);
+```
+То есть, можно сделать `yield` генератора, `co()` его выполнит и передаст значение обратно. Как мы видели выше, библиотека `co` -- довольно всеядна, поэтому будет работать и так и эдак.
+Однако, рекомендуется использовать для вызова функций-генераторов именно `yield*`.
+Причин для этого несколько:
+
+- Делегирование генераторов `yield*` -- это встроенный механизм JavaScript. Вместо возвращения значения обратно в `co`, выполнения кода библиотеки... Мы просто используем возможности языка. Это правильнее.
+- Поскольку не происходит лишних вызовов, это быстрее по производительности.
+- И, наконец, пожалуй, самое приятное -- делегирование генераторов не портит стек.
+
+Проиллюстрируем последнее на примере:
+
+```js
+//+ run
+'use strict';
+
+co(function*() {
+
+*!*
+ // при запуске в стеке не будет видно этой строки
+ yield g(); // (*)
+*/!*
+
+}).catch(function(err) {
+ alert(err.stack);
+});
+
+function* g() {
+ throw new Error("my error");
+}
+```
+
+При запуске этого кода стек может выглядеть примерно так:
+```js
+*!*
+at g (eval at runJS …, :13:9)
+*/!*
+ at GeneratorFunctionPrototype.next (native)
+ at onFulfilled (…/co/…/index.min.js:1:1136)
+ at …/co/…/index.min.js:1:1076
+ at co (…/co/…/index.min.js:1:1039)
+ at toPromise (…/co/…/index.min.js:1:1740)
+ at next (…/co/…/index.min.js:1:1351)
+ at onFulfilled (…/co/…/index.min.js:1:1172)
+ at …/co/…/index.min.js:1:1076
+ at co (…/co/…/index.min.js:1:1039)
+```
+
+Детали здесь не имеют значения, самое важное -- почти весь стек находится внутри библиотеки `co`.
+
+Из оригинального скрипта там только одна строка (первая):
+```js
+at g (eval at runJS …, :13:9)
+```
+
+То есть, стек говорит, что ошибка возникла в строке `13`:
+```js
+// строка 13 из кода выше
+throw new Error("my error");
+```
+
+Что ж, спасибо. Но как мы оказались на этой строке? Об этом в стеке нет ни слова!
+
+Заменим в строке `(*)` вызов `yield` на `yield*`:
+
+```js
+//+ run
+'use strict';
+
+co(function*() {
+
+*!*
+ // заменили yield на yield*
+ yield* g(); // (*)
+*/!*
+
+}).catch(function(err) {
+ alert(err.stack);
+});
+
+function* g() {
+ throw new Error("my error");
+}
+```
+
+Пример стека теперь:
+```js
+*!*
+at g (eval at runJS …, :13:9)
+*/!*
+ at GeneratorFunctionPrototype.next (native)
+*!*
+ at eval (eval at runJS …, :6:10)
+*/!*
+ at GeneratorFunctionPrototype.next (native)
+ at onFulfilled (…/co/…/index.min.js:1:1136)
+ at …/co/…/index.min.js:1:1076
+ at co (…/co/…/index.min.js:1:1039)
+*!*
+ at eval (eval at runJS …, :3:1)
+*/!*
+ at eval (native)
+ at runJS (…)
+```
+
+Если очистить от вспомогательных вызовов, то эти строки -- как раз то, что нам надо:
+```js
+at g (eval at runJS …, :13:9)
+ at eval (eval at runJS …, :6:10)
+ at eval (eval at runJS …, :3:1)
+```
+
+Теперь видно, что (читаем снизу) исходный вызов был в строке `3`, далее -- вложенный в строке `6`, и затем произошла ошибка была в строке `13`.
+
+Почему вариант с простым `yield` не работает -- достаточно очевидно, если внимательно посмотреть на код и воспроизвести в уме, как он работает. Оставляем это упражнение вдумчивому читателю.
+
+Итого, рекомендация уже достаточно обоснована -- при запуске вложенных генераторов используем `yield*`.
+
+## Итого
+
+
+- Генераторы создаются при помощи функций-генераторов `function*(…) {…}`.
+- Внутри генераторов и только них разрешён оператор `yield`. Это иногда создаёт недобства, поскольку в коллбэках `.map/.forEach` сделать `yield` нельзя. Впрочем, можно сделать `yield` массива (при использовании `co`).
+- Внешний код и генератор обмениваются промежуточными результатами посредством вызовов `next/yield`.
+- Генераторы позволяют писать плоский асинхронный код, при помощи библиотки `co`.
+
+
+Что касается кросс-браузерной поддержки -- она стремительно приближается. Пока же можно использовать генераторы вместе с [Babel](https://babeljs.io).
[head]