Skip to content

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

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

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

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

現在のコンシューマー向けコンピューターでは、すべてのプログラムは特定の時間枠で実行され、その後、別のプログラムが実行を継続できるように実行を停止します。この処理は非常に高速なサイクルで実行されるため、気づくことはできません。コンピューターは多くのプログラムを同時に実行しているように見えますが、これは錯覚です(マルチプロセッサーマシンを除く)。

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

ここでは内部構造について深く掘り下げませんが、プログラムが非同期であり、注意が必要になるまで実行を中断することは正常であり、コンピューターがその間に他のことを実行できることを覚えておいてください。プログラムがネットワークからの応答を待機している場合、リクエストが完了するまでプロセッサーを停止することはできません。

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

JavaScript

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

コード行は、次のように順番に実行されます。

js
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();

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

その答えは、その環境にありました。ブラウザは、この種の機能を処理できるAPIのセットを提供することで、それを行う方法を提供しています。

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

コールバック

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

js
document.getElementById('button').addEventListener('click', () => {
  // item clicked
});

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

コールバックは、別の関数に値として渡される単純な関数であり、イベントが発生したときにのみ実行されます。JavaScriptにはファーストクラス関数があるため、これを行うことができます。ファーストクラス関数は変数に割り当てられ、他の関数(高階関数と呼ばれる)に渡すことができます。

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

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

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

一般的な例の1つは、タイマーを使用することです。

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

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

js
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
  if (xhr.readyState === 4) {
    xhr.status === 200 ? console.log(xhr.responseText) : console.error('error');
  }
};
xhr.open('GET', 'https://yoursite.com');
xhr.send();

コールバックでのエラー処理

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

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

js
const fs = require('node:fs');
fs.readFile('/file.json', (err, data) => {
  if (err) {
    // handle error
    console.log(err);
    return;
  }
  // no errors, process data
  console.log(data);
});

コールバックの問題点

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

しかし、すべてのコールバックはネストのレベルを追加し、たくさんのコールバックがある場合、コードはすぐに複雑になり始めます:

js
window.addEventListener('load', () => {
  document.getElementById('button').addEventListener('click', () => {
    setTimeout(() => {
      items.forEach(item => {
        // ここにあなたのコード
      });
    }, 2000);
  });
});

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

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

コールバックの代替案

ES6以降、JavaScriptは、コールバックの使用を伴わない非同期コードに役立ついくつかの機能、Promises(ES6)およびAsync/Await(ES2017)を導入しました。