morimorig3.com

初学者向けPromise探求【コールバック・非同期編】

keyVisual

初学者向けPromise探求【コールバック・非同期編】

JavaScriptのPromiseを説明してくださいと言われて、サラッと説明できたらかっこいい!この記事で目指すのは、Promiseがなぜ必要かを説明できるようになることを目指します。

普段からPromiseを使う機会は多いと思います。非同期でデータを引っ張ってくるときに、取得処理をPromiseでラップしてthenでメソッドチェーンを繋いで・・

やってみろと言われたらできるけど、説明してと言われたらなかなか説明しづらい。そんな技術だと思います。

はじめに

Promiseの存在意義は、非同期の逐次処理をコールバックを使わず書けることにある

この意味がわかる方は以下の記事を読んでも何も得られないかもしれません。意味がわからなかった方は一緒に旅をはじめましょう。

コールバック

これから非同期の逐次処理をコールバックという部分を紐解いていきます。

最初はコールバックです。たとえば、このようなスクリプトは意図したように動くでしょうか。

let response = null;

// データを取得してresponseへ格納する関数
function funcA() {
  const xhr = new XMLHttpRequest();
  const URL = "https://api.github.com/users/morimorig3";
  xhr.open("GET", URL);
  xhr.send();
  xhr.onload = function () {
    response = xhr.response;
  };
}
// 格納されたデータを表示する関数
function funcB() {
  console.log(JSON.parse(response));
}

funcA();
funcB(); // null

データを取得してlogで表示してやろうというスクリプトですね。

  1. responseという空の箱を用意します。
  2. funcAでデータを取得して、responseに格納します。
  3. funcBでresponseに格納されたデータを表示します。

残念ながら、意図した通りには動きません。funcBはnullを返してしまいました。 なぜならばfuncAのデータの取得は非同期で行われているためです。(非同期についてはあとで説明するので、今はわからなくて大丈夫です。)

funcAのデータ取得が完了して、responseにデータを格納する前にfuncBが実行されるのでnullが表示されたというわけです。 関数の実行順序が意図通りにならなかったため、問題が発生していたんですね。

このような問題を解決するためにコールバックを使います。

let response = null;

function funcA(callback) {  // コールバック関数を受け取る
  const xhr = new XMLHttpRequest();
  const URL = "https://api.github.com/users/morimorig3";
  xhr.open("GET", URL);
  xhr.send();
  xhr.onload = function () {
    response = xhr.response; // データを変数に格納する
    callback(); // コールバック関数を呼び出す
  };
}

function funcB() {
  console.log(JSON.parse(response));
}

funcA(funcB); // データを取得する関数の引数にコールバック関数を渡す

今度はうまくいきました。funcBをコールバック関数として、funcAに渡しました。 そうすることで、関数が意図した実行順序通りに動きました。

非同期処理と同期処理

次に、非同期処理ち同期処理のお話です。

コールバックの例で、データ取得は非同期に行われているという表現をしました。 データ取得のプログラムが非同期に実行されなければ、コールバックのようなややこしい仕組みは必要ありません。 では、なぜわざわざ非同期で処理が実行されているのでしょうか。

スマホで新しいゲームをしたい!という処理で説明してみます。(だいたいコンビニ店員の話なので変えてみました)

  1. アプリストアを開く
  2. お目当てのアプリを探す
  3. ダウンロードしてインストール(5分間必要)
  4. 5分間待つ
    1. 待機中にゲームの攻略情報を検索する (非同期)
    2. 待機中は画面を操作できない (同期)
  5. お目当てのアプリをプレイする

処理4で、2つの分岐をさせています。 ダウンロードとインストールを非同期で実行する → 1 ダウンロードとインストールを同期的に実行する → 2

ゲームのインストールに5分間スマホを操作できないなんて考えられないですよね。 プログラムの実行も同じで、データの取得に数秒でも、数ミリ秒でも待たされてしまうのは非常に効率が悪いのです。そのため、データの取得のような時間のかかる処理は非同期に実行されることが多いのです。

コールバック地獄

これで非同期の逐次処理をコールバックの7割は説明ができたはずです。

最後は逐次処理の部分を見ていきましょう。逐次処理とは関数を1つずつ順番に実行することです。

前述したようにコールバックを使うことで、関数を意図した順序で実行できます。 しかし、あのコード読みやすいでしょうか?関数の中で他の関数を参照している。正直言って、私はとても苦手で頭の理解が追いつきません。

さらに、実行順序を守る関数が2つだけならまだしも、3つ、4つと増えていったときはどうでしょう。そのように、コールバックが複数組み合わさったコードをコールバック地獄と言います。(英語圏ではコールバックヘルと言います。)

コールバック地獄になったコードは読みづらくなってしまいます。読みづらいコードは、理解に時間がかかるため、保守コストが高くなります。保守コストが高い割に、理解が難しいためミスも発生しまいます。誰も幸せになれません。

そこで、Promiseの登場です。Promiseを使用すれば、先ほどのコールバックを使用したコードがこんなにコンパクトになります。

fetch(URL)
  .then((response) => response.json())
  .then((json) => console.log(json));

一目瞭然です。18行のコードがたった3行になりました。 記述もメソッドチェーンを用いているため非常に明瞭です。データを取得してjsonに変換しているんだろうということが直感的にわかります。

これを理解するためには、Promiseオブジェクト、Promiseオブジェクトの状態・メソッド、Responseオブジェクト、Promiseのスタティックメソッドなど、盛りだくさんの内容を説明する必要があります。

今回はPromiseがなぜ必要になったか。ということが理解できれば100点です!

今回学んだこと

想像以上に長くなってしまいました。 そのため、Promiseの解説については、次回に譲りたいと思います。

フロントエンドでは、非同期処理は必要不可欠になっています。 そして、非同期処理ではコールバックという処理が必要になります。 Promiseは、それらの処理をきれいに書くことができる技術であるということがわかりました。

どんな技術にも言えることですが、ある技術の解決する課題が1つだけということはありません。もちろんPromiseも同じで、存在意義はこれ1つだけではありません。実装者によってバラバラに実装されていたコールバックの処理のルールの統一化など。

使い方だけではなく、その背景を学ぶことで応用力や他の解決方法を見つける力も身につくはずです。背景まで学ぶのは骨が折れる作業ですが、得られるものは多いですね。

参考