導入
インターネット黎明期には、ウェブサイトはHTMLページ上の静的データで構成されることが多かった。しかし、ウェブアプリケーションがよりインタラクティブかつ動的になった現在では、APIデータを取得するための外部ネットワークリクエストなど、負荷の高い操作を実行する必要性が高まっている。JavaScriptでこれらの操作を実行するには、開発者は非同期プログラミング技術を用いる必要がある。.
JavaScriptはシングルスレッドのプログラミング言語であり、複数の操作を次々に処理する並行実行モデルを採用しているため、一度に1つのコマンドしか処理できません。しかし、APIからデータを要求するような操作は、要求するデータのサイズ、ネットワーク接続の速度、その他の要因に応じて、処理に要する時間が不確定になる場合があります。API呼び出しが同期的に行われると、ブラウザは操作が完了するまで、スクロールやボタンのクリックといったユーザー入力を処理できなくなります。これはブロッキングと呼ばれます。.
ブロッキング動作を回避するため、ブラウザ環境にはJavaScriptがアクセスできる多くの非同期Web APIが用意されています。これらのAPIは、他の操作と順番に実行するのではなく、並行して実行できます。これは、非同期操作の処理中もユーザーがブラウザを通常通り使用できるため便利です。.
イベントループ
このセクションでは、JavaScript がイベントループを使って非同期コードを処理する方法を説明します。まず、イベントループの動作をデモで確認し、次にイベントループの 2 つの要素であるスタックとキューについて説明します。.
非同期Web APIを一切使用しないJavaScriptコードは、同期的に、つまり一度に1つずつ順番に実行されます。これは、以下のサンプルコードで示されています。このコードでは、3つの関数が呼び出され、それぞれがコンソールに数値を表示します。
// Define three example functions
function first() {
console.log(1)
}
function second() {
console.log(2)
}
function third() {
console.log(3)
}このコードでは、console.log() を使用して数値を出力する 3 つの関数を定義します。.
次に、関数の呼び出しを記述します。
// Execute the functions
first()
second()
third()出力は、関数が呼び出された順序(first()、second()、three())に基づきます。
Output
1
2
3非同期Web APIを使用する場合、ルールはより複雑になります。試してみることができる組み込みAPIの1つは setTimeout です。これはタイマーを設定し、指定された時間後にアクションを実行します。setTimeout は非同期である必要があります。そうでない場合、待機中にブラウザ全体がフリーズし、ユーザーエクスペリエンスが低下します。.
非同期リクエストをシミュレートするには、2 番目の関数に setTimeout を追加します。
// Define three example functions, but one of them contains asynchronous code
function first() {
console.log(1)
}
function second() {
setTimeout(() => {
console.log(2)
}, 0)
}
function third() {
console.log(3)
}setTimeout は2つの引数を取ります。非同期に実行する関数と、その関数を呼び出すまでの待機時間です。このコードでは、console.log を匿名関数に格納し、setTimeout に渡して、0ミリ秒後に関数を実行するように設定しています。.
先ほどと同じように関数を呼び出します。
// Execute the functions
first()
second()
third()setTimeout を 0 に設定すれば、これら 3 つの関数を実行しても数値が順番に表示されると思われるかもしれません。しかし、これは非同期であるため、最後の関数で break が発生し、出力されます。
Output
1
3
2タイマーを0秒に設定するか5分に設定するかは関係ありません。非同期コードから呼び出される console.log は、トップレベルの同期関数の後に実行されます。これは、JavaScriptのホスト環境(この場合はブラウザ)が、同時発生または並列イベントを処理するためにイベントループと呼ばれる概念を使用しているためです。JavaScriptは一度に1つのコマンドしか実行できないため、特定のコマンドがいつ実行されているかを認識するためにイベントループが必要です。イベントループは、スタックとキューの概念を使用してこれを処理します。.
スタック
スタック、またはコールスタックは、現在実行中の関数の状態を保持します。スタックの概念に馴染みのない方のために説明すると、「後入れ先出し」(LIFO)特性を持つ配列と考えることができます。つまり、スタックの一番下からしか要素を追加または削除できません。JavaScriptは、スタック上の現在のフレーム(または特定の環境では関数呼び出し)を実行し、その後、それを削除して次のフレームに進みます。.
同期コードのみを含む例では、ブラウザは次の順序で実行されます。
- 初め() スタックに追加し、コンソールに 1 を記録する first() を実行し、スタックから first() を削除します。.
- 2番目() スタックに追加し、コンソールに 2 を出力する second() を実行し、スタックから second() を削除します。.
- 三番目 () スタックに追加し、コンソールに 3 を記録する third() を実行し、スタックから third() を削除します。.
setTimout を使用した 2 番目の例は次のとおりです。
- 初め() スタックに追加し、コンソールに 1 を記録する first() を実行し、スタックから first() を削除します。.
- 2番目() スタックに追加し、second() を実行します。.
- スタックに setTimeout() を追加し、タイマーを開始して匿名関数をキューに追加する Web API setTimeout() を実行し、スタックから setTimeout() を削除します。.
- 2番目() スタックから削除します。.
- 三番目 () スタックに追加し、コンソールに 3 を記録する third() を実行し、スタックから third() を削除します。.
- イベント ループは、キュー内の保留中のメッセージをチェックし、setTimeout() から匿名関数を見つけて、コンソールに 2 を記録する関数をスタックに追加し、スタックから削除します。.
setTimeout を使用すると、非同期 Web API でキューの概念が導入されます。これについては、このチュートリアルで後ほど説明します。.
列
キュー(メッセージキューまたはタスクキューとも呼ばれる)は、関数の待機領域です。コールスタックが空の場合、イベントループはキュー内の保留中のメッセージを最も古いものから順に確認します。保留中のメッセージが見つかった場合は、それをスタックに追加し、メッセージに含まれる関数を実行します。.
setTimeout の例では、タイマーが 0 秒に設定されているため、匿名関数はトップレベルの残りの実行の直後に実行されます。タイマーは、コードが正確に 0 秒または特定の時間に実行されることを意味するのではなく、その時間中に匿名関数をキューに追加することを意味することを覚えておくことが重要です。このキューシステムは、タイマーが期限切れ後に匿名関数を直接スタックに追加すると、現在実行中の関数が中断され、意図しない予測不可能な結果が生じる可能性があるために存在します。.
注: ジョブキューまたはマイクロタスクキューと呼ばれる、Promiseを処理する別のキューもあります。Promiseのようなマイクロタスクは、setTimeoutのようなマクロタスクよりも高い優先度で実行されます。.
イベントループがスタックとキューを使ってコード実行順序を制御する仕組みがわかったので、次はコード内で実行順序を制御する方法を理解しましょう。そのためには、まずイベントループが非同期コードを正しく処理するための主な方法、つまりコールバック関数について学びましょう。.
コールバック関数
setTimeout の例では、タイムアウトを指定した関数は、トップレベルの実行のメインコンテキストにあるすべての処理の後に実行されます。しかし、3番目の関数など、関数の1つがタイムアウト後に確実に実行されるようにしたい場合は、非同期コーディング手法を使用する必要があります。ここでのタイムアウトは、データを含む非同期API呼び出しを表すことができます。API呼び出しから取得したデータを処理するには、まずデータが返されることを確認する必要があります。.
この問題の主な解決策は、コールバック関数を使用することです。コールバック関数には特別な構文はありません。コールバック関数は、単に別の関数に引数として渡される関数です。別の関数を引数として受け取る関数は、高階関数と呼ばれます。この定義によれば、任意の関数は引数として渡されれば、呼び出される関数になることができます。電話の通話は本質的に非同期ではありませんが、非同期処理のために使用することができます。.
以下は、高階関数とコールバックの構文コード例です。
// A function
function fn() {
console.log('Just a function')
}
// A function that takes another function as an argument
function higherOrderFunction(callback) {
// When you call a function that is passed as an argument, it is referred to as a callback
callback()
}
// Passing a function
higherOrderFunction(fn)このコードでは、関数 fn を定義し、コールバック関数を引数として受け取る関数 aboveOrderFunction を定義し、fn をコールバックとして aboveOrderFunction に渡します。.
このコードを実行すると、次のことが実現されます。
Output
Just a functionsetTimeoutを使った1つ目、2つ目、3つ目の関数に戻りましょう。ここまでの内容は次のとおりです。
function first() {
console.log(1)
}
function second() {
setTimeout(() => {
console.log(2)
}, 0)
}
function third() {
console.log(3)
}課題は、3番目の関数の実行を、2番目の関数内の非同期アクションが完了するまで常に遅延させることです。ここでコールバックが役立ちます。1番目、2番目、3番目の関数を最上位レベルで実行する代わりに、3番目の関数を2番目の関数の引数として渡します。2番目の関数は、非同期アクションが完了した後にコールバックを実行します。.
コールバックで実装された 3 つの関数を次に示します。
// Define three functions
function first() {
console.log(1)
}
function second(callback) {
setTimeout(() => {
console.log(2)
// Execute the callback function
callback()
}, 0)
}
function third() {
console.log(3)
}次に、1 番目と 2 番目を実行し、3 番目を 2 番目の引数として渡します。
first()
second(third)このコード ブロックを実行すると、次の出力が得られます。
Output
1
2
3最初に 1 が出力され、タイマーが期限切れになった後 (この場合は 0 秒ですが、任意の値に変更できます)、2 が出力され、次に 3 が出力されます。非同期 Web API (setTimeout) が完了するまで動作します。.
ここで重要なのは、コールバック関数は非同期ではないということです。setTimeout は非同期タスクの処理を担当する非同期 Web API です。コールバックは、非同期タスクが完了したときに通知を受け取り、そのタスクの成功または失敗を処理することのみを可能にします。.
コールバックを使用して非同期タスクを実行する方法を学習したので、次のセクションでは、呼び出しをネストしすぎて「破滅のピラミッド」を作成することに関連する問題について説明します。.
ネストされたコールバックとピラミッドオブドゥーム
コールバック関数は、ある関数の実行を別の関数の実行が完了してデータを返すまで遅延させる効果的な方法です。しかし、コールバックはネスト構造を持つため、相互に依存する非同期リクエストが連続して多数ある場合、コードが複雑になりがちです。これは初期のJavaScript開発者にとって大きなストレスであり、ネストされた呼び出しを含むコードは「ピラミッド・オブ・トーメント」や「コールバック地獄」と呼ばれることが多かったのです。.
ネストされたコールバックのデモを次に示します。
function pyramidOfDoom() {
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
}, 500)
}, 2000)
}, 1000)
}このコードでは、新しい setTimeout がそれぞれ高階関数内に配置され、呼び出しの階層がどんどん深くなっていきます。このコードを実行すると、以下のようになります。
Output
1
2
3実際の非同期コードでは、これははるかに複雑になる可能性があります。非同期コードではエラー処理を行い、各レスポンスから次のリクエストにデータを渡す必要があるでしょう。これをコールバックで行うと、コードの可読性と保守性が損なわれます。.
以下は、実際に遊んでみることができる、より現実的な「運命のピラミッド」の実例です。
// Example asynchronous function
function asynchronousRequest(args, callback) {
// Throw an error if no arguments are passed
if (!args) {
return callback(new Error('Whoa! Something went wrong.'))
} else {
return setTimeout(
// Just adding in a random number so it seems like the contrived asynchronous function
// returned different data
() => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
500,
)
}
}
// Nested asynchronous requests
function callbackHell() {
asynchronousRequest('First', function first(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest('Second', function second(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest(null, function third(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
})
})
})
}
// Execute
callbackHell()このコードでは、各関数の可能な応答と可能なエラーを考慮する必要があるため、callbackHell 関数が視覚的にわかりにくくなります。.
このコードを実行すると、次の結果が得られます。
Output
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (<anonymous>:4:21)
at second (<anonymous>:29:7)
at <anonymous>:9:13結果
この非同期コード管理方法は複雑です。そのため、ES6ではPromiseの概念が導入されました。次のセクションでは、この点に焦点を当てます。.










コメント1件
とても素晴らしかったです。素晴らしいコンテンツをありがとうございます。