Hell async / awaitからの脱出

最近では、JavaScriptのasync / awaitコンストラクトは、コールバック地獄を取り除くのに最適な方法のように見えました。 しかし、async / awaitの不注意な使用は、新しい地獄の出現をもたらしました。


本日私たちが翻訳した資料の著者は、地獄が非同期/待ち状態であるものと、それから逃れる方法について話します。

hell async / awaitとは


非同期JavaScript機能を使用する場合、多くの場合、プログラマーは多くの関数呼び出しで構成される構造を使用します。各関数呼び出しの前にはawaitキーワードが付きます。 このような場合、 await式は互いに独立しawaitことが多いため、このようなコードはパフォーマンスの問題につながります。 ここでのポイントは、システムが次の機能を実行する前に、前の機能の完了を待つ必要があるということです。 これは地獄の非同期/待ちです。

例:ピザと飲み物を注文する


ピザや飲み物を注文するためのスクリプトを書く必要があると想像してください。 このスクリプトは次のようになります。

 (async () => { const pizzaData = await getPizzaData()    //   const drinkData = await getDrinkData()    //   const chosenPizza = choosePizza()    //   const chosenDrink = chooseDrink()    //   await addPizzaToCart(chosenPizza)    //   await addDrinkToCart(chosenDrink)    //   orderItems()    //   })() 

一見、このスクリプトは非常に正常に見えますが、期待どおりに機能します。 ただし、このコードを慎重に検討すると、非同期コード実行の機能を考慮していないため、実装が不完全であることがわかります。 ここで何が間違っているかを扱ったので、このスクリプトの問題を解決できます。

コードは非同期の即時起動関数式( IIFE )でラップされます。 すべてのタスクは、コードにリストされている正確な順序で実行されることに注意してください。次のタスクに進むには、前のタスクが完了するまで待つ必要があります。 つまり、これがここで発生することです。

  1. ピザの種類のリストを取得します。
  2. 飲み物のリストを取得します。
  3. リストからのピザの選択。
  4. リストから飲み物を選択します。
  5. 選択したピザをバスケットに追加します。
  6. 選択した飲み物をバスケットに追加します。
  7. チェックアウト。

上記の強調点は、スクリプト内の操作が厳密に順番に実行されることです。 コードの並列実行を利用しません。 次のことを考えてみましょう:飲み物のリストのダウンロードを開始するために、ピザの種類のリストを受け取るのはなぜですか? これらのタスクを同時に実行する必要があります。 ただし、リストからピザを選択できるようにするには、まずピザの種類のリストが読み込まれるのを待つ必要があります。 同じことは、飲み物を選択するプロセスにも当てはまります。

その結果、ピザに関連するタスクと飲み物に関連するタスクを並行して実行できるが、ピザにのみ関連する(または飲み物のみに関連する)個別の操作は連続して実行する必要があると結論付けることができます。

例:バスケットの内容に基づいて注文する


バスケットの内容に関するデータがダウンロードされ、注文を形成するためにリクエストが送信されるコードの例を次に示します。

 async function orderItems() { const items = await getCartItems()    //   const noOfItems = items.length for(var i = 0; i < noOfItems; i++) {   await sendRequest(items[i])    //   } } 

この場合、 forループは、次の反復に進むために、 sendRequest()関数への各呼び出しの完了を待機する必要があります。 ただし、この期待は実際には必要ありません。 すべてのリクエストを可能な限り迅速に完了し、完了を待ちます。
これで、あなたは地獄の非同期/待機の本質と、それがアプリケーションのパフォーマンスにどの程度影響するかを理解できるようになりました。 次に、次のセクションのタイトルの質問について考えます。

awaitキーワードの使用を忘れたらどうしますか?


非同期関数を呼び出すときにawaitキーワードの使用を忘れると、関数の実行が開始されます。 このような関数は、後で使用できるpromiseを返します。

 (async () => { const value = doSomeAsyncTask() console.log(value) //   })() 

待機なしで非同期関数を呼び出すことの別の結果は、コンパイラーがプログラマーが関数が完了するまで待つことを望んでいることを知らないことです。 その結果、コンパイラは非同期タスクを完了することなくプログラムを終了します。 したがって、 awaitキーワードが必要な場所を忘れないでください。

プロミスには興味深い特性があります。1行のコードでプロミスを取得でき、別のコードでは-その解決を待ちます。 この事実は、地獄の非同期/待ちから逃れるための鍵です。

 (async () => { const promise = doSomeAsyncTask() const value = await promise console.log(value) //   })() 

ご覧のとおり、 doSomeAsyncTask()呼び出すとdoSomeAsyncTask()が返されます。 この時点で、この関数は実行を開始します。 約束を解決した結果を得るために、 awaitキーワードを使用しawait 、次のコード行をすぐに実行してはならないことをシステムに伝えます。 代わりに、Promiseが解決するまで待ってから、次の行に進む必要があります。

どうすれば地獄の非同期/待機から抜け出すことができますか?


地獄の非同期/待機から抜け出すには、次のアクションプランを使用できます。

▍1。 他の式の実行に依存する式を見つける


最初の例では、ピザと飲み物を選択するスクリプトを示しました。 リストからピザを選択する前に、ピザの種類のリストをダウンロードする必要があると判断しました。 そして、ピザをバスケットに追加する前に、それを選択する必要があります。 その結果、これらの3つのステップは互いに依存していると言えます。 前のステップを完了することなく、次のステップに進むことはできません。

さて、もっと広く考えて飲み物について考えると、ピザを選択するプロセスは飲み物を選択するプロセスに依存しないため、これら2つのタスクを並列化できることがわかります。 コンピューターは並列タスクで非常にうまく機能します。

その結果、どの特定の表現が互いに依存しており、どの表現が依存していないのかがわかりました。

▍2。 個別の非同期関数のグループ依存式


すでにわかったように、ピザの選択プロセスは、ピザの種類のリストをロードし、特定のピザを選択してバスケットに追加するといういくつかのステップで構成されています。 これらのアクションは、別個の非同期関数にアセンブルする必要があります。 同様の一連のアクションも飲み物の特性であることを忘れずに、 selectPizza()およびselectDrink()と呼ばれる2つの非同期関数にselectDrink()ます。

▍3。 受信した非同期関数を並行して実行する


次に、JavaScriptイベントループの機能を使用して、受信した非同期関数の非ブロッキングの並列実行を整理します。 ここでは2つの一般的なパターンが適用されますPromise.all()早期復帰とPromise.all()メソッドです。

エラー処理


async / await hellを取り除くために、上記の3つのステップを実行します。 上記の例を修正してください。 これは、最初のものが今どのように見えるかです。

 async function selectPizza() { const pizzaData = await getPizzaData()    //   const chosenPizza = choosePizza()    //   await addPizzaToCart(chosenPizza)    //   } async function selectDrink() { const drinkData = await getDrinkData()    //   const chosenDrink = chooseDrink()    //   await addDrinkToCart(chosenDrink)    //   } (async () => { const pizzaPromise = selectPizza() const drinkPromise = selectDrink() await pizzaPromise await drinkPromise orderItems()    //   })() //    ,   ,      (async () => { Promise.all([selectPizza(), selectDrink()]).then(orderItems)   //   })() 

これで、ピザと飲み物に関連する式がselectPizza()およびselectDrink()関数にグループ化されました。 次のコマンドは前のコマンドの結果に依存するため、これらの関数内では、コマンドの実行順序が重要です。 関数が準備された後、非同期で呼び出します。

2番目の例では、未知の数の約束を処理する必要があります。 ただし、この問題の解決は非常に簡単です。 つまり、配列を作成し、その中にpromiseを配置する必要があります。 次に、 Promise.all()を使用して、これらすべての約束が解決するのを待つように手配できます。

 async function orderItems() { const items = await getCartItems()    //   const noOfItems = items.length const promises = [] for(var i = 0; i < noOfItems; i++) {   const orderPromise = sendRequest(items[i])    //     promises.push(orderPromise)    //   } await Promise.all(promises)    //   } 

まとめ


ご覧のとおり、「非同期/待機地獄」と呼ばれるものは一見かなりまともですが、外部の幸福の背後にはパフォーマンスへの悪影響があります。 しかし、この地獄から逃げるのはそれほど難しくありません。 コードを分析し、その助けを借りて解決されたタスクを並列化できるかを調べ、プログラムに必要な変更を加えるだけで十分です。

親愛なる読者! あなたは地獄の非同期/待ち合わせを見たことがありますか?

Source: https://habr.com/ru/post/J353658/


All Articles