Promiseやasync/awaitは非同期処理じゃない。


※ この記事はJavaScriptを勉強中の人のための記事です。

あらすじ

自分が非同期処理について学んでいたときの経験や、
人に非同期処理について教えているときの経験を振り返ると、
非同期処理について学んでいる人は
Promiseasync await = 非同期処理
という捉え方をしてしまうことが多いんじゃないかな?と思いました。
この記事では、そうではないという事を解説します。

理解するのも説明するのも面倒くさい内容なので、
この記事を読めばどういうことか理解できるように、まとめておこうと思いました。

(注)
この記事で言及する事は、ほとんどの内容が、
Promiseにもasync awaitにも当てはまる説明になります。
そのためこの記事では、これらをまとめてPromiseと呼ぶことにします。
(というか、「async/awaitはPromiseでもある」というのが正しいんですが、話が逸れてしまうので割愛します。)

違う、非同期処理じゃない。

前提として、まず意識しておいてほしいのが
Promiseは、非同期処理じゃないという事です。
(「非同期処理をするための機能」でもありません。)

Promiseが非同期処理だと解釈しちゃう背景としては、
おそらく、会話の流れが
「非同期処理について学びましょう」
「非同期処理を使う時には、Promiseを使います」
というような順番になりがちだからだと思います。

このような流れで話をしてしまうと、Promiseを学び始めた人は、
「非同期処理をするためにPromiseを使うんですねっ
という結論に達してしまいます。

しかし、これは間違いです。
Promiseは、非同期処理をするための機能ではありません。
どちらかと言うと、非同期処理をしないための機能です。

非同期処理について復習

詳しく説明する前に、
まず、非同期処理と、非同期じゃない処理(=同期処理)について、おさらいしましょう。

非同期処理
この処理が終わってなくても、次の処理へ進む処理のこと。
同期処理
この処理が終わるまで、次の処理に進まない処理のこと。

となります。

