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`: -В примере ниже происходит `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*`. +Причин для этого несколько: +
      +
    1. Делегирование генераторов `yield*` -- это встроенный механизм JavaScript. Вместо возвращения значения обратно в `co`, выполнения кода библиотеки... Мы просто используем возможности языка. Это правильнее.
    2. +
    3. Поскольку не происходит лишних вызовов, это быстрее по производительности.
    4. +
    5. И, наконец, пожалуй, самое приятное -- делегирование генераторов не портит стек.
    6. +
    +Проиллюстрируем последнее на примере: + +```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]