This commit is contained in:
Ilya Kantor 2015-07-10 18:12:52 +03:00
parent a96409db4b
commit 4d9c85d7d9
7 changed files with 240 additions and 80 deletions

View file

@ -262,7 +262,7 @@ httpGet("/article/promise/user.json")
[smart header="Метод `fetch`"]
Заметим, что ряд современных браузеров уже поддерживает [fetch](https://fetch.spec.whatwg.org) -- новый встроенный метод для AJAX-запросов, призванный заменить XMLHttpRequest. Он, конечно, гораздо мощнее, чем `httpGet`. И -- да, этот метод использует промисы. Полифилл для него доступен на [](https://github.com/github/fetch).
Заметим, что ряд современных браузеров уже поддерживает [fetch](/fetch) -- новый встроенный метод для AJAX-запросов, призванный заменить XMLHttpRequest. Он, конечно, гораздо мощнее, чем `httpGet`. И -- да, этот метод использует промисы. Полифилл для него доступен на [](https://github.com/github/fetch).
[/smart]

View file

@ -1,31 +0,0 @@
# AJAX-запросы: fetch
Метод [fetch](https://fetch.spec.whatwg.org/) -- это `XMLHttpRequest` нового поколения. Он предоставляет улучшенный интерфейс для осуществления запросов к серверу: как по части возможностей и контроля над происходящим, так и по синтаксису, так как построен на промисах.
Поддержка в браузерах пока не очень распространена, но есть [полифилл](https://github.com/github/fetch) и не один.
## Использование
Синтаксис:
Начнём сразу с примера:
```js
//+ run
'use strict';
fetch('/article/fetch/user.json')
.then( response => {
alert(response.headers.get('Content-Type')); // text/html; charset=utf-8
return response.json();
})
.then( user => alert(user.name) ) // iliakan
.catch( alert );
```
Поток такой:
<ul>
<li>

View file

@ -1,4 +0,0 @@
{
"name": "iliakan",
"isAdmin": true
}

View file

@ -76,7 +76,7 @@ alert(JSON.stringify(three)); // {value: 3, *!*done: true*/!*}
<img src="generateSequence-4.png">
Функция завершена. Внешний код должен увидить это из свойства `done:true` и прекратить вызовы. Впрочем, если новые вызовы `generator.next()` и будут, то они не вызовут ошибки, но будут возвращать один и тот же объект: `{done: true}`.
Функция завершена. Внешний код увидит это из свойства `done:true` и обработает `value:3`, как окончательный результат. Новые вызовы `generator.next()` больше не имеют смысла. Впрочем, если они и будут, то не вызовут ошибки, но будут возвращать один и тот же объект: `{done: true}`.
"Открутить назад" завершившийся генератор нельзя, но можно создать новый ещё одним вызовом `generateSequence()` и выполнить его.
@ -169,7 +169,7 @@ alert(sequence); // 2, 3, 4, 5
Естественно, раз в нашем распоряжении есть готовый генератор `generateSequence`, то хорошо бы его использовать.
Конечно, можно внутри `generateAlphaNum` запустить несколько `generateSequence`, объединить результаты и вернуть, но композиция -- это кое-что получше.
Конечно, можно внутри `generateAlphaNum` запустить несколько раз `generateSequence`, объединить результаты и вернуть. Так мы бы сделали с обычными функциями. Но композиция -- это кое-что получше.
Она выглядит так:
@ -243,6 +243,10 @@ for(let code of generateAlphaNum()) {
alert(str); // 0..9A..Za..z
```
Код выше по поведению полностью идентичен варианту с `yield*`.
Композиция -- это естественное встраивание одного генератора в поток другого. В частности, если поток данных из вложенного генератора оказался бесконечным (или ожидает какого-либо условия для завершения), то с точки зрения композиции это вполне нормально.
## yield -- дорога в обе стороны
До этого генераторы наиболее напоминали "итераторы на стероидах". Но, как мы сейчас увидим, это не так, есть фундаментальное различие, генераторы гораздо мощнее и гибче.
@ -253,8 +257,8 @@ alert(str); // 0..9A..Za..z
<ul>
<li>Возвращает `value` во внешний код, приостанавливая выполнение генератора.</li>
<li>Внешний код может обработать значение, и затем вызвать `next` с аргументом: `generator.next(result)`.</li>
<li>Генератор продолжит выполнение, этот аргумент будет записан в `result`.</li>
<li>Внешний код может обработать значение, и затем вызвать `next` с аргументом: `generator.next(arg)`.</li>
<li>Генератор продолжит выполнение, аргумент `next` будет возвращён как результат `yield` (и записан в `result`).</li>
</ul>
Продемонстрируем это на примере:
@ -274,23 +278,27 @@ function* gen() {
let generator = gen();
let question = generator.next().value;
// { value: "Сколько будет 2 + 2?", done: false }
let question = generator.next().value;
// "Сколько будет 2 + 2?"
setTimeout(() => generator.next(4), 2000);
```
На рисунке ниже прямоугольником изображён генератор, а вокруг него -- "внешний код", который с ним взаимодействует:
<img src="genYield2.png">
Выше проиллюстрировано то, что происходит в генераторе:
На этой иллюстрации показано то, что происходит в генераторе:
<ol>
<li>Первый `.next()` всегда без аргумента, он начинает выполнение и возвращает результат первого `yield`.</li>
<li>Результат `yield` переходит во внешний код (в `question`), он может выполнять любые асинхронные задачи.</li>
<li>Когда асинхронные задачи готовы, внешний код вызывает `.next(result)`, при этом выполнение продолжается, а `result` выходит из присваивания как результат `yield`.</li>
<li>Первый вызов `generator.next()` -- всегда без аргумента, он начинает выполнение и возвращает результат первого `yield` ("Сколько будет 2+2?"). На этой точке генератор приостанавливает выполнение.</li>
<li>Результат `yield` переходит во внешний код (в `question`). Внешний код может выполнять любые асинхронные задачи, генератор стоит "на паузе".</li>
<li>Когда асинхронные задачи готовы, внешний код вызывает `generator.next(4)` с аргументом. Выполнение генератора возобновляется, а `4` выходит из присваивания как результат `let result = yield ...`.</li>
</ol>
Посмотрим вариант побольше:
В примере выше -- только два `next`.
Возможно, происходящее будет проще понять, если их больше:
```js
//+ run
@ -315,6 +323,8 @@ alert( generator.next(4).value ); // "...3*3?"
alert( generator.next(9).done ); // true
```
Взаимодействие с внешним кодом:
<img src="genYield2-2.png">
<ol>
@ -336,11 +346,13 @@ alert( generator.next(9).done ); // true
Как мы видели в примерах выше, внешний код может вернуть генератору в качестве результата `yield` любое значение.
Можно "вернуть" не только результат, но и ошибку!
...Но "вернуть" можно не только результат, но и ошибку!
Это делает вызов `generator.throw(err)`.
Как и любая операция, `yield` может завершиться со значением, либо сгенерировать исключение. И то и то -- разновидность результата.
Например:
Для того, чтобы передать в `yield` ошибку, используется вызов `generator.throw(err)`. При этом на строке с `yield` возникает исключение.
Например, в коде ниже обращение к внешнему коду `yield "Сколько будет 2 + 2"` завершится с ошибкой:
```js
@ -348,13 +360,13 @@ alert( generator.next(9).done ); // true
'use strict';
function* gen() {
// Передать вопрос во внешний код и подождать ответа
try {
// в этой строке возникнет ошибка
let result = yield "Сколько будет 2 + 2?";
alert(result);
alert("не сработает, так как ошибка выпадет в try..catch");
} catch(e) {
alert(e);
alert(e); // выведет ошибку
}
}
@ -363,12 +375,38 @@ let generator = gen();
let question = generator.next().value;
*!*
generator.throw(new Error("ответ не найден в моей базе данных"));
generator.throw(new Error("ответ не найден в моей базе данных")); // (*)
*/!*
```
"Вброшенная" извне ошибка обрабатывается как обычно. Она возникает в строке с `yield` и может быть перехвачена `try..catch`, как продемонстрировано выше.
"Вброшенная" в строке `(*)` ошибка возникает в строке с `yield` и обрабатывается как обычно. В примере выше она перехватывается `try..catch` и выводится.
Если её не перехватить, то она "выпадет" из генератора. По стеку ближайший вызов, который инициировал выполнение -- это строка с `.throw`. Можно перехватить её там, как и продемонстрировано в примере ниже:
```js
//+ run
'use strict';
function* gen() {
// В этой строке возникнет ошибка
let result = yield "Сколько будет 2 + 2?";
}
let generator = gen();
let question = generator.next().value;
*!*
try {
generator.throw(new Error("ответ не найден в моей базе данных"));
} catch(e) {
alert(e); // выведет ошибку
}
*/!*
```
Если же ошибка и там не перехвачена, то дальше -- как обычно, либо `try..catch` снаружи, либо она "повалит" скрипт.
# Плоский асинхронный код
@ -377,16 +415,193 @@ generator.throw(new Error("ответ не найден в моей базе д
Общий принцип такой:
<ul>
<li>Генератор `yield'ит` не просто значения, а промисы.</li>
<li>Есть специальная "функция-чернорабочий" `execute(generator)` которая запускает генератор, последовательными вызовами `next` получает из него эти промисы, ждёт их выполнения и возвращает в генератор результат, пока генератор не завершится.</li>
<li>Последнее значение генератора `execute` возвращает через промис, коллбэк или просто использует, как в примере ниже.</li>
<li>Есть специальная "функция-чернорабочий" `execute(generator)` которая запускает генератор, последовательными вызовами `next` получает из него промисы -- один за другим, и, когда очередной промис выполнится, возвращает его результат в генератор следующим `next`.</li>
<li>Последнее значение генератора (`done:true`) `execute` уже обрабатывает как окончательный результат -- например, возвращает через промис куда-то ещё, во внешний код или просто использует, как в примере ниже.</li>
</ul>
Получается примерно так:
Напишем такой код для получения аватара пользователя с github и его вывода, аналогичный рассмотренному в статье про [промисы](/promise).
Для AJAX-запросов будем использовать метод [fetch](/fetch), он как раз возвращает промисы.
```js
//+ run
'use strict';
// генератор для получения и показа аватара
function* showUserAvatar() {
let userFetch = yield fetch('/article/generator/user.json');
let userInfo = yield userFetch.json();
let githubFetch = yield fetch(`https://api.github.com/users/${userInfo.name}`);
let githubUserInfo = yield githubFetch.json();
let img = new Image();
img.src = githubUserInfo.avatar_url;
img.className = "promise-avatar-example";
document.body.appendChild(img);
yield new Promise(resolve => setTimeout(resolve, 3000));
img.remove();
return img.src;
}
// вспомогательная функция-чернорабочий для выполнения промисов
function execute(generator, yieldValue) {
let next = generator.next(yieldValue);
if (!next.done) {
next.value.then(
result => execute(generator, result),
err => generator.throw(err)
);
} else {
// return из генератора (обработаем результат)
// обычно здесь вызов callback или что-то в этом духе
alert(next.value);
}
}
*!*
execute( showUserAvatar() );
*/!*
```
Функция `execute` в примере выше -- универсальная, она может работать с любым генератором, который `yield'ит` промисы.
Вместе с тем, это -- всего лишь набросок, чтобы было понятно, как такая функция в принципе работает. Есть уже готовые реализации, обладающие большим количеством возможностей.
Одна из самых известных -- это библиотека [co](https://github.com/tj/co).
Её нужно подключить (через `npm` или https://cdnjs.cloudflare.com/ajax/libs/co/4.1.0/index.min.js), после чего запускаем её с функцией-генератором, вот так:
```js
//+ run
'use strict';
co(function*() {
let result = yield new Promise(
resolve => setTimeout(resolve, 1000, 1)
);
alert(result); // 1
})
```
Библиотека `co`, как и `executor` выше, выполняет генератор, получает из него промисы и возвращает обратно их результаты. В примере выше `function*()` делает `yield` промиса с `setTimeout`, который через секунду возвращает `1`.
Вызов `co(…)` возвращает промис с результатом генератора. Если в примере выше `function*()` что-то возвратит, то это можно будет получить через `.then` в результате `co`:
```js
//+ run
'use strict';
co(function*() {
let result = yield new Promise(
resolve => setTimeout(resolve, 1000, 1)
);
*!*
return result; // return 1
}).then(alert); // 1
*/!*
```
[warn header="Обязательно нужен `catch`"]
Частая ошибка начинающих -- вообще забывать про обработку результата `co`. Даже если результата нет, ошибки нужно обработать через `catch`, иначе они "подвиснут" в промисе.
Такой код ничего не выведет:
```js
//+ run
co(function*() {
throw new Error("Sorry that happened");
})
```
Программист даже не узнает об ошибке. Особенно обидно, когда это опечатка или другая программная ошибка, которую обязательно нужно поправить.
Правильный вариант:
```js
//+ run
co(function*() {
throw new Error("Sorry that happened");
}).catch(alert); // обработать ошибку как-либо
```
Большинство примеров этого `catch` не содержат, но это лишь потому, что в примерах ошибок нет. А в реальном коде обязательно нужен `catch`.
[/warn]
**Библиотека `co` может работать не только с генераторами.**
Есть несколько видов значений, которые умеет обрабатывать `co`:
<ul>
<li>Объект-генератор -- выполняется описанным выше способом.</li>
<li>Функция-генератор `function*()` -- `co` её выполнит, затем выполнит полученный генератор.</li>
<li>Промис -- `co` вернёт его (на самом деле новый промис, но он вернёт этот).</li>
<li>Функция с единственным аргументом вида `function(callback)` -- библиотека `co` её запустит со своей функцией-`callback` и будет ожидать, что при ошибке она вызовет `callback(err)`, а при успешном выполнении -- `callback(null, result)`, то есть в первом аргументе -- будет ошибка (если есть), а втором -- результат (если нет ошибки). После чего результат будет передан в генератор.</li>
<li>Массив или объект из вышеперечисленного. При этом все задачи будут выполнены параллельно, и результат, в той же структуре, будет выдан наружу.</li>
</ul>
В примере ниже происходит `yield` всех этих видов значений:
```js
//+ run
'use strict';
Object.defineProperty(window, 'result', {
// присвоение result=… будет выводить значение
set: value => alert(JSON.stringify(value))
});
co(function*() {
result = yield function*() { // генератор
return 1;
}();
result = yield function*() { // функция-генератор
return 2;
};
result = yield Promise.resolve(3); // промис
result = yield function(callback) { // function(callback)
callback(null, 4);
};
result = yield { // две задачи выполнит параллельно, как Promise.all
one: Promise.resolve(1),
two: function*() { return 2; }
};
result = yield [ // две задачи выполнит параллельно, как Promise.all
Promise.resolve(1),
function*() { return 2 }
];
});
```
**Библиотеку `co` можно использовать один раз в самом внешнем вызове.**
Библиотека `co` обрабатывает результаты рекурсивно. То есть, если в результате `yield` получается генератор, то ...
TODO
function* showUserAvatar() {
let userFetch = yield fetch('/article/generator/user.json');
@ -407,29 +622,8 @@ function* showUserAvatar() {
return img.src;
}
function execute(generator, yieldValue) {
let next = generator.next(yieldValue);
if (!next.done) {
next.value.then(
result => execute(generator, result),
err => generator.throw(err)
);
} else {
// return из генератора (его результат)
alert(next.value);
}
}
*!*
execute( showUserAvatar() );
*/!*
```
co(showUserAvatar)
[head]
<style>
@ -440,6 +634,7 @@ execute( showUserAvatar() );
top: 0;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/co/4.1.0/index.min.js"></script>
[/head]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Before After
Before After