【JavaScript】async/awaitについて理解を深めたい

# やりたいこと

`async/await`と`Promise`が全然理解できていないと思ったため、よくわからないと思ったところを実験してみることにしました。
この記事はその実験結果をまとめたものです。

:::note warn
この実験をしたのが記事の執筆時から1ヶ月くらい前の話なので、記憶が朧げなところがあります。
できる限り思い出して書いていますが、間違いがあったらすみません。
:::


# 実験方法

以下の4つの関数を使って実験をします。

```js:実験に使う関数を定義
// Promiseを書いたasync
async function a() {
    return new Promise((resolve, reject) => {setTimeout(() => {
        console.log('aが実行');
        console.timeLog();
        resolve();
    },1000);})
}

// Promiseを返すasyncじゃない関数
function b() {
    return new Promise((resolve, reject) => {setTimeout(() => {
        console.log('bが実行');
        console.timeLog();
        resolve();
    },1500);})
}

// Promiseを書いていないasync
async function c() {
    setTimeout(() => {
        console.log('cが実行');
        console.timeLog();
    },1700);
}

// 謎の関数X
function x() {
    console.log('xが実行');
    console.timeLog();
}
```

それぞれの関数の特徴は以下の通りです。

| 関数名 | ログ出力までの時間 | asyncはあるか | Promiseを返すか |
| :-- | :-- | :-- | :-- |
a | 1000ミリ秒 | ある | 返す
b | 1500ミリ秒 | ない | 返す
c | 1700ミリ秒 | ある | 返さない
x | 0ミリ秒 | ない | 返さない

上の関数たちを`await`をつけたり`then`を使って実行してみたりします。
そしてその結果を記録していきます。


# 実験結果

上の関数をいろいろ実行してみた結果です。
長いので結論だけ読みたい方は飛ばしてください。

:::note info
`>`は返り値を、`time:数値`は前のログが出力されてから経過した時間を示しています。
:::


:::note info
特に何も考えず実験結果を常体で書いてしまったため、
ここから敬語じゃ無くなります。
:::


## 何もつけなかった場合

`a`は `async`があって`Promise`を返す関数。

```js:aを実行
a();
// > time:0 Promise {<penging>}
// time:1000 aが実行
```

`b`は `async`が無くて`Promise`を返す関数。

```js:bを実行
b();
// > time:0 Promise {<penging>}
// time:1500 bが実行
```

`c`は `async`があって`Promise`は返さない関数。

```js:cを実行
c();
// > time:0 Promise {<fulfilled>: undefined}
// time:1700 cが実行
```

`x`は `async`が無くて`Promise`も返さない関数。

```js:xを実行
x();
// time:0 xが実行
// > time:0 undefined
```

結果をまとめるとこんな感じ。

| 関数名 | 返り値 | 値が返ってくるまでの時間 |
| :-- | :-- | :-- |
a | `Promise {<penging>}` | 0ミリ秒
b | `Promise {<penging>}` | 0ミリ秒
c | `Promise {<fulfilled>: undefined}` | 0ミリ秒
x | `undefined` | 0ミリ秒

`then`や`await`などをつけないで実行した場合、値が返ってくるまでの時間(=処理が終了するまでの時間)はほぼ0。

また、`a`と`b`は全く同じ結果だった。
`c`は`a`や`b`と返り値が違った。


## thenを使った場合

次は`a, b, c`の関数に`then`をつけて実行してみる。
`then`で実行するのは`x`。

`a`は `async`があって`Promise`を返す関数。
```js:aを実行
a().then(x);
// > time:0 Promise {<penging>};
// time:1000 aが実行
// time:0 xが実行
```

`b`は `async`が無くて`Promise`を返す関数。
```js:bを実行
b().then(x);
// > time:0 Promise {<penging>};
// time:1500 bが実行
// time:0 xが実行
```

`c`は `async`があって`Promise`は返さない関数。
```js:cを実行
c().then(x);
// time:0 xが実行
// > time:0 Promise {<fulfilled>: undefined}
// time:1700 cが実行
```

ここまでの結果を表にまとめるとこんな感じ。

| 関数名 | 返り値 | ログの出力順 |
| :-- | :-- | :-- |
a | `Promise {<penging>}` | a → x
b | `Promise {<penging>}` | b → x
c | `Promise {<fulfilled>: undefined}` | x → c

`a,b`はこれらの関数が実行し終わってから`x`が実行された。
`c`は実行し終わる前に`x`が実行された。

また、前と同じく`a`と`b`の実験結果は変わらなかった。
ログの出力順的に`c`だけ実行完了を待っていないように思える。

次は複数の関数を`then`で実行してみる。

```js:aとbを実行
a().then(b).then(x);
// > time:0 Promise {<penging>};
// time:1000 aが実行
// time:1500 bが実行
// time:0 xが実行

// 実行順： a → b → x
// 上の意味： aが実行し終わった後にbが実行開始され、bが実行し終わってからxが実行開始された
```

```js:bとcを実行
b().then(c).then(x);
// > time:0 Promise {<penging>};
// time;1500 bが実行
// time:0 xが実行
// time:1700 cが実行

// 実行順: b → c = x
// 上の意味： bが実行し終わった後に、cとxが同時に実行開始された
```

```js:cとbの実行順を逆にする
c().then(b).then(x);
// > time:0 Promise {<penging>};
// time;1500 bが実行
// time:0 xが実行
// time:200 cが実行

// 実行順: c = b → x
// xはbの実行が終わってから実行された
```

```js:aとbとcを実行
a().then(b).then(c);
// > time:0 Promise {<penging>};
// time:1000 aが実行
// time:1500 bが実行
// time:1700 cが実行

// 実行順: a → b → c
```

