Javascript初心者の 非同期処理 Promise async await


はじめに

私はJavascript初心者です。
今までバックエンドエンジニアをやってきたので、Javascriptの概念に追いつくのに時間がかかりました。

その中で私を苦しめたのは、並列処理と非同期処理でした。
また、それを解決してくれたPromise async awaitについてのお話をしようと思います。
並列処理に関しては、またの機会に・・・。

そもそも非同期処理ってなに?

非同期処理とは・・・色々な尊敬すべき先輩たちが語っているので、私から説明するのもおこがましいのですが、簡単に説明します。
一言で言うと、I/O待ちをしないってことです。
別な言い方をすると、実行できる時に実行するってことですね。

具体的な例を出してみましょう。

この記事を読んでいるJavascript初心者の皆さん。ボタンのクリックイベントって違和感ありませんか?
私は同期処理ばかり書いてきたので、違和感がありました。
だってプログラムって上から順番に実行されるものでしょ?それが鉄則。そう思っていた時期が私にもありました。

Javascriptのボタンクリックイベントは、対象のボタンイベントを覚えていて、ボタンが押された時に実行される。
これは違和感ありありでした。

ざっくりですが、これが非同期処理で言う、実行できる時に実行をする。と言う考え方です。

ざっくりした非同期のイメージ

理解しにくい”時間がかかる処理”

ボタンクリックイベントなんかは、なんとなく理解できました。
JS読み込み時にクリックイベントが存在することを認識させておけば、クリックされた時点で、クリックイベントを発火できる!

なんだよ!楽勝じゃん!
とは問屋がおろしませんでした。

本質的には同じことなのですが、少し理解しにくい"時間がかかる処理"について。

例えば、今回仕事で利用したFirebaseです。
これ、Node.jsで作成して、サーバ側もJSでかけまっせ!ってやつです。
Node.jsは某有名な騎空団なゲームも利用しています。
「C10K問題」(クライアント1万台問題)を解消してくれるすごいやつです。
「C10K問題」や「Firebase」についてはまた今度機会があったら記事にしたいです。

Firebaseには、FirestoreっていうDBがあるのですが、クライアント側からFirestoreにアクセスすると時間がかかる。
PHPなんかで言うと、DBConnectionですね。

先ほどもお話したように、時間がかかる処理はI/O待ちしませんので、例えば以下のようなコードを書くと問題が起こります。

const snapshot = firebase.firestore().collection("aaa").get();

var snapDatas = snapshot.data();

for(const data of snapDatas){
  ........処理......
}

さて、どんな問題が起こるでしょうか?

Javascriptさんの気持ちを代弁しましょう。

Javascriptさんは基本的にせっかちです。

const snapshot = firebase.firestore().collection("aaa").get();

Javascriptさん「Firestoreに接続しに行ってるよー」
Javascriptさん「.............まだー?」
Javascriptさん「応答ないから先行くねー!」

と結果が返ってくる前に先に進んでしまいます。
その結果

var snapDatas = snapshot.data();

Javascriptさん「"undefined"って言われたー」

と言ってエラーを吐きます。
そしてプログラムが動き終わって暫くたった頃、

Javascriptさん「snapshotさん帰ってきたー」

と反応があります。
おせぇぇぇぇぇぇぇぇぇぇぇぇ!
Javascriptさん的には、今処理が終わったようです。
実行できる時に実行する。。。。。なるほどそう言うことね。。。。
ともあれ、ほしい時にデータが返ってこない!

これ・・・どうすんの?
これを解決してくれたのが、Promise async awaitでした。

Promiseってなに?

一言で言うと、非同期のJSを同期っぽく動作させてくれる関数です。

callbackでも同様の処理は一応作れますが、めちゃくちゃ複雑化するので、使わない方針で。
その辺はググってもらえればエンジニアの悲喜交々が聞こえてきますよきっと。
(IEの昔のバージョンはPromiseに対応していないため、悲鳴をあげているのはIEが要件に入ってるエンジニアなのではないかなー?)

Promiseの基本的な使い方は以下の通りです。

//Promise関数を利用して、本日の日付を取得する。
var result = new Promise(function(resolve){
  resolve(new Date);
});
//resultを実行した後に、年を取得する
var res = result.then(function(data){
  console.log(data.getFullYear());
});

ポイントはresolve()と、then()です。

resolve(new Date);
とやると、戻り値に「new Date」が設定されます。
thenは、Promise関数実行後に、実行してほしい処理を記載します。

then(function(data))
のdataにnew Dateが入っているわけですね。

読みにくい問題

Promiseで記載してあげると、順番を指定できることがわかりました。
しかし、そうすると結構読みにくくなる印象です。
なぜなら、

var res = result.then(function(dara){
  var testData = "";
  //ここに後続処理
  .
  .
  .
  .
  .
  .
  .
  .
  .
  .
  .
  .
});

thenの中に記載する内容が長くなれば長くなるほど、下に読んでいって、上に戻らないといけないわけです。
これは読みにくい。

async await

そこで登場するのがasync awaitです。
firebaseのソースを元に考えてみましょう。
まずはthenを利用した書き方。
ちなみに、firebase.firestore().collectiom("aa").get()が既にPromise関数で返ってくるため、
新規でPromise関数として宣言する必要はありません。

var getFirebaseData = function(){

  firebase.firestore.collection("aa").get().then(function(snapshot){
    var snapDatas = snapshot.data();

    for(const data of snapDatas){
      ........処理......
    }
    .
    .
    .
    .
    .
    .
    .
    .
    .
    .
  });
}

となります。

続いて、async awaitを利用した記載方法

var getFirebaseData = async function(){
  const snapshot = await firebase.firestore.collection("aa").get();

  var snapDatas = snapshot.data();

  for(const data of snapDatas){
    ........処理......
  }
  .
  .
  .
  .
  .
  .
  .
  .
  .
  .
}

こっちのが読みやすくないですか?

thenの中で書くと、スコープの管理もしなくちゃいけないですし、面倒が増えます。
何より、ごちゃつく!

そのため、Promiseの同期処理をしたい場合は、async awaitを利用するのがおすすめです!

まとめ

・時間がかかる処理を実行したいときはPromiseを思い出してみて。
・Promiseを使ったらasync awaitがとっても便利!