JavaScript非同期プログラミングとコールバック
プログラミング言語における非同期性
コンピュータは設計上、非同期です。
非同期とは、メインプログラムの流れとは独立して処理が行われることを意味します。
現在の一般消費者向けコンピュータでは、各プログラムは特定の時間枠で実行され、その後実行を停止して他のプログラムの実行を許可します。この動作は非常に高速なサイクルで実行されるため、気付くことは不可能です。私たちはコンピュータが多くのプログラムを同時に実行していると認識していますが、これは錯覚です(マルチプロセッサマシンを除く)。
プログラムは内部的に割り込みを使用します。割り込みは、システムの注意を引くためにプロセッサに送出される信号です。
ここでは内部構造については深く触れませんが、プログラムが非同期であり、注意が必要になるまで実行を停止し、その間にコンピュータが他の処理を実行することを許可するのは正常であることを覚えておいてください。プログラムがネットワークからの応答を待機している場合、リクエストが完了するまでプロセッサを停止することはできません。
通常、プログラミング言語は同期式であり、一部の言語は言語内またはライブラリを通じて非同期性を管理する方法を提供します。C、Java、C#、PHP、Go、Ruby、Swift、Pythonはすべてデフォルトで同期式です。それらのいくつかは、新しいプロセスを生成するスレッドを使用して非同期操作を処理します。
JavaScript
JavaScriptはデフォルトで同期式であり、シングルスレッドです。つまり、コードは新しいスレッドを作成して並列実行することはできません。
コード行は、次のように順番に実行されます。
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();
しかし、JavaScriptはブラウザ内で誕生しました。当初の主な役割は、onClick
、onMouseOver
、onChange
、onSubmit
などのユーザーアクションに応答することでした。同期プログラミングモデルでどのようにこれを行うことができるでしょうか?
答えは、その実行環境にあります。ブラウザは、この種の機能を処理できる一連のAPIを提供することで、これを可能にします。
最近では、Node.jsがノンブロッキングI/O環境を導入し、この概念をファイルアクセス、ネットワークコールなどに拡張しました。
コールバック
ユーザーがいつボタンをクリックするかは分かりません。そのため、クリックイベントのイベントハンドラーを定義します。このイベントハンドラーは、イベントが発生したときに呼び出される関数を引数として受け取ります。
document.getElementById('button').addEventListener('click', () => {
// アイテムがクリックされた
});
これがいわゆるコールバックです。
コールバックは、別の関数に値として渡される単純な関数であり、イベントが発生したときだけ実行されます。JavaScriptは、変数に代入したり、他の関数(高階関数と呼ばれる)に渡したりできる一等関数を持つため、これが可能です。
クライアントコード全体をwindowオブジェクトのloadイベントリスナーでラップすることが一般的です。これは、ページの準備ができたときだけコールバック関数を実行します。
window.addEventListener('load', () => {
// ウィンドウが読み込まれた
// 必要な処理を行う
});
コールバックは、DOMイベントだけでなく、あらゆる場所で利用されます。
一般的な例として、タイマーの使用があります。
setTimeout(() => {
// 2秒後に実行される
}, 2000);
XHRリクエストもコールバックを受け付けます。この例では、特定のイベント(この場合はリクエストの状態の変化)が発生したときに呼び出されるプロパティに関数を割り当てています。
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です。エラーがある場合、エラーの説明とその他の情報が含まれています。
const fs = require('node:fs');
fs.readFile('/file.json', (err, data) => {
if (err) {
// エラー処理
console.log(err);
return;
}
// エラーなし、データ処理
console.log(data);
});
コールバックの問題点
コールバックは単純なケースには最適です!
しかし、コールバックを追加するたびにネストのレベルが増え、多くのコールバックがあると、コードはすぐに複雑になります。
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// your code here
});
}, 2000);
});
});
これはわずか4レベルの単純なコードですが、もっと多くのネストレベルを見たことがあり、楽しくありません。
どうすれば解決できるでしょうか?
コールバックの代替手段
ES6以降、JavaScriptは、コールバックを使用しない非同期コードを支援するいくつかの機能を導入しました。Promises
(ES6)とAsync/Await
(ES2017)です。