```js:aとcとbを実行
a().then(c).then(b);
// > time:0 Promise {<penging>};
// time:1000 aが実行
// time:1500 bが実行
// time:200 cが実行

// 実行順: a → c = b
```

まとめるとこんな感じ。

| 実行した関数 | 実行順 |
| :-- | :-- |
`a().then(b).then(x)` | a → b → x
`b().then(c).then(x)` | b → c = x
`c().then(b).then(x)` | c = b → x
`a().then(b).then(c)` | a → b → c
`a().then(c).then(b)` | a → c = b

`c`の箇所には = が入っている。
つまり`c.then(x)`としても`x`は`c`の処理が終わる前に実行されてしまう。
ちなみに`c`は`async`をつけていて何も返さない関数(なお`async`がついているため実際には`Promise<undefined>`が返ってくる)。

どうやら、内部に`setTimeout`を使っている場合、ただ`async`を関数につけただけでは処理が終わるまで待つことはできない模様。


## awaitをつけた場合

今度は`await`をつけて関数を実行してみます。

```js:aを実行
await a();
// time:1000 aが実行
// > time:0 undefined
```

```js:bを実行
await b();
// time:1500 bが実行
// > time:0 undefined
```

```js:cを実行
await c();
// > time:0 undefined
// time:1700 cが実行
```

```js:xを実行
await x();
// time:1700 xが実行
// > time:0 undefined
```

結果をまとめるとこんな感じ。

| 関数名 | 最後のログが出力されるまでの時間 | 値が返ってくるまでの時間 |
| :-- | :-- | :-- |
a | 1000ミリ秒 | 1000ミリ秒
b | 1500ミリ秒 | 1500ミリ秒
c | 1700ミリ秒 | 0ミリ秒
x | 0ミリ秒 | 0ミリ秒

`a`と`b`が値が返ってくるまでに時間がかかるのに対し、`c`はほぼ0秒で返ってくる。
やっぱり`async`をつけただけだと実行は待ってくれなそう。


# まとめ

というわけで、実験結果をまとめていきたいと思います。

:::note info
ここから敬語に戻ります。
:::

前提として、関数の宣言の先頭に`async`をつけると、その関数は`Promise`を返すようになります。

```js:先頭にasyncをつけるとPromiseを返す
// asyncなし
function notAsync() {
    return 'aaa';
}
notAsync(); // 'aaa'

// asyncあり
async function hasAsync() {
    return 'bbb';
}
hasAsync(); // Promise {<fulfilled>: 'bbb'}
```


## 効果がない例

まず、`Promise`を返す関数に`await`をつけた場合、関数は`Promise`の結果を返すようになります。

`Promise`を返さない関数に`await`をつけた場合、`await`などなかったかのように関数は実行されます。
このとき、特にエラーなどは出ません。

```js:awaitをつける
// Promiseを返さない
function notAsync() {
    return 'aaa';
}
await notAsync(); // 'aaa'、このawaitはないも同然

// Promiseを返す
async function hasAsync() {
    return 'bbb';
}
await hasAsync(); // 'bbb'、このawaitはPromiseを見てくれるはず
```

例えば、`setTimeout`は`Promise`を返さないため、`await setTimeout(() => {関数}, 1000)`のようにしても、その後に書いた処理は`setTimeout`内の処理が終わる前に実行されてしまいます。

```js:setTimeoutにawaitしても効果なし
async function test() { // awaitが入っている関数にはasyncが必要
    await setTimeout(() => { // このawaitに効果はない
        console.log('setTimeout内');
    }, 1000);
    console.log('setTimeout後'); // こっちが先に出力される
}

test();
// time:0 setTimeout後
// > time:0 Promise {<fulfilled>: undefined}
// time:1000 setTimeout内
```

そして、ただ`async`をつけただけだと`await`しても効果はなく、関数の内部に`await`を含まないと処理の完了を待ってくれません。
例えば、`setTimeout`を含む関数の先頭に`async`ををつけても効果はありません。

```js:awaitのない関数にasyncしても効果なし
async function test() { // このasyncは効果なし
    setTimeout(() => {
        console.log('setTimeout内');
    }, 1000);
}

await test(); // asyncは無いも同然状態のため、このawaitは効果なし
console.log('testの後'); // 先に出力される
// time:0 testの後
// > time:0 undefined
// time:1000 setTimeout内
```


## 効果がある例

じゃあ何なら効果あるのかと言いますと、要は内部で`Promise`を返す関数なら何でもいいはずです。

例えば第一引数の時間だけ待つ以下の関数は、`setTimeout`を含みますが、`Promise`を返していて`resolve`を呼び出しているので効果があります。

```js:特定の秒数待つ関数
function sleep(sec) { // asyncはあってもなくても同じ
    return new Promise((resolve, reject) => { // Promiseを返している
        setTimeout(() => { resolve() }, sec); // secミリ秒後にresolve
    });
}

await sleep(1000);
console.log('sleep後'); // 1000ミリ秒後に出力される
```

また、最初から`Promise`を返してくれる関数に対しても使えます。
例えば`fetch`などです。

```js:awaitに効果がある例
const response = await fetch('url');
```

```js:asyncとawaitに効果がある例
// urlからjsonを返す関数、特に使い道はない
async function getJson(url) {
    const res = await fetch(url); // fetchはPromiseを返す
    return res.json();
}

// 使うとしたらこんな感じ
const { sumple } = await getJson('url');
```

## 結論

`Promise`は便利です。
これを使うと今までできなかったことができるようになります。

また`async/await`を使うとコードを大幅に短くすることができます。
そしてネストが浅くなります。

なのでこの2つは積極的に使っていきましょう。

:::note info
ちなみにこの結論はうまいこと話をまとめるためだけに書かれました。
なので特に意味はありません。
:::


以上です。