📚

ES2018 Async Iteration

2021/09/20に公開

ES2018 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 と Observable[2] の違い

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 に詳しく書かれています。 ↩︎

Discussion