More than 1 year has passed since last update.


Async Iteration について

Async Iteration は 2018年1月の TC39 Meeting で Stage 4 になり1、ES2018 に入ることが期待される新しい ECMAScript の仕様です。


言葉の定義

JavaScript の イテレータ を極める!の記事に倣って言葉の定義をしていきます。

先にそちらの記事読むことをおすすめします。


AsyncIterator とは

AsyncIterator は next メソッドを実行すると Promise<IteratorResult> を返すオブジェクトです。


AsyncIteratorの例

const asyncIterator = {

next() {
return Promise.resolve({ value: 42, done: false });
}
};

{ value: Promise.resolve(42), done: false } を返すわけではないことに注意してください。つまり完了したかどうかも非同期で扱います


AsyncIterable とは

AsyncIterable は Symbol.asyncIterator メソッドを実行すると AsyncIterator を返すオブジェクトです。


AsyncIterableの例

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 を使うことによって値を取り出すことが出来ます。


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* キーワードを使うことが出来ます。


AsyncGeneratorFunctionsを使った例

/**

* 引数のミリ秒だけ待つ 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 になっているので以下のように書くことも出来ます。


AsyncGeneratorFunctionsを使った例2

/**

* 引数のミリ秒だけ待つ 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 を使わない場合でも順番を保証することが出来ます


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 の両方を展開することが出来ます。


yield*の例

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前の発表で、若干内容が古いので気をつけてください。





  1. https://github.com/rwaldron/tc39-notes/blob/master/es8/2018-01/jan-25.md#13iih-async-iteration-for-stage-4 



  2. ここでの Observable は RxJSStage 1 Observable のことを意味しています。 



  3. EventTarget#dispatchEvent を使うことで好きなタイミングでイベントを発火できるように、実は Observable でも工夫次第で Pull Streams を作ることが出来ます。ただし作ってみるとわかりますが Async Generator Functions のように順番保証された Pull Streams を RxJS の Subject で実装するのと Async Generator Functions を Generator Functions から実装するのでは同じくらいの労力がかかるので、素直にネイティブや Babel で Async Generator Functions を使うことをおすすめします。 



  4. キャンセル周りの経緯は https://blog.jxck.io/entries/2017-07-19/aborting-fetch.html に詳しく書かれています。