多くのプログラミング言語は、上から順番にプログラムを実行します。
しかし、例えば、データベースにアクセスしてデータを取得している間に、他の処理を実行するという場合があります。マルチスレッドタイプのプログラミング言語では、このような場合にも複数の処理を同時に行うことが可能です。

一方、JavaScriptは、一度に一つしか処理を行うことができないシングルスレッドタイプの言語に該当します。ここでJavaScriptを扱う中で重要になってくるのが、非同期処理という概念です。

今回は、非同期処理の概念から、非同期処理を行うためのコールバック関数について解説していきます。

同期処理

まず、非同期処理の前に、同期処理についてかんたんに説明します。

同期処理は、上から順番に処理を実行し、一つの処理が終わったら次の処理へ移動します。
言い換えると、一つの処理が終わるまで次の処理を行うことができません。

処理1 (はじめの処理)
↓
処理2(1番目の処理が終わったら実行)
↓
処理3(2番目の処理が終わったら実行)

同期処理は、プログラムの基本的な考え方であり、処理全体を把握しやすいといったメリットがあります。
また同時に、プログラムによってはタスク完了までに時間を要するため、ユーザーにとってストレスになる側面もあります。

非同期処理

JavaScriptでは、同時に複数の処理を行うことができない代わりに、時間のかかる処理が完了するまでの間、他の処理を進めていくことができます。これを非同期処理と言います。

非同期処理は、書かれているコードとは異なる順番で処理を実行します。

処理1 (はじめの処理 → 完了)
↓
処理2(時間のかかる処理 → 待機します)
↓
処理3(2番目の処理を待っている間に実行 → 完了)
↓
処理2(準備完了 → 処理を完了させるよ)

処理に時間がかかるプログラムが含まれている場合には、非同期処理は非常に効率的です。

コールバック関数

非同期処理には「コールバック関数」「Promise」「Async/Await」など、さまざまな方法がありますが、ここでは、非同期処理のもっとも基本的な手法であるコールバック関数について取り上げます。

コールバック関数とは、ある関数の引数として渡される関数のことです。
ある関数が一定の処理を終えたら呼び出されるため、コールバック関数と呼ばれています。

setTimeout関数

JavaScriptの代表的な非同期処理のコールバック関数として、setTimeout関数があります。

console.log('1');

setTimeout(() => {
  console.log('2')
}, 1000);

console.log('3');

// 1
// 3
// 2

上記のコードでは、console.log('1')setTimeout()console.log('3')の順番に処理が登録されます。ここまでは同期処理と同じです。
しかし、setTime関数で登録したコールバック関数は、1000ミリ秒後に処理を実行させるように、非同期的なタイミングで呼び出されます。
そのため、setTimeout()よりも後に書かれているconsole.log('3')が先に実行されるということになります。

関数の引数に指定

次は、他の関数の引数に関数を指定する方法を見てみましょう。

function getWeather(weather) {
  console.log('今日の天気は' + weather);
}

function tellWeather(data) {
  let tellSomething = '晴れです';
  data(tellSomething);
}

tellWeather(getWeather);
// 今日の天気は晴れです

一番下のコードを見ると、tellWeather関数の引数にgetWeather関数を指定していることが確認できます。このgetWeatherがコールバック関数に当たります。
tellWeather関数の処理が終えたら、getWeather関数が呼び出される仕組みです。

コールバック関数は、必ずしも関数宣言で書く必要はなく、人によっては無名関数やアロー関数の方が分かりすい場合もあります。
上記のサンプルをアロー関数に書き換えるとこのようになります。

function tellWeather(data) {
  let tellSomething = '晴れです';
  data(tellSomething);
}

tellWeather(weather => console.log('今日の天気は' + weather));
// 今日の天気は晴れです

普段から何気なく使っていたコールバック関数は、効率的に処理を行うための非同期処理として作用されていることが確認できました。

コールバック地獄

コールバック関数は、かんたんなプログラムであれば直感的に理解がしやすいのですが、複数の非同期処理を行う必要がある場合、把握しづらくなるデメリットがあります。

次のコードを見てください。
関数の呼び出し時に、sample_1()の結果を使ってsample_2()を呼び出し、さらにその結果を使ってsample_3()を呼び出す…のように処理がネストの状態になっています。

function sample_1() {
  処理;
}

function sample_2() {
  処理;
}

function sample_3() {
  処理;
}

function sample_4() {
  処理;
}

function sample_5() {
  処理;
}

// 呼び出し
sample_1(function () {
  sample_2(function () {
    sample_3(function() {
      sample_4(function() {
        sample_5();
      });
    });
  });
});

このように複数のコールバック関数を連結して、ネストの状態が続いていることをコールバック地獄と呼びます。
関数の実行結果を次々と新しいコールバック関数に利用していくことで、ネストが深くなっているのが分かります。

そのため、コールバック関数は複雑なプログラムになるにつれて、どのような処理をしているのか把握しにくくなる点に注意が必要です。

まとめ

今回は、非同期処理について解説しました。

もっとも代表的な手法としてコールバック関数が使われています。
ES6以前のJavaScriptでは、非同期処理のためのコールバック関数を使う方法しかありませんでしたが、現在はPromiseやAsync/Awaitという手法が採用され、コールバック地獄の心配もなくなりました。

これらの手法を扱っていくうえで、コールバック関数の非同期処理の仕組みを理解することはとても重要です。ぜひ、参考にしてください。

合わせて読みたい非同期処理シリーズ

第1回:非同期処理とコールバック関数(当記事)
第2回:Promise -then・catch
第3回:Promise -finally・Promise.all
第4回:Async/Await