JavaScript の非同期プログラミングとコールバック

プログラミング言語における非同期性

コンピュータは設計上、非同期です。

非同期とは、メインプログラムのフローとは独立して物事が起こりうることを意味します。

現在の一般消費者向けコンピュータでは、すべてのプログラムは特定のタイムスロットで実行され、その後、他のプログラムが実行を続けられるように実行を停止します。この処理は非常に高速なサイクルで実行されるため、気づくことは不可能です。私たちはコンピュータが多くのプログラムを同時に実行していると考えていますが、これは(マルチプロセッサマシンを除き)幻想です。

プログラムは内部的に、システムの注意を引くためにプロセッサに送られる信号である割り込みを使用します。

ここではその内部の詳細には立ち入りませんが、プログラムが非同期であり、注意が必要になるまで実行を停止し、その間にコンピュータが他のことを実行できるようにすることは普通である、と心に留めておいてください。プログラムがネットワークからの応答を待っているとき、リクエストが完了するまでプロセッサを停止させることはできません。

通常、プログラミング言語は同期的であり、一部の言語は言語自体またはライブラリを通じて非同期性を管理する方法を提供しています。C、Java、C#、PHP、Go、Ruby、Swift、Python はすべてデフォルトで同期的です。これらのいくつかは、スレッドを使用して非同期操作を処理し、新しいプロセスを生成します。

JavaScript

JavaScript はデフォルトで同期的であり、シングルスレッドです。これは、コードが新しいスレッドを作成して並列実行できないことを意味します。

コードの行は、例えば以下のように、次々に直列で実行されます。

const  = 1;
const  = 2;
const  =  * ;
.();
doSomething();

しかし、JavaScript はブラウザ内で生まれました。当初のその主な仕事は、onClickonMouseOveronChangeonSubmit などのユーザーのアクションに応答することでした。同期的なプログラミングモデルで、これをどのようにして実現できたのでしょうか?

その答えは、その環境にありました。ブラウザは、この種の機能を処理できる一連の API を提供することで、これを実現する方法を提供します。

最近では、Node.js がこの概念をファイルアクセスやネットワーク呼び出しなどに拡張するために、ノンブロッキング I/O 環境を導入しました。

コールバック

ユーザーがいつボタンをクリックするかは予測できません。そこで、クリックイベントに対するイベントハンドラを定義します。このイベントハンドラは関数を受け入れ、イベントがトリガーされたときにその関数が呼び出されます。

.('button').('click', () => {
  // item clicked
});

これが、いわゆるコールバックです。

コールバックは、別の関数に値として渡される単純な関数であり、イベントが発生したときにのみ実行されます。これが可能なのは、JavaScript が第一級関数(first-class functions)を持っているためで、これにより関数を変数に代入したり、他の関数(高階関数と呼ばれる)に渡したりすることができます。

すべてのクライアントコードを window オブジェクトの load イベントリスナーでラップするのが一般的です。これにより、ページが準備できたときにのみコールバック関数が実行されます。

.('load', () => {
  // window loaded
  // do what you want
});

コールバックは DOM イベントだけでなく、あらゆる場所で使用されます。

一般的な例の一つは、タイマーの使用です。

(() => {
  // runs after 2 seconds
}, 2000);

XHR リクエストもコールバックを受け入れます。この例では、特定のイベントが発生したとき(この場合はリクエストの状態が変化したとき)に呼び出されるプロパティに関数を割り当てることで実現しています。

const  = new ();
. = () => {
  if (. === 4) {
    if (. === 200) {
      .(.);
    } else {
      .('error');
    }
  }
};
.('GET', 'https://yoursite.com');
.();

コールバックでのエラーハンドリング

コールバックでエラーをどのように処理するのでしょうか?非常に一般的な戦略は、Node.js が採用した方法、つまり、コールバック関数の最初のパラメータをエラーオブジェクトにする、エラーファーストコールバックです。

エラーがない場合、オブジェクトは null です。エラーがある場合、それにはエラーの説明やその他の情報が含まれます。

const  = ('node:fs');

.('/file.json', (, ) => {
  if () {
    // handle error
    .();
    return;
  }

  // no errors, process data
  .();
});

コールバックの問題点

コールバックは単純なケースには最適です!

しかし、コールバックはそれぞれがネストのレベルを追加するため、多くのコールバックがあるとコードは急速に複雑になります。

.('load', () => {
  .('button').('click', () => {
    (() => {
      items.forEach( => {
        // your code here
      });
    }, 2000);
  });
});

これは単なる4レベルのコードですが、私はもっと多くのネストレベルを見てきましたし、それは楽しいものではありません。

これをどう解決するのでしょうか?

コールバックの代替手段

ES6 以降、JavaScript はコールバックを使用しない非同期コードを扱うのに役立ついくつかの機能を導入しました:Promise (ES6) と Async/Await (ES2017) です。