fixes #54, es6

This commit is contained in:
Ilya Kantor 2015-07-12 17:47:54 +03:00
parent 4de55f598b
commit 302a99d1f4
8 changed files with 196 additions and 80 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Before After
Before After

View file

@ -125,49 +125,6 @@
function getCoords(elem) {
var box = elem.getBoundingClientRect();
var body = document.body;
var docEl = document.documentElement;
var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
var clientTop = docEl.clientTop || body.clientTop || 0;
var clientLeft = docEl.clientLeft || body.clientLeft || 0;
var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;
return {
top: Math.round(top),
left: Math.round(left)
};
}
function getPageScroll() {
if (window.pageXOffset != undefined) {
return {
left: pageXOffset,
top: pageYOffset
}
}
var html = document.documentElement;
var body = document.body;
var top = html.scrollTop || body && body.scrollTop || 0;
top -= html.clientTop;
var left = html.scrollLeft || body && body.scrollLeft || 0;
left -= html.clientLeft;
return {
top: top,
left: left
};
}
</script>
</body>

Binary file not shown.