LoginSignup
3
1

More than 1 year has passed since last update.

JavaScriptのasyncIteratorについて

Posted at

初めに

非同期関数やイテレーションの最終パートです。今回は[Symbol.asyncIterator]for await...ofasyncGeneratorについてまとめてみました。

イテレータの基本概念、前の文章にまとめてみました。

非同期関数の基本概念や、動きなどはこちらです。

今回の参考文章はこちらです。

Asynchronous iterator - [Symbol.asyncIterator]

組み込みの仕方や使い方では[Symbol.iterator]とほぼ同じです。[Symbol.asyncIterator]プロパティだけを持つ場合はfor await...ofでイテレータしなければならない。

Syntax

// set [Symbol.asyncIterator] property
iterableData[Symbol.asyncIterator] = async function* something() {...}
iterableData[Symbol.asyncIterator] = async function* () {...}
// or inside of iterable Data
async*[Symbol.asyncIterator]() {...}

// call [Symbol.asyncIterator]
async function something(iterableData) {
  for await (let item of iterableData) {
  ...
  }
}

非同期ジェネレータを動かすには必ずイテレータできるデータ構造か、データを生成する[Symbol.asyncIterator]/[Symbol.Iterator]プロパティを持たねばなりません。そしてプロパティをfor await...of非同期に呼び出すにはasync functionが必要です。

// @@asyncIterator
let arr = [1, 2, 3];
arr[Symbol.asyncIterator] = async function* () {
  for (let i = 0; i < this.length; i++) {
    yield this[i];
  }
};
async function useForAwait(arr) {
  console.log('start for await');
  // for await...of call @@asyncIterator => [Symbol.asyncIterator]
  for await (let item of arr) {
    console.log(item)
  }
  console.log('end for await');
}
console.log('start');
useForAwait(arr);
console.log('end');
// start
// start for await
// end
// 1
// 2
// 3
// end for await

forループは同調に動きますが、awaitPromiseのように非同期に処理しMicrotask Queueに移行されて、Call StackがクリアになってからMicrotask Queueから古い順番から実行していくのです。
前の文章ではawaitMicrotask Queueの動きなどまとめてみたので、ご参考になれば嬉しいです。

call [Symbol.asyncIterator] vs. [Symbol.iterator]

ここから検証に入りたいと思います。
普通の関数にfor...of、非同期関数にfor...of、非同期関数にfor await...ofでは、一体どちらのプロパティを呼び出すのか、そして動きではどこか違うかを知りたいです。

// for...of in function
function controlledFn(iterable) {
  console.log('for...of in controlledFn start');
  for (let item of iterable) {
    console.log(item);
  }
  console.log('for...of in controlledFn end');
}
// for...of in async function
async function useIterator(iterable) {
  console.log('for...of in useIterator start');
  for (let item of iterable) {
    console.log(item);
  }
  console.log('for...of in useIterator end');
}
// await for...of in async function
async function useAsyncIterator(iterable) {
  console.log('await for...of in async function start');
  for await (let item of iterable) {
    console.log(item)
  }
  console.log('await for...of in async function end');
}

まずは普通に[Symbol.iterator]だけ持っている配列を用いて、別々結果を見ていきたいと思います。

// iterable with [Symbol.iterator] property
let arr = [1, 2, 3];
// result
console.log('test start');
controlledFn(arr);
console.log('test end');
// test start
// for...of in controlledFn start
// 1
// 2
// 3
// for...of in controlledFn end
// test end
//
console.log('test start');
useIterator(arr);
console.log('test end');
// test start
// for...of in useIterator start
// 1
// 2
// 3
// for...of in useIterator end
// test end
//
console.log('test start');
useAsyncIterator(arr);
console.log('test end');
// test start
// await for...of in async function start
// test end
// 1
// 2
// 3
// await for...of in async function end

