Michi's Tech Blog

一人前のWebエンジニアを目指して

JavaScriptの非同期処理についてまとめてみた①

こんにちは!
スマレジ テックファームのMichiです!

よくエンジニアの会話の中で、

JavaScriptは非同期処理やからな~」

というような会話が聞こえてきます。それに対して、

ワイ「いや~、そうっすよね~(適当)」

っていう返事を数カ月前まではしていたのですが、最近になってようやくちゃんとした意味がわかってきました。
というわけで、今回はJavaScriptの非同期通信についてまとめてみます。

非同期とは?

そもそも非同期(Asynchronous)とは何でしょうか? MDNのドキュメントには、こうあります。

2つ以上の事象が同時に発生したり、関連する複数の事象が互いの完了を待たずに発生したりする概念を指します。

要するに、

  • 複数のものごとが同時に発生するよ
  • それらは相手の反応を待たないで、それぞれで勝手に行動するよ

ってことですね。
そしてWeb開発の世界では、この「非同期」という言葉は2つの文脈で使われます。

1. プログラミングとしての非同期処理

通常のプログラミング言語PythonPHP)は同期処理です。上から下に書かれたプログラムは順番に実行され、途中の処理を飛ばして次の処理へ行くことはありません。
一方でJavaScriptでは、一部の関数において非同期処理を行います。つまり、前に書かれた処理の完了を待たずに、次のプログラムの処理を進めるわけです。

2. サーバーとの非同期通信(Ajax

AjaxAsynchronous JavaScript and XML)は、JavaScriptを使用してクライアント ⇔ サーバー間の非同期通信を行う手法です。
通常のWebアプリケーションでは、クライアントからリクエストを送信すると、サーバーからレスポンスが返却されるまで、クライアント側は待機する必要がありました。これを同期通信といいます。
これに対し、AjaxではJavaScriptを使用して非同期通信を行い、サーバーからのレスポンスを待たずとも、HTMLの一部を更新したり、ユーザーから画面への入力を受け付けることができるようになりました。この技術はSPA*1において活用されています。


この記事では、1. の概念と具体的なコードについて取り上げます。2. ついては前回の記事『バニラJSでAjaxを実装してみよう!』をご覧ください。

JavaScriptの非同期処理を見てみよう

setTimeout関数

では、実際にJavaScriptで非同期処理を行うコードを見てみましょう。
この手の例でよく挙げられるのが、setTimeout関数を利用した非同期処理です。

setTimeout(func, dur)
    func: 実行される処理(コールバック関数)  dur: 待機時間(ミリ秒)

setTimeout関数は、指定の時間だけ待機した後に処理を実行する関数です。第一引数にコールバック関数*2として実行したい処理、第二引数に待機時間を取ります。

setTimeout(() => console.log(5), 1000);

これであれば、1000ミリ秒(1秒)の待機後に、コンソールに5の値が表示されます。
これを利用して、「5、4、3、2、1、0」のように1秒ずつカウントダウンする処理を作ってみます。

console.log('開始')
setTimeout(() => console.log(5), 1000);
setTimeout(() => console.log(4), 1000);
setTimeout(() => console.log(3), 1000);
setTimeout(() => console.log(2), 1000);
setTimeout(() => console.log(1), 1000);
setTimeout(() => console.log(0), 1000);

よし、できた!実行してみましょう。

分かりやすいように、実行時間も一緒に出力されるようにしています

あれ、おかしいですね。「開始」の1秒後に、5~0までのカウントダウンが一斉に実行されています。
これこそがJavaScriptの非同期処理です。

どういうことかと言うと、最初のsetTimeout(2行目)に渡した関数は1秒後に実行されますが、その1秒間を待たずに次のsetTimeout(3行目)が実行されてしまいます。そして、3行目のsetTimeoutの1秒間を待たずに4行目が…というように、それぞれのsetTimeoutが前の関数の処理終了を待たずに実行されてしまうことによって、5~0のカウントダウンはすべて同じタイミングで実行されてしまうのです。

コールバック地獄

では、1秒ごとにカウントダウンしたい場合はどうすればよいのでしょうか?
もっとも簡単な方法は、コールバック関数の中に次に実行したい処理を書いてしまうことです。例を見てみましょう。

console.log('開始')
setTimeout(() => {
    console.log(5);
    setTimeout(() => {
        console.log(4);
    }, 1000);
}, 1000);

ちゃんと1秒ごとに実行されていますね。
この調子で、残りのカウントダウンも実装してみましょう。

console.log('開始')
setTimeout(() => {
    console.log(5);
    setTimeout(() => {
        console.log(4);
        setTimeout(() => {
            console.log(3);
            setTimeout(() => {
                console.log(2);
                setTimeout(() => {
                    console.log(1);
                    setTimeout(() => {
                        console.log(0);
                    }, 1000);
                }, 1000);
            }, 1000);
        }, 1000);
    }, 1000);
}, 1000);

うん、見るからにヤバそうなコードが出来上がりました笑
このように、コールバック関数が連発することによって、ネストが深くなりすぎるコードのことをコールバック地獄と呼びます(ふざけた名前やけど、ホンマです)。

解決策

では、このコールバック地獄を回避する方法はないのでしょうか?
あります。それが、Promiseasync/awaitなのですが、説明が長くなってきたので、続きは次週にします。

ここまで読んでいただきありがとうございました。

*1:Single Page Application。単一のページで、Webアプリケーションを構成する設計構造のこと。

*2:別の関数から呼び出してもらうための関数