Async Iteration について
Async Iteration は 2018年1月の TC39 Meeting で Stage 4 になり1、ES2018 に入ることが期待される新しい ECMAScript の仕様です。
言葉の定義
JavaScript の イテレータ を極める!の記事に倣って言葉の定義をしていきます。
先にそちらの記事読むことをおすすめします。
AsyncIterator とは
AsyncIterator は next
メソッドを実行すると Promise<IteratorResult>
を返すオブジェクトです。
const asyncIterator = {
next() {
return Promise.resolve({ value: 42, done: false });
}
};
{ value: Promise.resolve(42), done: false }
を返すわけではないことに注意してください。つまり完了したかどうかも非同期で扱います。
AsyncIterable とは
AsyncIterable は Symbol.asyncIterator
メソッドを実行すると AsyncIterator を返すオブジェクトです。
const asyncIterator = {
next() {
return Promise.resolve({ value: 42, done: false });
}
};
const asyncIterable = {
[Symbol.asyncIterator]() {
return asyncIterator;
}
};
Sync Iteration との比較表
定義 | 説明 | 持っているメソッド、プロパティ |
---|---|---|
Iterable | Iterator を持つオブジェクト | [Symbol.iterator](): Iterator |
Iterator | IteratorResult を列挙するオブジェクト | next(): IteratorResult |
IteratorResult | 値と完了したかどうかを持つオブジェクト | value, done: boolean |
AsyncIterable | AsyncIterator を持つオブジェクト | [Symbol.asyncIterator](): AsyncIterator |
AsyncIterator | IteratorResult を非同期で列挙するオブジェクト | next(): Promise<IteratorResult> |
AsyncIterable から値を取り出す
Iterable は for-of
で列挙することが出来ましたが、同様に AsyncIterable は Async Functions(と後述する Async Generator Functions)内で for-await-of
を使うことによって値を取り出すことが出来ます。
/**
* 引数のミリ秒だけ待つ Promise を発行する函数
*/
function delay(msec) {
return new Promise(resolve => setTimeout(resolve, msec));
}
// 後述しますがこの AsyncIterable は悪い例です
const asyncIterable = {
[Symbol.asyncIterator]() {
const values = [1, 12, 123];
let index = 0;
return {
// Promise<IteratorResult> を返す函数
async next() {
const value = values[index];
await delay(500 - index++ * 100);
return { value, done: value === undefined };
}
}
}
};
(async () => {
for await (const val of asyncIterable) {
console.log(val); // 500, 400, 300 msec 待って値が取り出される
}
})();
for-await-of
は一つ前の AsyncIterator の next
メソッドから取り出した Promise<IteratorResult>
が fullfilled されるまで次の next
の実行を待つという特徴があります。
ところでこの AsyncIterable の例だと for-await-of
を使わずに以下のように即座に複数の AsyncIterator の next
を呼び出すと順番が前後してしまいます(悪い例)。後述する Async Generator Functions では for-await-of
を使わない場合でもそれが起きないようになっていますのでそちらを使いましょう。
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
asyncIterator.next().then(console.log);
asyncIterator.next().then(console.log);
asyncIterator.next().then(console.log);
asyncIterator.next().then(console.log); // 200ms 後に真っ先にこの console.log が呼ばれる
Async Generator Functions
AsyncIterable を作るのに欠かせないのが Async Generator Functions です。中で await
, yield
, yield*
キーワードを使うことが出来ます。
/**
* 引数のミリ秒だけ待つ Promise を発行する函数
*/
function delay(msec) {
return new Promise(resolve => setTimeout(resolve, msec));
}
const asyncIterable = {
async *[Symbol.asyncIterator]() {
const values = [1, 12, 123];
let index = 0;
for (;;) {
const value = values[index];
await delay(500 - index++ * 100);
if (value === undefined) { break; }
yield value;
}
}
};
Generator Functions と同様に Async Generator Functions の返り値自体が AsyncIterable になっているので以下のように書くことも出来ます。
/**
* 引数のミリ秒だけ待つ Promise を発行する函数
*/
function delay(msec) {
return new Promise(resolve => setTimeout(resolve, msec));
}
async function* asyncGenerator() {
const values = [1, 12, 123];
let index = 0;
for (;;) {
const value = values[index];
await delay(500 - index++ * 100);
if (value === undefined) { break; }
yield value;
}
}
const asyncIterable = asyncGenerator();
// @@asyncIterator メソッドを実行すると自身を返す
// asyncIterable === asyncIterable[Symbol.asyncIterator]();
「AsyncIterable から値を取り出す」のところでも触れましたが、Async Generator Functions を使うと for-await-of
を使わない場合でも順番を保証することが出来ます。
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
asyncIterator.next().then(console.log); // 500ms 後に取り出せる
asyncIterator.next().then(console.log); // 500 + 400ms 後に取り出せる
asyncIterator.next().then(console.log); // 500 + 400 + 300ms 後に取り出せる
asyncIterator.next().then(console.log); // 500 + 400 + 300 + 200ms 後に完了する
yield*
について補足
Async Generator Functions 内の yield*
では Iterable と AsyncIterable の両方を展開することが出来ます。
async function* asyncGenerator() {
yield 1;
yield 2;
yield 3;
}
async function* asyncGenerator2() {
// AsyncIterable を展開する
yield* asyncGenerator();
// Iterable を展開する
yield* [4, 5, 6];
}
(async () => {
for await (const val of asyncGenerator2()) {
console.log(val); // 1, 2, 3, 4, 5, 6
}
})();
Async Iteration と Observable2 の違い
Async Iteration は Pull Streams です。AsyncIterator の next
を呼ぶことで次の値の取得を要請する必要があります。要請しないと永遠に次の値を得ることが出来ません。
一方で Observable は Push Streams です3。DOM API の EventTarget#addEventListener
のように Observable#subscribe
すると勝手に次の値が流れてきます。
参考動画
The future of ES6 (Jafar Husain) - Full Stack Fest 2016 14m36s~
TC39 member である Jafar Husain さんによる Async Iteration と Observable を紹介する動画です。 CancelToken
が DOM API の AbortController
になる4前の発表で、若干内容が古いので気をつけてください。
-
https://github.com/rwaldron/tc39-notes/blob/master/es8/2018-01/jan-25.md#13iih-async-iteration-for-stage-4 ↩
-
ここでの Observable は RxJS や Stage 1 Observable のことを意味しています。 ↩
-
EventTarget#dispatchEvent
を使うことで好きなタイミングでイベントを発火できるように、実は Observable でも工夫次第で Pull Streams を作ることが出来ます。ただし作ってみるとわかりますが Async Generator Functions のように順番保証された Pull Streams を RxJS の Subject で実装するのと Async Generator Functions を Generator Functions から実装するのでは同じくらいの労力がかかるので、素直にネイティブや Babel で Async Generator Functions を使うことをおすすめします。 ↩ -
キャンセル周りの経緯は https://blog.jxck.io/entries/2017-07-19/aborting-fetch.html に詳しく書かれています。 ↩