fixes #54, es6
BIN
1-js/10-es-modern/17-generator/anon.png
Normal file
After Width: | Height: | Size: 30 KiB |
|
@ -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
|
|||
<li>Третий `next(9)` передаёт `9` в генератор как результат второго `yield` и возобновляет выполнение, которое завершается окончанием функции, так что `done: true`.</li>
|
||||
</ol>
|
||||
|
||||
Получается "пинг-понг": каждый `next(value)` передаёт в генератор значение, которое становится результатом текущего `yield`, возобновляет выполнение и получает выражение из следующего `yield`.
|
||||
Получается "пинг-понг": каждый `next(value)` передаёт в генератор значение, которое становится результатом текущего `yield`, возобновляет выполнение и получает выражение из следующего `yield`. Исключением является первый вызов `next`, который не может передать значение в генератор, т.к. ещё не было ни одного `yield`.
|
||||
|
||||
На рисунке ниже изображены шаги 3-4 из примера:
|
||||
|
||||
<img src="genYield2-3.png">
|
||||
|
||||
Исключением является первый вызов `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`:
|
||||
|
||||
<ul>
|
||||
<li>Объект-генератор -- выполняется описанным выше способом.</li>
|
||||
<li>Промис.</li>
|
||||
<li>Объект-генератор.</li>
|
||||
<li>Функция-генератор `function*()` -- `co` её выполнит, затем выполнит полученный генератор.</li>
|
||||
<li>Промис -- `co` вернёт его (на самом деле новый промис, но он вернёт этот).</li>
|
||||
<li>Функция с единственным аргументом вида `function(callback)` -- библиотека `co` её запустит со своей функцией-`callback` и будет ожидать, что при ошибке она вызовет `callback(err)`, а при успешном выполнении -- `callback(null, result)`, то есть в первом аргументе -- будет ошибка (если есть), а втором -- результат (если нет ошибки). После чего результат будет передан в генератор.</li>
|
||||
<li>Функция с единственным аргументом вида `function(callback)` -- библиотека `co` её запустит со своей функцией-`callback` и будет ожидать, что при ошибке она вызовет `callback(err)`, а при успешном выполнении -- `callback(null, result)`. То есть, в первом аргументе -- будет ошибка (если есть), а втором -- результат (если нет ошибки). После чего результат будет передан в генератор.</li>
|
||||
<li>Массив или объект из вышеперечисленного. При этом все задачи будут выполнены параллельно, и результат, в той же структуре, будет выдан наружу.</li>
|
||||
</ul>
|
||||
|
||||
В примере ниже происходит `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*`.
|
||||
|
||||
Причин для этого несколько:
|
||||
<ol>
|
||||
<li>Делегирование генераторов `yield*` -- это встроенный механизм JavaScript. Вместо возвращения значения обратно в `co`, выполнения кода библиотеки... Мы просто используем возможности языка. Это правильнее.</li>
|
||||
<li>Поскольку не происходит лишних вызовов, это быстрее по производительности.</li>
|
||||
<li>И, наконец, пожалуй, самое приятное -- делегирование генераторов не портит стек.</li>
|
||||
</ol>
|
||||
|
||||
Проиллюстрируем последнее на примере:
|
||||
|
||||
```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 …, <anonymous>: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 …, <anonymous>: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 …, <anonymous>:13:9)
|
||||
*/!*
|
||||
at GeneratorFunctionPrototype.next (native)
|
||||
*!*
|
||||
at eval (eval at runJS …, <anonymous>: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 …, <anonymous>:3:1)
|
||||
*/!*
|
||||
at eval (native)
|
||||
at runJS (…)
|
||||
```
|
||||
|
||||
Если очистить от вспомогательных вызовов, то эти строки -- как раз то, что нам надо:
|
||||
```js
|
||||
at g (eval at runJS …, <anonymous>:13:9)
|
||||
at eval (eval at runJS …, <anonymous>:6:10)
|
||||
at eval (eval at runJS …, <anonymous>:3:1)
|
||||
```
|
||||
|
||||
Теперь видно, что (читаем снизу) исходный вызов был в строке `3`, далее -- вложенный в строке `6`, и затем произошла ошибка была в строке `13`.
|
||||
|
||||
Почему вариант с простым `yield` не работает -- достаточно очевидно, если внимательно посмотреть на код и воспроизвести в уме, как он работает. Оставляем это упражнение вдумчивому читателю.
|
||||
|
||||
Итого, рекомендация уже достаточно обоснована -- при запуске вложенных генераторов используем `yield*`.
|
||||
|
||||
## Итого
|
||||
|
||||
<ul>
|
||||
<li>Генераторы создаются при помощи функций-генераторов `function*(…) {…}`.</li>
|
||||
<li>Внутри генераторов и только них разрешён оператор `yield`. Это иногда создаёт недобства, поскольку в коллбэках `.map/.forEach` сделать `yield` нельзя. Впрочем, можно сделать `yield` массива (при использовании `co`).</li>
|
||||
<li>Внешний код и генератор обмениваются промежуточными результатами посредством вызовов `next/yield`.</li>
|
||||
<li>Генераторы позволяют писать плоский асинхронный код, при помощи библиотки `co`.</li>
|
||||
</ul>
|
||||
|
||||
Что касается кросс-браузерной поддержки -- она стремительно приближается. Пока же можно использовать генераторы вместе с [Babel](https://babeljs.io).
|
||||
|
||||
[head]
|
||||
<style>
|
||||
|
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 57 KiB |