プログラミングを始めたばかりの場合は、おそらく、プログラムを一連の連続したロジック ブロックとして考えているでしょう。各ブロックは特定の処理を実行し、次のブロックが実行できるようにその結果を渡します。ほとんどの場合、おっしゃるとおりで、ほとんどのプログラムは逐次的に実行されます。このモデルを使用すると、作成と保守が簡単なプログラムを構築できます。ただし、この逐次モデルが機能しない、または最適ではない特定の使用例があります。例として、書籍リーダー アプリケーションを考えてみましょう。このアプリケーションには、出現する単語をすべて検索したり、ブックマーク間を移動したりするなど、いくつかの高度な機能があります。ここで、ユーザーが現在長い本を読んでいて、「The」などの一般的な単語が出現する箇所をすべて探すことにしたとします。アプリケーションは通常、その単語が出現するすべての箇所を検索してインデックスを作成するのに数秒かかります。逐次プログラムでは、検索操作が完了するまで、ユーザーはアプリケーションを操作 (ページの変更やテキストの強調表示) できません。それが最適なユーザー エクスペリエンスではないことがおわかりいただけたでしょうか。
この図は、書籍リーダー アプリケーションの一般的な実行フローを示しています。ユーザーが長時間実行される操作 (この場合は、大きな書籍内で「the」が出現するすべての検索) を開始すると、アプリケーションはその操作の継続中ずっと「フリーズ」します。この場合、ユーザーは検索操作が完了するまで結果が表示されずに次のブックマーク ボタンをクリックし続けることになり、すべての操作が一度に有効になり、エンド ユーザーはアプリケーションが遅れているように感じます。
この例は、以前に紹介した逐次モデルに実際には対応していないことに気づいたかもしれません。これは、ここでの操作が互いに独立しているためです。ユーザーは次のブックマークに移動するために「the」の出現回数を知る必要がないため、操作の実行順序はそれほど重要ではありません。次のブックマークに移動する前に、検索操作の終了を待つ必要はありません。以前の実行フローに対する改善の可能性は、次のロジックに基づいています。バックグラウンドで長い検索操作を実行し、受信操作を続行し、長い操作が完了したら、ユーザーに通知するだけです。実行の流れは以下のようになります。
この実行フローにより、ユーザー エクスペリエンスが大幅に向上します。これで、ユーザーは長時間実行操作を開始し、アプリケーションの通常の使用を続行し、操作が完了したら通知を受け取ることができるようになりました。これが非同期プログラミングの基礎です。
他の言語の中でも特に Javascript は、考えられるほぼすべての非同期動作を実現するための広範な API を提供することで、このスタイルの非同期プログラミングをサポートしています。結局のところ、JavaScript は本質的に非同期言語である必要があります。前の例を参照すると、非同期ロジックはすべてのユーザー インタラクション アプリケーションの基礎であり、JavaScript は主に、ほとんどのプログラムがユーザー アクションに応答するブラウザ上で使用されるように構築されました。
以下に、非同期 JavaScript に関する簡単なガイドを示します。
コールバック
典型的なプログラムには、通常、多数の関数があります。関数を使用するには、一連のパラメータを指定して関数を呼び出します。関数コードが実行されて結果が返されますが、何も異常はありません。非同期プログラミングでは、このロジックがわずかに変わります。書籍リーダー アプリケーションの例に戻りますが、検索操作には未知の時間がかかるため、通常の関数を使用して検索操作ロジックを実装することはできません。通常の関数は基本的に操作が完了する前に戻りますが、これは私たちが期待する動作ではありません。解決策は、検索操作の完了後に実行される別の関数を指定することです。これは、プログラムが通常どおりフローを続行でき、検索操作が完了すると、指定された関数が実行されてユーザーに検索結果を通知するためのユースケースをモデル化しています。この関数はコールバック関数と呼ばれるものです。
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
まず、検索操作関数 searchOccurrences を定義します。検索する単語と、検索操作が完了した後に実行する関数となる 2 番目のパラメーター「callback」を受け取ります。検索操作関数は意図的に抽象化されています。考えられる 2 つの結果のみに注目する必要があります。最初のケースは、すべてが成功し、結果変数に検索結果が入っている場合です。この場合、次のパラメータを指定してコールバック関数を呼び出すだけです。最初のパラメータはエラーが発生していないことを意味する null、2 番目のパラメータは検索された単語、3 番目のパラメータのうちおそらく最も重要なパラメータです。 、検索操作の結果です。
2 番目のケースは、エラーが発生した場合です。これは、検索操作の実行が完了し、コールバック関数を呼び出す必要がある場合でもあります。 try および catch ブロックを使用してエラーをインターセプトし、catch ブロックからのエラー オブジェクトを使用してコールバック関数を呼び出すだけです。
次に、コールバック関数 handleSearchOccurrences を定義し、そのロジックを非常に単純に保ちました。メッセージをコンソールに出力するだけです。まず、「err」パラメータをチェックして、main 関数でエラーが発生したかどうかを確認します。この場合、検索操作がエラーで終了したことをユーザーに通知するだけです。エラーが発生しなかった場合は、検索操作の結果を示すメッセージが出力されます。
最後に、単語「the」を使用して searchOccurrences 関数を呼び出します。関数はメイン プログラムをブロックすることなく通常どおり実行され、検索が完了するとコールバックが実行され、検索結果またはエラー メッセージを含む結果メッセージが取得されます。
ここで重要なのは、メイン関数とコールバック関数内の結果変数にのみアクセスできるということです。次のようなことを試してみると:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
プログラムは searchOccurrences 関数の実行を待機しないため、出力結果は不定になります。 main 関数内で結果変数が割り当てられる前に、次の命令である print ステートメントに移動します。その結果、割り当てられていない結果変数が出力されます。
したがって、このロジックに基づいて、結果変数を使用するすべてのコードをコールバック関数内に保持する必要があります。これは今は問題に見えないかもしれませんが、すぐに本当の問題に発展する可能性があります。順番に実行する必要がある非同期関数のチェーンがある場合を想像してください。典型的なコールバック ロジックでは、次のようなものを実装します。
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
各コールバックにはエラー パラメータがあり、各エラーは個別に処理する必要があることに注意してください。これにより、上記のすでに複雑なコードがさらに複雑になり、保守が難しくなります。 Promise がコールバック地獄の問題を解決してくれることを願っています。それについては次に説明します。
約束
Promise はコールバックの上に構築され、同様の方法で動作します。これらは、コールバック地獄などのコールバックに関するいくつかの明らかな問題を解決するために、ES6 機能の一部として導入されました。 Promise は、正常に完了した場合 (解決)、およびエラーが発生した場合 (拒否) に実行される独自の関数を提供します。以下は、Promise を使用して実装された searchOccurrences の例を示しています。
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
適用した変更を見てみましょう。
searchOccurrences 関数は Promise を返します。 Promise 内では同じロジックを保持しています。つまり、成功した実行とエラーが発生した実行の両方を処理する 1 つのコールバック関数ではなく、コールバックを表す 2 つの関数solve と request を使用しています。 Promise は 2 つの結果を分離し、main 関数を呼び出すときに明確な構文を提供します。解決関数は、「then」キーワードを使用して main 関数に「フック」されます。ここでは、resolve 関数の 2 つのパラメーターを指定して、検索結果を出力するだけです。同様のことが拒否関数にも当てはまり、「catch」キーワードを使用してフックできます。コードの可読性とクリーンさの点で Promise が提供する利点を理解していただければ幸いです。まだ議論している場合は、非同期関数を連鎖させて次々に実行することで、コールバック地獄の問題を解決する方法を確認してください。
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
非同期/待機
Async/Await は、JavaScript の非同期ツールベルトに新しく追加されたものです。 ES8 で導入されたこれらは、非同期操作の実行を単に「待機」することにより、非同期関数の上に新しい抽象化レイヤーを提供します。プログラムのフローは、非同期操作から結果が返されるまでその命令でブロックされ、その後プログラムは次の命令に進みます。同期実行フローについて考えているのであれば、それは正しいです。一周してきました! Async/await は、同期プログラミングの単純さを非同期の世界にもたらすことを試みます。これはプログラムの実行とコード内でのみ認識されることに注意してください。内部的にはすべて同じままで、Async/await は依然として Promise を使用しており、コールバックはその構成要素です。
例を見て、Async/await を使用して実装してみましょう。
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
コードはあまり変更されていません。ここで注目すべき重要な点は、searchOccurrences 関数宣言の前にある「async」キーワードです。これは、関数が非同期であることを示します。また、searchOccurrences 関数を呼び出すときは、「await」キーワードに注意してください。これは、プログラムが次の命令に進む前に、結果が返されるまで関数の実行を待機するようにプログラムに指示します。つまり、結果変数には常に searchOccurrences 関数の戻り値が保持され、次の約束は保持されません。その意味では、Async/Await 関数には Promise のような保留状態がありません。実行が完了したら、print ステートメントに移動します。今回の結果には、実際に検索操作の結果が含まれています。予想どおり、新しいコードは同期している場合と同じ動作をします。
覚えておくべきもう 1 つの小さな点は、コールバック関数がなくなったため、同じ関数内で searchOccurrences エラーを処理する必要があることです。エラーをコールバック関数に伝播してそこで処理することはできないためです。ここでは例のために、エラーが発生した場合にエラー メッセージを出力しているだけです。
## まとめ
この記事では、JavaScript で非同期ロジックを実装するために使用されるさまざまなアプローチについて説明しました。私たちは、なぜ通常の同期スタイルのプログラミングから非同期モデルに移行する必要があるのか、具体的な例を検討することから始めました。次に、非同期 JavaScript の主要な構成要素であるコールバックに移りました。コールバックの制限により、これらの制限を克服するために長年にわたって追加されたさまざまな代替手段、主に Promise と Async/await が誕生しました。非同期ロジックは、外部 API の呼び出し、データベース クエリの開始、ローカル ファイル システムへの書き込み、ログイン フォームでのユーザー入力の待機など、Web 上のあらゆる場所で見つけることができます。クリーンで保守可能な非同期 Javascript を作成することで、より自信を持ってこれらの問題に取り組むことができれば幸いです。
この記事が気に入ったら、CLA ブログ をチェックしてください。そこでは、テクノロジー業界に入る方法に関するさまざまなテーマについて議論しています。また、以前の無料ワークショップについては、youtube チャンネル をチェックし、ソーシャル メディア でフォローしてください。 -labs-academy/) なので、今後のイベントをお見逃しなく!