[Symbol.iterator]だけ持っている場合は、for await...ofもちゃんと作用している、というより普通のfor...ofawaitを入れて本来の働きをもたらしただけだと思います。[Symbol.iterator]持ちのiterableであればfor await...ofも利用できます。

[Symbol.asyncIterator]プロパティを追加したらどうなるでしょう。

// iterable add [Symbol.AsyncIterator] property
arr[Symbol.asyncIterator] = async function* () {
  for (let i = 0; i < this.length; i++) {
    yield this[i];
  }
};
// result
console.log('test start');
controlledFn(arr);
console.log('test end');
// test start
// for...of in controlledFn start
// 1
// 2
// 3
// for...of in controlledFn end
// test end
//
console.log('test start');
useIterator(arr);
console.log('test end');
// test start
// for...of in useIterator start
// 1
// 2
// 3
// for...of in useIterator end
// test end
//
console.log('test start');
useAsyncIterator(arr);
console.log('test end');
// test start
// await for...of in async function start
// test end
// 1
// 2
// 3
// await for...of in async function end

結果は変わらなかった、と思いながら[Symbol.iterator]を消したらどうなるでしょう。

// only [Symbol.AsyncIterator]
arr[Symbol.iterator] = null;
arr[Symbol.asyncIterator] = async function* () {
  for (let i = 0; i < this.length; i++) {
    yield this[i];
  }
};
// controlledFn(arr); // TypeError: iterable is not iterable
// useIterator(arr); // TypeError: iterable is not iterable
console.log('test start');
useAsyncIterator(arr);
console.log('test end');
// test start
// await for...of in async function start
// test start
// 1
// 2
// 3
// await for...of in async function end
/**
 * [Symbol.iterator] => for...of/for await...of
 * [Symbol.iterator] & [Symbol.AsyncIterator] => for...of/for await...of
 * only [Symbol.AsyncIterator] => for await...of
*/

[Symbol.AsyncIterator]プロパティだけ残された場合はfor await...ofでイテレータできるが、for...ofではできません。
これでfor await...ofの使用では[Symbol.AsyncIterator]プロパティを呼び出すか、for...of[Symbol.iterator]を呼び出したうえでawaitの機能が働くとわかりました。

for await...of

前の検証から[Symbol.AsyncIterator]プロパティが持たなくても、for await...offor...of[Symbol.iterator]を呼び出してからawaitの機能を加えることがわかった。でもそれって、イテレータは[Symbol.iterator]で統合してawaitに任せればいいという話になるのでは?[Symbol.AsyncIterator]の存在が曖昧になって使うタイミングがわからなくなってしまう気がする。

なのでここからさらに別の検証に入りたいと思います。
ネストされた反復可能のオブジェクトならfor...offor await...ofまたはほかの処理でどうなるでしょうか。

nested iterable

まずはfor...offor await...of

// nested iterable
let arr = [['a', 'b'], ['c', 'd']];
// for...of => for await...of
async function test(arr) {
  console.log('test start');
  for (let item of arr) {
    console.log('check')
    for await (let element of item) {
      console.log(element)
    }
  }
  console.log('test end');
}
console.log('start');
test(arr);
console.log('end');
// start
// test start
// check // for...of is synchronous
// end // for await...of is asynchronous
// a
// b
// check
// c
// d
// test end

次にfor await...offor...of

// for await...of => for...of
async function test(arr) {
  console.log('test start');
  for await (let item of arr) {
    console.log('check');
    for (let element of item) {
      console.log(element);
    }
  }
  console.log('test end');
}
console.log('start');
test(arr);
console.log('end');
// start
// test start
// end // for await...of is asynchronous
// check
// a
// b
// check
// c
// d
// test end

単純なfor...offor await...ofのループならfor await...offor...ofとは同じだったが、コードが複雑になったら中に別の処理を任せるかもしれないのでconsole.log('check')を入れてみました。そうするとやはり両者動きの違いが見えてきました。

