awaitの取り入れ方

これまでの記事で、以下のことを示しました。

- `await`を使う理由は、別の処理に実行権を譲ること
- `await`を使うと、プログラムの複雑さが増す
- まずは`await`を使わずに実装し、時間のかかる処理から`await`を取り入れる

今回の記事では、どのようにして`await`を取り入れるかについて説明していきます。

## `await`の取り入れ方

例として、以下のコードに`await`を取り入れることを考えます。

```javascript
const heavy = () => {
  for (let i = 0; i < 1000; i++) {
    light();  // 0.01秒
  }
};
```

`light()`には0.01秒かかり、それを1000回呼ぶ`heavy()`は10秒（0.01秒×1000）かかります。この処理には`await`がないため、`heavy()`を呼ぶと10秒間フリーズします。ここに`await`を取り入れます。

取り入れる手順は以下になります。

1. `sleep()`を定義する
2. `await sleep(0);`を適切な場所に記述する
3. 関数の定義に`async`を付ける

`sleep()`の定義にはいくつかバリエーションがありますが、今回は`window.setTimeout()`を使った実装にします。次に`await sleep(0);`の記述場所ですが、具体的には「軽い処理と軽い処理の間」が記述に適した場所です。ということで、`light();`の前もしくは後に記述します。

`await`を取り入れたコードは次のようになります。

```javascript
const sleep = msec => {
  return new Promise(resolve => {
    window.setTimeout(() => resolve(), msec);
  });
};

const heavy = async () => {
  for (let i = 0; i < 1000; i++) {
    light();
    await sleep(0);
  }
};
```

このコードでは、`light()`呼び出し後に別の処理に実行権を譲っています。したがって、フリーズを体感することなく`heavy()`の処理が終わります。

### 実行権を譲る＝遅い

実行権を譲るということは、一度実行権を手放すということです。一度手放した実行権が戻ってくるのには時間がかかるため、手放す回数が多いほど、`await sleep(0);`にかかる時間の割合は大きくなります。具体的には、`await sleep(0);`行をコメントアウトするかどうかで実行時間が4秒以上変わります。

<details><summary>実際に動かしたコード</summary><div>

```javascript
const sleep = msec => {
  return new Promise(resolve => {
    window.setTimeout(() => resolve(), msec);
  });
};

const light = () => {
  const start = new Date().getTime();
  while (new Date().getTime() - start < 10);
};

const heavy = async () => {
  for (let i = 0; i < 1000; i++) {
    light();
    await sleep(0);  // ここをコメントアウト
  }
};

(async () => {
  const s = performance.now();
  await heavy();
  console.log(performance.now() - s);
})();
```
</div></details>

このように、`await`の回数に応じて実行時間は長くなるため、極力`await`の回数を少なくしたいです。そこで`heavy()`を次のように書き換えます。

```javascript
const heavy = async () => {
  let c = 0;
  for (let i = 0; i < 1000; i++) {
    light();
    c++;
    if (c % 3 === 0) {
      c = 0;
      await sleep(0);
    }
  }
};
```

書き換え前のコードでは`await`が1000回実行されていましたが、書き換え後のコードでは333回になりました。これは、`light()`を3回実行してから`await`を1回実行するということです。間に0.03秒のフリーズが発生しているため、少しカクつくかな？と感じるかもしれませんが、その代わりに実行時間が2.6秒以上短縮されます。

> このあたりは、実際に動かしながらチューニングする部分です。3回に1回ではカクつきがひどいと感じるかもしれません。そういったときは、`c % 3 === 0`の`3`を`2`に書き換えます。逆に、カクつきがひどくてもいいから早く処理を終わらせたい場合は`3`を`4`や`5`に書き換えます。このように、実際に動かしながら妥協点を探していきます。

### `heavy()`の変貌

`heavy()`の修正前と修正後のコードを比較します。

```javascript
const heavy = () => {
  for (let i = 0; i < 1000; i++) {
    light();
  }
};
```

```javascript
const heavy = async () => {
  let c = 0;
  for (let i = 0; i < 1000; i++) {
    light();
    c++;
    if (c % 3 === 0) {
      c = 0;
      await sleep(0);
    }
  }
};
```

修正前のコードは読みやすいです。`heavy()`は`light()`を1000回呼び出す関数だということがひと目で分かります。しかし、修正後のコードはごちゃっとしており、ひと目では`heavy()`が何をしているかがわかりません。

なぜごちゃっとしているかというと、「`light()`を1000回呼び出す処理」と「3回に1回スリープする処理」が一つのブロック内に記述されているからです。

### `yield`を使って処理を分離

この2つの処理を分離する方法として、`yield`を使う方法があります。ということで、実際に`heavy()`に`yield`を取り入れてみます。

まず、`heavy()`の適切な場所に`yield`を記述します。「適切な場所」というのは、`await sleep(0);`のときと同じで「軽い処理と軽い処理の間」です。

```javascript
const heavy = function* () {
  for (let i = 0; i < 1000; i++) {
    light();
    yield;
  }
};
```

次に、`heavy()`の呼び出し側のコードを次のように書き換えます。

```javascript
(async () => {
  for (var _ of heavy()) {
    await sleep(0);
  }
})();
```

これで`yield`による修正は終わりです。

この修正の優れているところは、「`light()`を1000回呼び出す処理」と「スリープする処理」が完全に分かれていることです。

スリープする回数を3回に1回の頻度に減らしてみます。このとき、`heavy()`を一切書き換えることなく修正が完了します。

```javascript
const heavy = function* () {
  for (let i = 0; i < 1000; i++) {
    light();
    yield;
  }
};

(async () => {
  let c = 0;
  for (var _ of heavy()) {
    c++;
    if (c % 3 === 0) {
      c = 0;
      await sleep(0);
    }
  }
})();
```

まったくスリープしないという選択もできます。もちろん、`heavy()`を修正する必要はありません。

```javascript
(() => {
  for (var _ of heavy());
})();
```

`yield`と`await`を使うことで、2つの異なる処理を綺麗に分離できることがわかりました。

## まとめ

この記事では、`await`の取り入れ方について解説しました。そして、`yield`と`await`を組み合わせることで、2つの異なる処理を綺麗に分離できることを示しました。