非同期JavaScript:コールバック地獄から非同期と待機まで

公開: 2022-03-11

成功するWebアプリケーションを作成するための鍵の1つは、ページごとに数十のAJAX呼び出しを行えることです。

これは典型的な非同期プログラミングの課題であり、非同期呼び出しの処理方法を選択すると、大部分はアプリが作成または中断され、ひいてはスタートアップ全体が失敗する可能性があります。

JavaScriptで非同期タスクを同期することは、非常に長い間深刻な問題でした。

この課題は、JavaScriptフレームワークを使用するフロントエンド開発者と同様に、Node.jsを使用するバックエンド開発者にも影響を及ぼします。 非同期プログラミングは私たちの日常業務の一部ですが、多くの場合、課題は軽視され、適切なタイミングで考慮されていません。

非同期JavaScriptの簡単な歴史

最初の最も簡単な解決策は、コールバックとしての入れ子関数の形で提供されました。 この解決策は、コールバック地獄と呼ばれるものにつながりました、そして、あまりにも多くのアプリケーションはまだそれのやけどを感じます。

次に、 Promisesを取得しました。 このパターンにより、コードがはるかに読みやすくなりましたが、Do n't Repeat Yourself(DRY)の原則とはかけ離れていました。 アプリケーションのフローを適切に管理するために同じコードを繰り返さなければならないケースはまだ多すぎました。 async / awaitステートメントの形式での最新の追加により、JavaScriptの非同期コードは、他のコードと同じように読み取りと書き込みが簡単になりました。

これらの各ソリューションの例を見て、JavaScriptでの非同期プログラミングの進化について考えてみましょう。

これを行うために、次の手順を実行する簡単なタスクを調べます。

  1. ユーザーのユーザー名とパスワードを確認します。
  2. ユーザーのアプリケーションロールを取得します。
  3. ユーザーのアプリケーションアクセス時間をログに記録します。

アプローチ1:コールバック地獄(「運命のピラミッド」)

これらの呼び出しを同期するための古代の解決策は、ネストされたコールバックを介したものでした。 これは単純な非同期JavaScriptタスクにとってはまともなアプローチでしたが、コールバック地獄と呼ばれる問題のために拡張できませんでした。

イラスト:非同期JavaScriptコールバック地獄のアンチパターン

3つの単純なタスクのコードは次のようになります。

 const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };

各関数は、前のアクションの応答であるパラメーターで呼び出される別の関数である引数を取得します。

あまりにも多くの人が上の文章を読むだけで脳の凍結を経験するでしょう。 何百もの同様のコードブロックを含むアプリケーションがあると、コードを自分で作成したとしても、コードを保守する人にとってさらに問題が発生します。

database.getRolesがネストされたコールバックを持つ別の関数であることに気付くと、この例はさらに複雑になります。

 const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };

保守が難しいコードがあることに加えて、この場合、DRYの原則にはまったく価値がありません。 たとえば、エラー処理は各関数で繰り返され、メインのコールバックはネストされた各関数から呼び出されます。

非同期呼び出しのループなど、より複雑な非同期JavaScript操作は、さらに大きな課題です。 実際、コールバックを使用してこれを行う簡単な方法はありません。 これが、BluebirdやQなどのJavaScriptPromiseライブラリが非常に注目を集めた理由です。 これらは、言語自体がまだ提供していない非同期要求に対して一般的な操作を実行する方法を提供します。

そこで、ネイティブJavaScriptPromisesが登場します。

JavaScriptの約束

約束は、コールバック地獄を脱出するための次の論理的なステップでした。 このメソッドはコールバックの使用を削除しませんでしたが、関数のチェーンを簡単にし、コードを単純化して、読みやすくしました。

イラスト:非同期JavaScriptPromises図

Promisesを配置すると、非同期JavaScriptの例のコードは次のようになります。

 const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo => dataBase.getRoles(userInfo)) .then(rolesInfo => dataBase.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch((err) => { //do whatever the error handler needs }); };

この種の単純さを実現するには、例で使用されているすべての関数をPromisifiedにする必要があります。 getRolesメソッドが更新されてPromiseが返される方法を見てみましょう。

 const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };

2つのコールバックを使用してPromiseを返すようにメソッドを変更し、 Promise自体がメソッドからアクションを実行します。 これで、 resolveコールバックとrejectコールバックがそれぞれPromise.catch Promise.thenにマップされます。

getRolesメソッドは、まだ内部的に破滅現象のピラミッドになりやすいことに気付くかもしれません。 これは、データベースメソッドがPromiseを返さないため、データベースメソッドが作成される方法によるものです。 データベースアクセスメソッドもPromiseを返した場合、 getRolesメソッドは次のようになります。

 const getRoles = new function (userInfo) { return new Promise((resolve, reject) => { database.connect() .then((connection) => connection.query('get roles sql')) .then((result) => resolve(result)) .catch(reject) }); };

アプローチ3:非同期/待機

運命のピラミッドは、Promisesの導入によって大幅に軽減されました。 ただし、 Promise.thenメソッドと.catchメソッドに渡されるコールバックに依存する必要がありました。

Promisesは、JavaScriptの最もクールな改善の1つへの道を開きました。 ECMAScript 2017は、JavaScriptのPromisesにasyncおよびawaitステートメントの形式で構文糖衣構文を導入しました。

このコードサンプルが示すように、 Promiseベースのコードを同期的であるかのように記述できますが、メインスレッドをブロックすることはありません。

 const verifyUser = async function(username, password){ try { const userInfo = await dataBase.verifyUser(username, password); const rolesInfo = await dataBase.getRoles(userInfo); const logStatus = await dataBase.logAccess(userInfo); return userInfo; }catch (e){ //handle errors as needed } };

Promiseが解決するのを待つことは、 async関数内でのみ許可されます。つまり、 verifyUserasync functionを使用して定義する必要がありました。

ただし、この小さな変更が行われると、他の方法で追加の変更を行うことなく、 Promiseawaitことができます。

非同期-約束の待望の解決

非同期関数は、JavaScriptの非同期プログラミングの進化における次の論理的なステップです。 これらにより、コードがよりクリーンになり、保守が容易になります。 関数をasyncとして宣言すると、常にPromiseが返されるため、心配する必要はありません。

なぜ今日JavaScript async関数を使い始めるべきですか?

  1. 結果のコードははるかにクリーンです。
  2. エラー処理ははるかに簡単で、他の同期コードと同じようにtry / catchに依存しています。
  3. デバッグははるかに簡単です。 .thenブロック内にブレークポイントを設定すると、同期コードをステップ実行するだけなので、次の.thenに移動しません。 ただし、同期呼び出しであるかのように、 await呼び出しをステップスルーできます。