for...ofは同期に動作するので、for...offor await...ofならconsole.log('check')が先に出力して、一度外側に残りのconsole.log('end')を処理してからまたfor await...ofに戻る。

for await...offor...ofのほうが逆でした。まずconsole.log('end')、それからconsole.log('check')を出力した。

当たり前の結果かもしれませんが、何か必ず先に処理してほしいものがあればfor...offor await...ofのなかに挟む、重要度がいまいちで、あるいは何かを待ってから同期に処理してほしいならfor await...offor...ofに。

配列のイテレータと出力なら一番簡単なのはmap()、ではawait map()ならどうなるでしょうか。一緒に見ていきたいと思います。

async function test1(arr) {
  console.log('test start');
  // await synchronous result
  await arr.map((item) => {
    for (let element of item) {
      console.log(element);
    }
  })
  console.log('test end');
}
console.log('start');
test1(arr);
console.log('end');
// start
// test start
// a
// b
// c
// d
// end
// test end

awaitの後ろに同期に動作する関数であれば、同期関数が先に実行してからawaitが機能します。なぜそうなるのか。awaitはジェネレータ関数のyieldの概念からのキーワードなので、実際にawaitの動きは、さきに後ろの式(expression)が処理してから、前のawaitへ解決されたプロミス(Settled Promise)の検定してそのまま通過するか、(式の結果がPromiseではなかった場合)暗黙にPromise.resolve()呼び出しして処理する。

for await (let/const variable of iterable) {}は、forループが形成する前に、await(let/const variable of iterable) {}は解決されたプロミスではないからMicrotaskに移行して非同期処理し、メインスレッドに残るconsole.log('end')Call Stackへ処理し終わり、awaitに解決されたプロミスの結果がforループに、そしてaync functionに残りの処理を終わらせるのです。

nested arrayLike

[Symbol.iterator]を組み込まれてないけれどイテレータできる構造を思っている配列風オブジェクトならどうなるのでしょうか。

一度Array.from()にしてから[Symbol.iterator]を持たせるのが一つだと思います。これでは先の例の配列for...of => for await...offor await...offor...ofとは変わりはありません。

// ArrayLike Object with calling `[Symbol.iterator]`
let arrayLikeObj = {
  0: ['a', 'b'],
  1: ['c', 'd'],
  length: 2
};
// for...of => for await...of
async function useArrayLike(arrayLike) {
  console.log('test start');
  // asynchronous
  for (let item of Array.from(arrayLikeObj)) {
    console.log('check');
    for await (let element of item) {
      console.log(element);
    }
  }
  console.log('test end');
}
console.log('start');
useArrayLike(arrayLikeObj);
console.log('end');
// start
// test start
// check
// end
// a
// b
// check
// c
// d
// test end
//
// for await...of => for...of
async function useArrayLike(arrayLike) {
  console.log('test start');
  // asynchronous
  for await (let item of Array.from(arrayLikeObj)) {
    console.log('check');
    for (let element of item) {
      console.log(element);
    }
  }
  console.log('test end');
}
console.log('start');
useArrayLike(arrayLikeObj);
console.log('end');
// start
// test start
// end
// check
// a
// b
// check
// c
// d
// test end

もし、Array.from()経由してmap()for await...ofで処理したらどうなるでしょう。

let arrayLikeObj = {
  0: ['a', 'b'],
  1: ['c', 'd'],
  length: 2
};
async function awaitElement(item) {
  for await (let element of item) {
    console.log(element);
  }
}
// async function
async function asyncFn(arrayLikeObj) {
  console.log('test start');
  // Array.from() => synchronous
  // map() => parallel processing
  // for await...of => asynchronous
  Array.from(arrayLikeObj, (item) => awaitElement(item))
  console.log('test end');
}
console.log('start');
asyncFn(arrayLikeObj);
console.log('end');
// start
// test start
// test end
// end
// a // item = [a, b] => await element: a
// c // item = [c, d] => await element: c
// b
// d
// note: map() makes item in arrayLike be processed in parallel, 
// and await makes element processed asynchronously