もし、理解できてない場合、私が以前書いた記事が参考になると思います。
【JavaScript】非同期処理とasync/await ~難しいこと抜きで、まず使いはじめるための知識~ ( ´ε` )💻
(中盤からはasync/awaitの使い方の説明になるので、気にしなくていいです。)

Promiseは、同期処理をするための機能

Promiseを使うのは、本来であれば非同期処理であるAjaxなどの機能を、
きちんと処理が終わるのを待ってから進めたいときに使う機能です。

つまり、非同期処理を同期処理にするための機能です。

コードを使って解説

「そんな一言でバッサリ言われてもわからん!」という人のために
実際にAjaxを実行するための関数 fetch() を使って解説します。

まず、非同期処理という概念がない理想の世界では、
下記のようなイメージになると思います。

// あなた: URLからデータを取得してきてresponseに代入!
let reponse = fetch( 'https://example.com' );
// あなた: 取得したデータをJSで利用しやすい形に変換!
response = response.json();

// あなた: よし! これで、変換したデータを使って処理ができるぞ!
console.log(response);

// あなた: v(^_^)v

実際には、fetch()が非同期処理なので、
下記のような悲しい結果になります。

// あなた: URLからデータを取得してきてresponseに代入!
let reponse = fetch( 'https://example.com' );
// fetchさん: いま、通信中だよ〜! 取得できるまでもうちょっと待ってね!

// あなた: 取得したデータをJSで利用しやすい形に変換!
response = response.json();
// JavaScriptさん: エラー発生! reponseにデータが入ってないよ?
// fetchさん: まだ、通信中だよ〜!

// 1秒後...

// fetchさん: 通信おわったよ〜! 取得したデータをresponseに入れたから確認してね!
// JavaScriptさん: 遅いよ〜! もうエラー出しちゃったよ。

// あなた: (へんじがない。ただの しかばね のようだ。)

非同期処理を、非同期処理のまま処理してしまうと、
上記のような結果になってしまいます。

このような結末を阻止するために利用するのが、Promiseとなります。

Promise thenを使って説明して欲しい人は こちら!



Promise thenを使って書く場合、こんなコードになります。
fetchの返り値は、もともとPromiseのインスタンスだと決まってるので、new Promiseする必要はありません。
fetch( 'https://example.com' )
  .then( function( response ){
    response = response.json();
    return response;
  })
  .then( function( response ){
    console.log(response);
  });

セリフをつけるなら、こんな感じです。

// あなた: URLからデータを取得!
// fetchさん: いま、通信中だよ〜! 取得できるまでもうちょっと待ってね!
fetch( 'https://example.com' )
  // あなた: データが取得できたら、thenの中の処理を実行してね!
  .then( function( response ){

  // 1秒後...

  // fetchさん: 通信おわったよ〜! 
  // JavaScriptさん: おっけ〜! 次の処理をはじめるよ! 取得したデータは引数(response)に代入したよ!

    // あなた: 取得したデータをJSで利用しやすい形に変換!
    response = response.json();
    // あなた: 変換したデータを、次の処理にバトンタッチするよ!
    return response;
  })
  // あなた: データが変換できたら、次のthenの処理を実行してね!
  .then( function( response ){

  // 0.1秒後...

  // JavaScriptさん: おっけ〜! 次の処理をはじめるよ! 変換したデータは引数に入ってるよ!

    // あなた: よし! この関数の中なら、変換したデータを使って処理ができるぞ!
    console.log(response);

    // あなた: v(^_^)v

  });

※ 実はresponse.json()も非同期処理なので、変換したいデータは次のthenに渡してから続きの処理を実行する必要があります。非同期処理じゃない普通の関数であれば、ひとつのthenの中にまとめてしまっても構いません。(同期処理であれば、もともと、処理が終わるまで待つようになっているので。)

async awaitを使って説明して欲しい人は こちら!



async awaitを使って書く場合、こんなコードになります。
awaitを使うためには、その処理をasync関数の中に記述する必要があります。
async function xxx(){
  let response = await fetch( 'https://example.com' );
  response = await response.json();
  console.log(response);
}

xxx();

セリフをつけるなら、こんな感じです。

// あなた: awaitを使う処理を書くぞ!
async function xxx(){

  // あなた: URLからデータを取得してきてresponseに代入!
  let response = await fetch( 'https://example.com' );
  // fetchさん: いま、通信中だよ〜! 取得できるまでもうちょっと待ってね!
  // JavaScriptさん: おっけ〜! awaitがついてるから、この処理が終わるまで、次の処理には進まないよ!

  // 1秒後...

  // fetchさん: 通信おわったよ〜! 取得したデータをresponseに入れたから確認してね!
  // JavaScriptさん: おっけ〜! 次の処理をはじめるよ! 

  // あなた: 取得したデータをJSで利用しやすい形に変換!
  // JavaScriptさん: おっけ〜! awaitがついてるから、この処理が終わるまで、次の処理には進まないよ!
  response = await response.json();

  // 0.1秒後...

  // JavaScriptさん: おっけ〜!response.json()の処理がおわって、responseに代入されたよ!    
  // あなた: よし! この関数の中なら、変換したデータを使って処理ができるぞ!
  console.log(response);

  // あなた: v(^_^)v

}

// あなた: おっと危ない! 上で定義した処理を、忘れずに実行するぞ!
xxx();

※ 関数を定義してすぐに実行したい場合、いちいち関数名を決めるのが嫌だったら即時関数という書き方を使えば、定義してすぐに実行できます。
※ 実はresponse.json()も非同期処理なのでawaitする必要があります。しかし、非同期処理じゃない普通の関数であれば、awaitを付ける必要はありません。(同期処理であれば、もともと、処理が終わるまで待つようになっているので。)

おわり

いかがでしょうか?
前の章で書いた例のように、
本来は非同期通信であるAjaxなどを使った処理を、順番どおりに実行するための機能が、
Promiseasync awaitだということを、ご理解いただけたら幸いです。