LINDORのAdvent Calendar(本物)の16日目を開けたところ。
配列とかおれおれAdvent Calendar2018 – 16日目

繰り返しにもいろいろあるよ。

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);
  }
})();

1000ミリ秒止まりながら繰り返す様子。

非同期のおさらい

軽く 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 of async/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-of はエラー、 for-of はOKになる。

普通の 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 = 'さんきゅー';
};

2秒ごとに同意を促す文言が表示され続ける例。
同意するまでメッセージが更新され続ける。

その他

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 でもできるようにしておこう、とかそういう感じなのかなあ。

ああでも配列以外のジェネレーター自体の良い使い道がまだわかってないから、そこら辺わかったら非同期に繰り返したくなるかも。

関連

参考