es6
This commit is contained in:
parent
a96409db4b
commit
4d9c85d7d9
7 changed files with 240 additions and 80 deletions
|
@ -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]
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "iliakan",
|
||||
"isAdmin": true
|
||||
}
|
|
@ -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 |
Binary file not shown.
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 63 KiB |
Loading…
Add table
Add a link
Reference in a new issue