繰り返しにもいろいろあるよ。
for
,while
→ for文を仕様からじっくり見てみる。あとwhileとか。(配列とかおれおれAdvent Calendar2018 – 13日目)for-in
→ for-inの仕様も見てみたよ。使う機会なさそうだけど。(配列とかおれおれAdvent Calendar2018 – 14日目)for-of
→ for-ofで配列も普通のオブジェクトも反復しよう。(配列とかおれおれAdvent Calendar2018 – 15日目)for-await-of
→ 別稿forEach()
→ 別稿
for-of
の亜種で、非同期に繰り返すやつに対応します。
const sleep = (ms) => new Promise((f) => setTimeout(f, ms)); async function* foo () { yield 11; await sleep(1000); yield 22; await sleep(1000); yield 33; } (async () => { for await (const value of foo()) { console.log(value); } })(); // 11 // 22 // 33
仕様
for-in
, for-of
と同じ章で説明されます。
基本的な処理もそれらと一緒。なので過去記事にも目を通しておいて頂きたいです。なにとぞ。
async
内でのみ利用可能
await
なので。
// OK (async () => { for await (const v of obj) { // … } }); // SyntaxError: Unexpected reserved word (() => { for await (const v of obj) { // … } });
非同期に反復可能なオブジェクト
for-of
では「反復可能」なオブジェクトが利用可能で、それは適切な [Symbol.iterator]()
メソッドが設定されているもの、でした。
for-await-of
で利用可能なオブジェクトは「非同期反復可能」なもので、それは適切な [Symbol.asyncIterator]()
メソッドが設定されているもの、です。
for-of
のときと同じく、そういうオブジェクトを自作することができます。
const sleep = (ms) => new Promise((f) => setTimeout(f, ms)); const obj = { async* [Symbol.asyncIterator] () { yield 11; await sleep(1000); yield 22; await sleep(1000); yield 33; }, }; (async () => { for await (const value of obj) { console.log(value); } })();
非同期のおさらい
軽く await
のお話をします。知ってる人は飛ばして次へ。
async
とは
関数を Promise
化するやつです。
// Promise版 const f = () => { const promise = new Promise((resolve, reject) => { resolve(123); }); return promise; };
// async版 const f = async () => 123; const p = f(); console.log(p instanceof Promise); // => true p.then((result) => { console.log(result); // => 123 });
return
すると resolve()
、 throw
は reject()
です。
今回の例だと非同期関数の中で何もしてないけど、もちろん普通は Promise
なりまた await
なりで非同期に処理をします。
await
とは
Promise
の then()
の代わりです。
// promise-then版 p.then((result) => { console.log(result); });
// async-await版 const result = await p; console.log(result);
インデントが深くならないところが素敵。
async
な関数内でのみ利用可能です。
Chrome DevToolsのコンソールだと await
動くけど、あれは特別。
外だとエラーに。
SyntaxError: await is only valid in async function
catch()
の代わりは構文の方の try-catch
です。
fetch()
の例
例えば、指定のパスのHTMLを取得し解析、 <title>
に設定されている文字列を得るやつ。
const fetchTitle = (path) => fetch(path) .then((res) => res.text()) // text()はPromiseを返す .then((html) => html.match(/<title>(.*)<\/title>/i)[1]);
これ↑を、こう↓書けます。
const fetchTitle = async (path) => { const res = await fetch(path); const html = await res.text(); return html.match(/<title>(.*)<\/title>/i)[1]; };
でもって async
が付いてる fetchTitle()
は Promise
オブジェクトを返すので、こう使います。(もちろんこいつらも await
でも良い。)
// 現在ページのタイトルを取得 fetchTitle(location.href) .then((title) => console.log('Title:', title)); // トップページのタイトルを取得 fetchTitle('/') .then((title) => console.log('Title:', title));
使い方
話を戻して for-await-of
は、非同期反復子 (AsyncIterator) が返す結果を await
しながら反復します。
こんな非同期反復子を返す関数があったとします。
async function* foo () { yield 11; await sleep(1000); yield 22; await sleep(1000); yield 33; }
繰り返さない例
まずはここから。
// 普通の反復子 const it = foo(); const result = it.next(); console.log(result);
// 非同期反復子 const ait = foo(); const result = await ait.next(); console.log(result);
普通の for
で繰り返す例
next()
呼び出すところで await
してますね。
for
文で同じようにして、値を非同期に得ながら反復することができます。
const ait = foo(); for (let cur = await ait.next(); !cur.done; cur = await ait.next()) { const { value } = cur; console.log('for', value); }
for-await-of
で書く例
長い for
文になってしまったけれど、大丈夫、僕らには for-await-of
構文があります。
for await (const value of foo()) { console.log(value); }
はいできあがり。
ついでに、普通の for
文で普通に await
する例
ここまでやってきた for-await-of
の特徴は反復子が非同期、というところなので、何でもないところで非同期にやるなら普通に await
するだけです。
for (const url of urls) { const res = await fetch(url); console.log(res); }
可能なら Promise.all()
を
というわけで色々書いてきたんだけど、非同期に繰り返すのは良くないです。
だってせっかく非同期に処理できるのだから、順番ではなく一度にまとめて実行して、その後に結果を順に処理していく方が高効率です。
例えば非同期処理がひとつ500msかかるとして、3つ順にやれば合計1500msの待機時間が必要になります。これらを Promise.all()
で並列に実行してやれば、待ち時間は結局500msのままです。
繰り返し中の await
を禁じるESLintの設定
ESLintにもそういう設定があります。
Performing an operation on each element of an iterable is a common task. However, performing an
await
as part of each operation is an indication that the program is not taking full advantage of the parallelization benefits ofasync
/await
.Usually, the code should be refactored to create all the promises at once, then get access to the results using
Promise.all()
. Otherwise, each successive operation will not start until the previous one has completed.
反復の各要素に対して操作を行うことは一般的な作業です。しかしながら、各段階の操作で await
を実行すると、そのプログラムが async
/await
による並列化の恩恵を十分に享受できないことになります。
一般にこのようなコードは、一度に全てのプロミスを作成しそのうえで Promise.all()
を用いて結果を得るようリファクターされるべきです。そうでないと連続的な処理 (successive operation) が前の処理が完了するまで始まりません。
for-await-of
を禁じるESLintの設定
ただし前項のものは普通の for
内で await
書いたときに引っかける用で、 for-await-of
は引っかからないです。
for-await-of
を( for-of
は許容しつつ)弾く設定はこんな感じ↓です。
module.exports = { "rules": { 'no-restricted-syntax': [ 'error', 'ForOfStatement[await=true]', ], }, };
普通の for
文で普通に await
しない例
上の方で書いた繰り返しの途中で await
する方の例を、 Promise.all()
を使って並列に実行して、その全体を await
で待つようにした例です。
const responses = await Promise.all( urls.map((url) => { console.log(url); return fetch(url); }) ); for (const res of responses) { console.log(res); }
複数の fetch()
が同時に走るので、こっちの方が速いです。
例
えーと、非同期に繰り返す例かあ……。
同意を促すメッセージ
ごめん思いつかなかった。
// 抜粋 async function* waitForAgreeing (interval = 2000) { let index = 0; while (true) { if (elAgree.checked) { return; } await sleep(interval); yield messages[index]; index = (index + 1) % messages.length; } } const main = async () => { for await (const message of waitForAgreeing()) { elMessage.textContent = message; } elMessage.textContent = 'さんきゅー'; };
その他
for-of
自体があまり良くない?
for-await-of
を弾く設定を上の方で書いたけど、そもそも for-of
を弾くべき?
AirbnbのESLintの設定をよく使ってるんだけど、そこでは for-of
の利用自体が禁止されています。(別途 for-in
も禁止。)
理由は設定に書いてある。
iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.
イテレーター、ジェネレーターは再生成ランタイム (regenerator-runtime) が必要で、それは本ガイドの許容範囲からするとあまりに重すぎます。またそれとは別に、ループの利用は回避し、配列の反復を利用してください。
arr.forEach()
を使えってさ。 Object.keys()
とかもだめなのかな。どれくらい遅いんだろ。
ちなみに for
と比べて forEach()
が遅すぎるってことはないです。
おまけ: for-await-in
for await (const key in obj) { console.log(key); }
SyntaxError: Unexpected token in
おしまい
あんまり使いどころがなあ。 for
でやれることだしせっかくだから for-of
でもできるようにしておこう、とかそういう感じなのかなあ。
ああでも配列以外のジェネレーター自体の良い使い道がまだわかってないから、そこら辺わかったら非同期に繰り返したくなるかも。
関連
- for文を仕様からじっくり見てみる。あとwhileとか。(配列とかおれおれAdvent Calendar2018 – 13日目)
- for-inの仕様も見てみたよ。使う機会なさそうだけど。(配列とかおれおれAdvent Calendar2018 – 14日目)
- for-ofで配列も普通のオブジェクトも反復しよう。(配列とかおれおれAdvent Calendar2018 – 15日目)
参考
- GlobalFetch.fetch() | MDN
- Body.text() | MDN
- no-await-in-loop – Rules – ESLint – Pluggable JavaScript linter
- no-restricted-syntax – Rules – ESLint – Pluggable JavaScript linter
- javascript/style.js at 685f37be39fd01bcfdd349de9acdcd5a50414520 · airbnb/javascript
- 関羽の似顔絵イラスト | かわいいフリー素材集 いらすとや
- ECMAScript® 2018 Language Specification
- 13.7.5 The for-in, for-of, and for-await-of Statements
- 13.7.5.12 Runtime Semantics: ForIn/OfHeadEvaluation ( TDZnames, expr, iterationKind )
- 13.7.5.13 Runtime Semantics: ForIn/OfBodyEvaluation ( lhs, stmt, iteratorRecord, iterationKind, lhsKind, labelSet [ , iteratorKind ] )
- 25.5 AsyncGenerator Objects
- 25.1.3 The %AsyncIteratorPrototype% Object
- 25.1.1.4The AsyncIterator Interface