Array.from()は同期関数だけど、出力は第二引数のmap()に非同期関数awaitElement()に任したので、出力する前に外側のconsole.log('test end')、それからもっと外側にconsole.log('end')、メインスレッドに同期処理がない(=Call Stackクリア)のを確認してから、Microtask Queueから順番にCall Stackに入れて処理していく。

ここでまずarrayLikeObjの出力順に注目してください。acbdでした。map()forループより高速処理できるのは、forみたいに一回に一つの要素に作用するのではなく、一回に全要素に作用するのです。つまり、

// Microtask (1)
  for await (let element of [a, b]) {
    console.log(element);
  }
// console.log(a) => // Microtask Queue (1)

// Microtask (2)
  for await (let element of [c, d]) {
    console.log(element);
  }
// console.log(c) => // Microtask Queue (2)

Microtaskでは非同期処理の行いで、先に処理されたもの(Settled Promise)がMicrotask Queueに移行するのでこのような結果になりました。先にデモした二重forループ一番の違いです。

外側の関数が普通の関数でも使える。結果も同じです。

// normal function
function normalFn(arrayLikeObj) {
  console.log('test start');
  // synchronous => asynchronous
  Array.from(arrayLikeObj, (item) => awaitElement(item));
  console.log('test end');
}
console.log('start');
normalFn(arrayLikeObj);
console.log('end');
// start
// test start
// test end
// end
// a // item = [a, b] => await element: a
// c // item = [c, d] => await element: c
// b
// d

ArrayLike Object with calling [Symbol.asyncIterator]

では、[Symbol.asyncIterator]プロパティだけ使ったらまた、どうなるでしょうか。

// ArrayLike Object with calling `[Symbol.asyncIterator]`
let arrayLikeObj = {
  0: ['a', 'b'],
  1: ['c', 'd'],
  length: 2,
  async*[Symbol.asyncIterator]() {
    for (let i = 0; i < this.length; i++) {
      yield this[i]
    }
  }
};
async function useArrayLike(arrayLike) {
  console.log('test start');
  // asynchronous
  // arrayLike didn't have [Symbol.iterator] property
  // for...of => for await...of => TypeError: arrayLikeObj is not iterable
  for await (let item of arrayLike) {
    for (let element of item) {
      console.log(element);
    }
  }
  console.log('test end');
}
console.log('start');
useArrayLike(arrayLikeObj);
console.log('end');
// start
// test start
// end
// a
// b
// c
// d
// test end

[Symbol.asyncIterator]だけ設置したので、arrayLikeObjオブジェクトには普通のイテレータメソッドが使えません。for await...ofで要素を走査することで、要素の性質によってイテレータメソッドが使えるかもしれませんが、([Symbol.asyncIterator]を呼び出すため)for await...ofを使わなければならないことで、先ほどの例みたいにArray.from(arrayLikeObj, (item) => awaitElement(item))配列ごとに並列処理をすることができません。(厳密に言うとArray.from()arrayLikeObjを配列にしてからmap()する。配列にする段階でArray.prototype[Symbol.iterator]を使えるようになって、結果として[[a, b], [c, d]]でした。)

for await...ofでイテレーションを行い、map()awaitElement()に要素を入れて走査すると、また別の結果が出ました。

async function awaitElement(item) {
  console.log('check');
  for await (let element of item) {
    console.log(element);
  }
}
async function useArrayLike(arrayLike) {
  console.log('test start');
  // asynchronous
  for await (let item of arrayLike) {
    item.map((element) => awaitElement(element));
  }
  console.log('test end');
}
console.log('start');
useArrayLike(arrayLikeObj);
console.log('end');
// start
// test start
// end // for await (let item of arrayLike)
// check
// check
// a
// b
// check
// check
// test end // for await (let element of item)
// c
// d

