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

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

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

同期処理とは

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

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

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

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

非同期処理とは

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

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

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

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

コールバック関数

非同期処理には、「コールバック関数」「Promise」「Async/Await」など、さまざまな方法があります。

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

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

サンプル1

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’)」が先に実行されるということになります。

サンプル2

setTimeoutは、処理の順序を直感的に理解しやすいため、別の視点からもコールバック関数を見ていきましょう。

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();
      });
    });
  });
});

このように複数のコールバック関数を連結して、ネストの状態が続いていることを「コールバック地獄」と呼びます。

関数の実行結果を次々と新しいコールバック関数に利用していくことで、ネストが深くなっているのが分かります。

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

まとめ

今回はJavaScriptを扱うにあたって、非常に重要な概念である非同期処理について解説しました。
もっとも代表的な手法としてコールバック関数が使われています。

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

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

JavaScriptの関数をおさらい