この章では、JavaScriptにおける非同期プログラミングの基礎について説明します。
このセクションでは、JavaScriptにおける非同期プログラミングに関する内容のロードマップを提供します。
詳細を心配しないでください!
まだすべてを理解していなくても心配しないでください。これは、これから何が起こるのかを簡単に覗き見しただけです。
通常の関数は同期です。呼び出し側は、呼び出し先が計算を完了するまで待ちます。A行のdivideSync()
は同期関数呼び出しです。
function main() {
try {
const result = divideSync(12, 3); // (A)
.equal(result, 4);
assertcatch (err) {
} .fail(err);
assert
} }
デフォルトでは、JavaScriptのタスクは、単一のプロセスで順次実行される関数です。これは次のようになります。
while (true) {
const task = taskQueue.dequeue();
task(); // run task
}
このループは、マウスのクリックなどのイベントがタスクをキューに追加するため、イベントループとも呼ばれます。
この協調マルチタスクのスタイルにより、タスクが、たとえばサーバーからの結果を待っている間、他のタスクの実行をブロックすることは望ましくありません。次のサブセクションでは、このケースを処理する方法について説明します。
divide()
が結果を計算するためにサーバーを必要とする場合はどうなるでしょうか?その場合、結果は異なる方法で配信される必要があります。呼び出し側は、結果が準備できるまで(同期的に)待つ必要はなく、準備ができたら(非同期的に)通知される必要があります。結果を非同期的に配信する方法の1つは、divide()
に、呼び出し側に通知するために使用するコールバック関数を渡すことです。
function main() {
divideCallback(12, 3,
, result) => {
(errif (err) {
.fail(err);
assertelse {
} .equal(result, 4);
assert
};
}) }
非同期関数呼び出しがある場合
divideCallback(x, y, callback)
次のステップが発生します。
divideCallback()
はサーバーにリクエストを送信します。main()
が終了し、他のタスクを実行できます。エラーerr
:その後、次のタスクがキューに追加されます。
.enqueue(() => callback(err)); taskQueue
result
値:その後、次のタスクがキューに追加されます。
.enqueue(() => callback(null, result)); taskQueue
Promiseは2つのものです。
Promiseベースの関数の呼び出しは次のようになります。
function main() {
dividePromise(12, 3)
.then(result => assert.equal(result, 4))
.catch(err => assert.fail(err));
}
async関数をPromiseベースのコードのより良い構文として見なすことができます。
async function main() {
try {
const result = await dividePromise(12, 3); // (A)
.equal(result, 4);
assertcatch (err) {
} .fail(err);
assert
} }
A行で呼び出しているdividePromise()
は、前のセクションと同じPromiseベースの関数です。ただし、呼び出しを処理するための同期的な構文があります。await
は、特別な種類の関数であるasync関数内でのみ使用できます(キーワードfunction
の前のキーワードasync
に注意してください)。await
は現在のasync関数を一時停止し、そこから戻ります。待機された結果の準備ができると、関数の実行は中断された場所から続行されます。
関数が別の関数を呼び出すたびに、後者の関数が終了した後、どこに戻るかを覚えておく必要があります。これは通常、スタック(コールスタック)を介して行われます。呼び出し側は戻る場所をプッシュし、呼び出し先はそれが完了した後、その場所にジャンプします。
これは、いくつかの呼び出しが発生する例です。
function h(z) {
const error = new Error();
console.log(error.stack);
}
function g(y) {
h(y + 1);
}
function f(x) {
g(x + 1);
}
f(3);
// done
最初に、このコードを実行する前は、コールスタックは空です。11行目の関数呼び出しf(3)
の後、スタックには1つのエントリがあります。
9行目の関数呼び出しg(x + 1)
の後、スタックには2つのエントリがあります。
f()
内の場所)6行目の関数呼び出しh(y + 1)
の後、スタックには3つのエントリがあります。
g()
内の場所)f()
内の場所)3行目のerror
のロギングは、次の出力を生成します。
DEBUG
Error:
at h (file://demos/async-js/stack_trace.mjs:2:17)
at g (file://demos/async-js/stack_trace.mjs:6:3)
at f (file://demos/async-js/stack_trace.mjs:9:3)
at file://demos/async-js/stack_trace.mjs:11:1
これは、Error
オブジェクトが作成された場所のいわゆるスタックトレースです。戻り先ではなく、呼び出しが行われた場所を記録していることに注意してください。2行目で例外を作成することも、別の呼び出しです。そのため、スタックトレースにはh()
内の場所が含まれています。
3行目の後、各関数は終了し、そのたびに、トップエントリがコールスタックから削除されます。関数f
が完了すると、トップレベルスコープに戻り、スタックは空になります。コードフラグメントが終了すると、それは暗黙的なreturn
のようになります。コードフラグメントが実行されるタスクであると考えると、空のコールスタックで戻ることはタスクを終了します。
デフォルトでは、JavaScriptはWebブラウザとNode.jsの両方で、単一のプロセスで実行されます。いわゆるイベントループは、そのプロセス内でタスク(コードの一部)を順次実行します。イベントループを図21に示します。
2つの当事者がタスクキューにアクセスします。
タスクソースは、キューにタスクを追加します。これらのソースの一部は、JavaScriptプロセスと並行して実行されます。たとえば、1つのタスクソースはユーザーインターフェースイベントを処理します。ユーザーがどこかをクリックし、クリックリスナーが登録されている場合、そのリスナーの呼び出しがタスクキューに追加されます。
イベントループは、JavaScriptプロセス内で継続的に実行されます。各ループ反復中に、キューから1つのタスクを取り出し(キューが空の場合は、空になるまで待ちます)、それを実行します。そのタスクは、コールスタックが空でreturn
がある場合に完了します。制御はイベントループに戻り、イベントループはキューから次のタスクを取得して実行します。以降も同様です。
次のJavaScriptコードは、イベントループの近似値です。
while (true) {
const task = taskQueue.dequeue();
task(); // run task
}
ブラウザのユーザーインターフェースメカニズムの多くも(タスクとして)JavaScriptプロセスで実行されます。したがって、実行時間の長いJavaScriptコードは、ユーザーインターフェースをブロックする可能性があります。それを実証するWebページを見てみましょう。そのページを試すには、2つの方法があります。
demos/async-js/blocking.html
次のHTMLは、ページのユーザーインターフェースです。
<a id="block" href="">Block</a>
<div id="statusMessage"></div>
<button>Click me!</button>
アイデアは、「ブロック」をクリックすると、実行時間の長いループがJavaScriptを介して実行されることです。そのループ中、ブラウザ/JavaScriptプロセスがブロックされているため、ボタンをクリックできません。
JavaScriptコードの簡略化されたバージョンは次のようになります。
document.getElementById('block')
.addEventListener('click', doBlock); // (A)
function doBlock(event) {
// ···
displayStatus('Blocking...');
// ···
sleep(5000); // (B)
displayStatus('Done');
}
function sleep(milliseconds) {
const start = Date.now();
while ((Date.now() - start) < milliseconds);
}function displayStatus(status) {
document.getElementById('statusMessage')
.textContent = status;
}
これらはコードの重要な部分です。
block
のHTML要素がクリックされるたびに、doBlock()
を呼び出すようにブラウザに指示します。doBlock()
はステータス情報を表示し、次にsleep()
を呼び出して、JavaScriptプロセスを5000ミリ秒間ブロックします(B行)。sleep()
は、十分な時間が経過するまでループすることで、JavaScriptプロセスをブロックします。displayStatus()
は、IDがstatusMessage
の<div>
内にステータスメッセージを表示します。実行時間の長い操作がブラウザをブロックしないようにするための方法はいくつかあります。
操作は結果を非同期的に配信できます。ダウンロードなど、一部の操作はJavaScriptプロセスと並行して実行できます。そのような操作をトリガーするJavaScriptコードは、操作が完了すると結果とともに呼び出されるコールバックを登録します。呼び出しは、タスクキューを介して処理されます。この結果を配信するスタイルは、呼び出し側が結果の準備ができるまで待たないため、非同期と呼ばれます。通常の関数呼び出しは、結果を同期的に配信します。
別のプロセスで長い計算を実行します。これは、いわゆるWeb Workersを介して行うことができます。Web Workersは、メインプロセスと並行して実行されるヘビー級プロセスです。それぞれに独自のランタイム環境(グローバル変数など)があります。これらは完全に分離されており、メッセージパッシングを介して通信する必要があります。詳細については、MDN Webドキュメントを参照してください。
長い計算中に休憩を取ります。次のサブセクションで説明します。
次のグローバル関数は、パラメーターcallback
をms
ミリ秒の遅延後に実行します(型シグネチャは簡略化されています。setTimeout()
にはより多くの機能があります)。
function setTimeout(callback: () => void, ms: number): any
関数は、次のグローバル関数を介してタイムアウトをクリア(コールバックの実行をキャンセル)するために使用できるハンドル(ID)を返します。
function clearTimeout(handle?: any): void
setTimeout()
は、ブラウザとNode.jsの両方で使用できます。次のサブセクションで、その動作を示します。
setTimeout()
はタスクに休憩を与える
setTimeout()
の別の見方として、現在のタスクが休憩し、後でコールバックを介して処理を続けるという考え方があります。
JavaScript はタスクに関して保証をします。
各タスクは、次のタスクが実行される前に必ず終了します(「完了まで実行」)。
その結果、タスクは作業中にデータが変更されること(同時変更)を心配する必要がありません。これにより、JavaScript でのプログラミングが簡略化されます。
次の例は、この保証を示しています。
console.log('start');
setTimeout(() => {
console.log('callback');
, 0);
}console.log('end');
// Output:
// 'start'
// 'end'
// 'callback'
setTimeout()
は、そのパラメータをタスクキューに入れます。したがって、パラメータは現在のコード(タスク)が完全に終了した後に実行されます。
パラメータ ms
は、タスクがキューに入れられるタイミングのみを指定し、正確な実行タイミングを指定するものではありません。キューに先に終了しないタスクがある場合など、実行されないことさえあります。前のコードで、パラメータ ms
が 0
であるにもかかわらず、'callback'
の前に 'end'
がログに記録されるのはそのためです。
長時間実行される操作が完了するのを待つ間、メインプロセスがブロックされるのを避けるために、JavaScript では結果が非同期で配信されることがよくあります。これを行うための3つの一般的なパターンは次のとおりです。
最初の2つのパターンは次の2つのサブセクションで説明します。Promise は次の章で説明します。
パターンとしてのイベントは、次のように機能します。
このパターンの複数のバリエーションが JavaScript の世界に存在します。次に3つの例を見ていきます。
IndexedDB は、Web ブラウザに組み込まれているデータベースです。以下はその使用例です。
const openRequest = indexedDB.open('MyDatabase', 1); // (A)
.onsuccess = (event) => {
openRequestconst db = event.target.result;
// ···
;
}
.onerror = (error) => {
openRequestconsole.error(error);
; }
indexedDB
には、操作を呼び出すための珍しい方法があります。
各操作には、リクエストオブジェクトを作成するための関連メソッドがあります。たとえば、A 行では、操作は「open」、メソッドは .open()
、リクエストオブジェクトは openRequest
です。
操作のパラメータは、メソッドのパラメータを介してではなく、リクエストオブジェクトを介して提供されます。たとえば、イベントリスナー(関数)は、プロパティ .onsuccess
および .onerror
に格納されます。
操作の呼び出しは、メソッド(A 行)を介してタスクキューに追加されます。つまり、操作の呼び出しがすでにキューに追加された後に、操作を構成します。ここでは、完了まで実行されるセマンティクスだけが、競合状態から私たちを救い、現在のコードフラグメントが終了した後に操作が実行されることを保証します。
XMLHttpRequest
XMLHttpRequest
API を使用すると、Web ブラウザ内からダウンロードを行うことができます。これは、ファイル http://example.com/textfile.txt
をダウンロードする方法です。
const xhr = new XMLHttpRequest(); // (A)
.open('GET', 'http://example.com/textfile.txt'); // (B)
xhr.onload = () => { // (C)
xhrif (xhr.status == 200) {
processData(xhr.responseText);
else {
} .fail(new Error(xhr.statusText));
assert
};
}.onerror = () => { // (D)
xhr.fail(new Error('Network error'));
assert;
}.send(); // (E)
xhr
function processData(str) {
.equal(str, 'Content of textfile.txt\n');
assert }
この API では、最初にリクエストオブジェクトを作成し(A 行)、次にそれを構成し、次にアクティブ化します(E 行)。構成は次のとおりです。
GET
、POST
、PUT
など。xhr
を介して配信されることに注意してください。(私は、この種の入力データと出力データの混在は好きではありません。)DOM イベントの動作については、§39.4.1「ブラウザのユーザーインターフェースがブロックされる可能性がある」ですでに説明しました。次のコードも click
イベントを処理します。
const element = document.getElementById('my-link'); // (A)
.addEventListener('click', clickListener); // (B)
element
function clickListener(event) {
event.preventDefault(); // (C)
console.log(event.shiftKey); // (D)
}
最初に、ブラウザに ID が 'my-link'
の HTML 要素を取得するように要求します(A 行)。次に、すべての click
イベントのリスナーを追加します(B 行)。リスナーでは、まずブラウザにデフォルトのアクション(リンクのターゲットに移動すること)を実行しないように指示します(C 行)。次に、シフトキーが現在押されているかどうかをコンソールに記録します(D 行)。
コールバックは、非同期の結果を処理するための別のパターンです。これらは1回限りの結果にのみ使用され、イベントよりも冗長性が低いという利点があります。
例として、テキストファイルを読み込み、その内容を非同期で返す関数 readFile()
を考えてみましょう。以下は、Node.js スタイルのコールバックを使用する場合の readFile()
の呼び出し方法です。
readFile('some-file.txt', {encoding: 'utf8'},
, data) => {
(errorif (error) {
.fail(error);
assertreturn;
}.equal(data, 'The content of some-file.txt\n');
assert; })
成功と失敗の両方を処理する単一のコールバックがあります。最初のパラメータが null
でない場合はエラーが発生しました。それ以外の場合は、2 番目のパラメータに結果が見つかります。
練習問題:コールバックベースのコード
以下の演習では、非同期コードのテストを使用しますが、これは同期コードのテストとは異なります。詳細については、§10.3.2「Mochaでの非同期テスト」を参照してください。
exercises/async-js/read_file_cb_exrc.mjs
.map()
のコールバックベースのバージョンの実装:exercises/async-js/map_cb_test.mjs
ブラウザまたは Node.js のどちらでも、多くの場合、選択の余地はなく、非同期コードを使用する必要があります。この章では、そのようなコードが使用できるいくつかのパターンを見てきました。それらすべてに2つの欠点があります。
最初の欠点は、Promises(次の章で説明)で軽減され、async 関数(次の次の章で説明)でほとんど解消されます。
残念ながら、非同期コードの感染性はなくなりません。ただし、async 関数を使用すると同期と非同期の切り替えが簡単になるという事実によって緩和されます。