初めに
非同期関数やイテレーションの最終パートです。今回は[Symbol.asyncIterator]
、for await...of
、asyncGenerator
についてまとめてみました。
イテレータの基本概念、前の文章にまとめてみました。
非同期関数の基本概念や、動きなどはこちらです。
今回の参考文章はこちらです。
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ループは同調に動きますが、await
でPromise
のように非同期に処理しMicrotask Queue
に移行されて、Call Stack
がクリアになってからMicrotask Queue
から古い順番から実行していくのです。
前の文章ではawait
やMicrotask 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...of
にawait
を入れて本来の働きをもたらしただけだと思います。[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...of
はfor...of
で[Symbol.iterator]
を呼び出してからawait
の機能を加えることがわかった。でもそれって、イテレータは[Symbol.iterator]
で統合してawait
に任せればいいという話になるのでは?[Symbol.AsyncIterator]
の存在が曖昧になって使うタイミングがわからなくなってしまう気がする。
なのでここからさらに別の検証に入りたいと思います。
ネストされた反復可能のオブジェクトならfor...of
かfor await...of
またはほかの処理でどうなるでしょうか。
nested iterable
まずはfor...of
⇒for 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...of
⇒for...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...of
⇒for await...of
のループならfor await...of
⇒for...of
とは同じだったが、コードが複雑になったら中に別の処理を任せるかもしれないのでconsole.log('check')
を入れてみました。そうするとやはり両者動きの違いが見えてきました。
for...of
は同期に動作するので、for...of
⇒for await...of
ならconsole.log('check')
が先に出力して、一度外側に残りのconsole.log('end')
を処理してからまたfor await...of
に戻る。
for await...of
⇒for...of
のほうが逆でした。まずconsole.log('end')
、それからconsole.log('check')
を出力した。
当たり前の結果かもしれませんが、何か必ず先に処理してほしいものがあればfor...of
⇒for await...of
のなかに挟む、重要度がいまいちで、あるいは何かを待ってから同期に処理してほしいならfor await...of
⇒for...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...of
とfor await...of
⇒for...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
の出力順に注目してください。a
⇒c
⇒b
⇒d
でした。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, d
にawaitElement()
を施す。
そして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
か、asyncGenerator
でfor 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