1399
921

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaScript 2Advent Calendar 2019

Day 19

ラーメンで理解するasync/await

Last updated at Posted at 2019-12-18

JavaScript 2 Advent Calendar 2019 の19日目の記事です。

(19/12/23 10:41追記)
Promise.allについて最後に追記しました。

対象

  • async/awaitがなんだかはある程度知ってる人
  • async/awaitをなんとなくで使ってる人

そもそもasync/awaitって?

async/awaitは、Promiseによる非同期処理をより簡潔に効率よく記述できる手法。
普通にPromiseを使うとネストが深くて辛くなるのを救ってくれる。
「async/await Promise」で検索すれば比較についてはたくさん出るので今回は書かない。

便利だから全部async/awaitにしちゃおう!

って思うんですけど、実は罠があって。
ちゃんと気をつけないと非効率な感じになっちゃうよっていうのが今回のお話。
ただ、コードを並べて説明してもよくわからない気がしたので、みんな大好きラーメンで例えてみようと思います。

image.png

ラーメンを作ろう!

突然ですが、あなたはラーメン屋店主です。
あなたのラーメン屋には腕のいい三人の店員がいます。

それぞれ、

  • 麺担当
  • スープ担当
  • 具担当

とします。

(なんかどっかで見た)

ラーメンを作るためには、

  1. 麺担当に麺をゆでてもらう
  2. スープ担当にスープを作ってもらう
  3. 具担当にチャーシューを切ってもらう
  4. 1~3までが出来たらそれらを組み合わせてあなたがラーメンを完成させる

という工程が必要になります。

APIを準備しよう!

こんなAPIが用意されているとしましょう。

エンドポイント メソッド 概要
/noodle GET 麺を作成し取得する
/soup GET スープを作成し取得する
/pork GET 具を作成し取得する
/ramen POST ラーメンを作る

実装してみよう!

なんのこっちゃない、かんたんかんたん


async () => {
  
  // 麺担当に麺をゆでてもらう
  const noodles = await axios.get('/noodle');
  
  // スープ担当にスープを作ってもらう
  const soup = await axios.get('/soup');
  
  // 具担当にチャーシューを切ってもらう
  const pork = await axios.get('/pork');
  
  // ラーメンを完成させる。
  const ramen = await axios.post('/ramen', {
    noodles,
    soup,
    pork
  });

  // 提供
  return ramen;
}

ちょちょいのちょい!
すっきりしてて良さそうですが、これはNGです。

awaitの罠

awaitは、指定した関数のPromiseの結果が返されるまで、async function内の処理を一時停止します。
つまりどういうことかというと、

await axios.get('/noodle');

の結果が帰ってくるまで処理を待ってしまいます。
その間 /soup /pork へのリクエストはされません。

つまり、
麺を茹で上がるのを待って、スープを作り始め
スープが出来上がるのを待って、チャーシューを切り始める
などといったものすごく非効率なことになってしまうのです!

そんなことしてたら出来上がる頃には麺がのびのびになってしまう...

一人ずつ作業が終わるのを待つのではなく、三人店員がいるんだから同時に指示をしたいですよね?

どうすればいいの?

awaitをするタイミングをずらします。

async () => {
  
  // 麺担当に麺をゆでてもらう
  const noodlesPromise = axios.get('/noodle');
  
  // スープ担当にスープを作ってもらう
  const soupPromise = axios.get('/soup');
  
  // 具担当にチャーシューを切ってもらう
  const porkPromise = axios.get('/pork');

  // 全員が出来上がるのを待つ
  const noodles = await noodlesPromise;
  const soup = await soupPromise;
  const pork = await porkPromise;
  
  // ラーメンを完成させる。
  const ramen = await axios.post('/ramen', {
    noodles,
    soup,
    pork
  });

  // 提供
  return ramen;
}

awaitはPromiseを返す処理とセットにしがちですが、別にバラバラに書いても良いのです。

これを行うことで、
麺担当・スープ担当・具担当に同時に調理指示をする。
その後全員が調理終了し、完成したらそれらを使ってラーメンを完成させる。
ということが実現できるわけです。

image.png

完成!やったね!

まとめ

ラーメンでふわふわっとした感じになっちゃいましたが、伝えたかったことは
待つ必要がある場合は待つ、待つ必要がない場合は待たない
ということをちゃんと意識して書くと良さそうっていうあれです。

最後に

書いててラーメン食べたくなってしまったのでラーメン食べに行ってきます。

(追記)Promise.allについて

すごく記事の反響が大きくてびっくりしています。またありがとうございます。
ここのコメントでもTwitterでもPromise.allのほうが良いんじゃないの?という意見を頂いているので、そこについてです。

結論から言うとPromise.allで書くこともできます。また、状況によってはそちらのほうがベターだと思います。

たとえば今回の場合では、

async () => {
  const [noodles, soup, pork] = await Promise.all(['/noodles','/soup','/pork'].map(axios.get));

  const ramen = await axios.post('/ramen', {
    noodles,
    soup,
    pork
  });

  // 提供
  return ramen;
}

とか書けたりすると思います。

ただ、この記事の意図としては「Promiseの受け取りとawaitのタイミングをずらすことができる」だったのと、なるべくシンプルな記事にしたかったので書くか迷ったのですが、省かせてもらっていました。

1399
921
14

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1399
921

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?