毎回のfor await...ofではmap()a, b、そしてc, dawaitElement()を施す。
そしてawaitにあってしまうと、一度外側にメインに残る処理を終わらせてから出力する。

最後に[Symbol.asyncIterator]for await...ofについてもうちょっとまとめたいと思います。正直にいうと、[Symbol.asyncIterator]がプロパティとして使う理由があまりないかもしれません。[Symbol.asyncIterator][Symbol.iterator]、非同期の実現は呼び出し側のawaitにあるので、プロパティとしてわざわざ[Symbol.asyncIterator]を設置する必要がないと感じています。

awaitが、for await...ofの並行処理(concurrent)とmap()の非同期関数による並列処理(parallel)の動きがわかったのは一番の収穫だと思います。

AsyncGenerator

AsyncGenerator[Symbol.asyncIterator]に組み込んだメソッドですが、もとといえば同期処理から非同期へ変形したジェネレータのことです。

参考文章から取った例です。

// synchronous generator
function* syncGenerator(iterable, fn) {
  // call iterable object's [Symbol.iterator] property
  const iterator = iterable[Symbol.iterator]();
  while (true) {
    const { value, done } = iterator.next();
    if (done) break;
    // if generator didn't finsh yet, ouput fn(value)
    yield fn(value);
  }
}

// asynchronous generator
async function* asyncGenerator(iterable, fn) {
  const iterator = iterable[Symbol.iterator]();
  while (true) {
    // await => asynchronous
    const { value, done } = await iterator.next();
    if (done) break;
    yield fn(value);
  }
}

async functionの内部にawaitを使うことによって非同期を実現するのです。

async functionか、asyncGeneratorfor await...ofを通して呼び出すことができる。

// use async function to call asyncGenerator
(async function () {
  for await (let line of readLines(filePath)) {
    console.log(line);
  }
})();
// or another asyncGenerator to call asyncGenerator
async function* prefixLines(asyncItarable) {
  for await (let line of asyncItarable) {
    yield '> ' + line;
  }
}

Syntax

AsyncGenerator(async function* expression)

AsyncGeneratorFunction((async function* () {}).constructor)

下のように(async function* () {}).constructorコンストラクタを使ってシンプルな非同期ジェネレータを作ることができる。

// (async function* () { }).constructor
// num
let num = 10;
const asyncGeneratorFn = (async function* () { }).constructor;
let createAsyncGenerator = new asyncGeneratorFn(
  'a',
  'num',
  'yield a * num'
);
let asyncGen = createAsyncGenerator(5, num);
console.log('start');
asyncGen
  .next()
  .then((result) => console.log(result));
console.log('end');
start
end
{ value: 50, done: false }

// arr
let arr = [1, 2, 3];
const asyncGeneratorFn = (async function* () { }).constructor;
let createAsyncGenerator = new asyncGeneratorFn(
  'a',
  'arr',
  'yield arr.map((item) => item * a)'
);
let asyncGen = createAsyncGenerator(5, arr);
console.log('start');
asyncGen
  .next()
  .then((result) => console.log(result));
console.log('end');
// start
// end
// { value: [ 5, 10, 15 ], done: false }

async function*createAsyncGenerator(5, arr)を書き直したら下のようになります。

// async function*
async function* foo(a, arr) {
  yield await Promise.resolve(arr.map((item) => item * a));
}
foo(5, [1, 2, 3])
  .next()
  .then((result) => console.log(result));
// { value: [ 5, 10, 15 ], done: false }

yield*

同期ジェネレータのyield*と使い方が同じです。

// yield in async generator vs. await in async generator
async function* test1() {
  yield 'a';
  yield 'b';
  return 2;
}
async function* test2() {
  let result = yield* test1();
  console.log(result);
  // let awaitAsyncGeneratorFn = await test1(); // meaningless
  // console.log(awaitAsyncGeneratorFn); // Object [AsyncGenerator] {}
}

for await (let item of test2()) {
  console.log(item);
}
// a
// b
// 2 // result
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1