forEachでawaitが機能しない問題を解決する

forEachでawaitが機能しない問題を解決する

1. はじめに

数日前、プロジェクトでトラバーサルに使用したときに落とし穴に遭遇し、解決するのに 1 日かかりました。ここで覚えておいてください。

2. 問題

まず、非常に簡単なトピックを紹介します。配列が与えられたら、1 秒ごとにそれを出力します。ここでは、プロジェクトで始めたコードを貼り付けます。(もちろん、これはビジネスとはまったく関係ありません)

const _ = require('lodash');
定数エコー = 非同期 (i) => {
  タイムアウトを設定する(() => {
    コンソールにログ出力します。
  }, 5000);
}
arrs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]とします。
定数タスク = 非同期() => {
  _.forEach(arrs, 非同期(i) => {
    echo(i) を待機します。
  })
}
定数実行 = 非同期() => {
  console.log('run-start====>date:', 新しい Date().toLocaleDateString())
  タスク() を待機します。
  console.log('run-end====>date:', 新しい Date().toLocaleDateString())
}
(非同期() => {
  console.log('開始...')
  実行を待機します();
  console.log('終了...')
})()
// 始める...
// 実行開始====>日付: 2018-8-25
// 実行終了====>日付: 2018-8-25
// 終わり...
// 私 ===> 1
// i ===> 2
// i ===> 3
// i ===> 4
// i ===> 5
// 私 ===> 6
// i ===> 7
// i ===> 8
// i ===> 9

上記のコードと出力が与えられました。ここでのawaitが効果がないのは不思議です。最初は業務を追加したため、業務コードに問題があり、その後コードを抽出しましたが、それでも機能しませんでした。その時、私は本当にawaitを疑っていました。

最後に、質問に対する答えが示されます。

lodashのforEachと[].forEachはawaitをサポートしていません。トラバース中にawaitを実行する必要がある場合は、for-ofを使用できます。

正しいコードは次のとおりです。

const _ = require('lodash');
定数エコー = 非同期 (i) => {
  新しい Promise を返します ((resolve,reject)=>{
    タイムアウトを設定する(() => {
      console.log('i===>', i,new Date().toLocaleTimeString());
      解決する(i) ;
    }, 2000);
  })
}
arrs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]とします。
定数タスク = 非同期() => {
  // _.forEach(arrs, async (i) => {
  // echo(ji) を待機します。
  // })
  // arrs.forEach(async (i)=> {
  // echo(i) を待ちます。
  // });
  (定数 i の arrs) の場合 {
    echo(i) を待ちます。
  }
}
定数実行 = 非同期() => {
  console.log('run-start====>date:', 新しい Date().toLocaleDateString())
  タスク() を待機します。
  console.log('run-end====>date:', 新しい Date().toLocaleDateString())
}
(非同期() => {
  console.log('開始...')
  実行を待機します();
  console.log('終了...')
})()
// 出力開始...
実行開始====>日付: 2018-8-26
1 20:51:29
2 20:51:31
3 20:51:33
4 20:51:35
5 20:51:37
6 20:51:39 です
7 20:51:42 です
8 20:51:44 です
9 20:51:46 です
10 20:51:48
実行終了====>日付: 2018-8-26
終わり...

結論

問題を解決するときに、消去法を使用できる場合があります。たとえば、この例では、await メカニズムは問題ないはずです。問題がある場合、それをテストする順番は絶対にありません。したがって、残っている問題は、for トラバーサルの原因にしかなりません。

最初は lodash で実装していたので、lodash の forEach が await 処理をしていない(あるいは冗長にしている)のではないかと疑問に思ったかもしれません。このときは、別の方法を試すこともできます。一般的には、経験の問題です。

補足: forEach で async/await を使用する際に発生する問題

1. 問題の説明

数日前、プロジェクトで JavaScript の非同期問題が発生しました。

一連のデータがあり、それぞれを非同期で処理する必要があり、処理が同期されることが期待されます。

コードの説明は次のとおりです。

// データを生成する const getNumbers = () => {
 Promise.resolve([1, 2, 3]) を返す
}
// 非同期処理 const doMulti = num => {
 新しい Promise を返します ((resolve, reject) => {
  タイムアウトを設定する(() => {
   if (数値) {
    解決(数値 * 数値)
   } それ以外 {
    拒否(新しいエラー('数値が指定されていません'))
   }
  }, 2000)
 })
}
// メイン関数 const main = async () => {
 コンソールにログ出力します。
 定数数値 = [1, 2, 3];
 nums.forEach(非同期(x) => {
  const res = doMulti(x) を待機します。
  コンソールログ(res);
 });
 コンソールログ('終了');
};
// main() を実行します。

この例では、forEach は各数値を反復処理し、doMulti 操作を実行します。コード実行の結果は次のようになります。まず、start と end がすぐに印刷されます。 2秒後に1、4、9が同時に出力されます。

この結果は予想とは少し異なります。2秒ごとに非同期処理を実行し、1、4、9を順番に出力したいと考えています。したがって、現在のコードは並列に実行される必要がありますが、シリアルに実行されることが期待されます。

forEach ループを for ループに置き換えてみましょう。

定数main = 非同期() => {
 コンソールにログ出力します。
 const nums = getNumbers() を待機します。
 for (定数xの数値) {
  const res = doMulti(x) を待機します。
  コンソールログ(res);
 }
 コンソールログ('終了');
};

実行結果は予想どおりで、出力は start、1、4、9、end です。

2. 問題分析

考え方は同じですが、使用されるトラバーサル方法が異なります。なぜこのようなことが起こるのでしょうか? MDN で forEach のポリフィルを検索しました。MDN-Array.prototype.forEach() を参照してください。

// ECMA-262、第 5 版、15.4.4.18 の作成手順
// 参考: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {
 Array.prototype.forEach = function(コールバック、thisArg) {
  var T、k;
  もしこれがnullの場合
   throw new TypeError(' これは null または定義されていません');
  }
  // 1. OをtoObject()を呼び出して、
  // |this| 値を引数として渡します。
  var O = Object(これ);
  // 2. lenValueをGet()内部呼び出しの結果とする
  // 引数「length」を持つ O のメソッド。
  // 3. lenをtoUint32(lenValue)とします。
  var len = O.length >>> 0;
  // 4. isCallable(callback) が false の場合、TypeError 例外をスローします。 
  // 参照: http://es5.github.com/#x9.11
  if (typeof コールバック !== "function") {
   throw new TypeError(callback + ' は関数ではありません');
  }
  // 5. thisArgが指定された場合はTをthisArgとする。そうでない場合は
  // T は未定義です。
  引数の長さが1より大きい場合
   T = この引数;
  }
  // 6. kを0とする
  0 = 0;
  // 7. k < len の間繰り返します
  (k < 長さ) の間
   var kValue;
   // a. Pk を ToString(k) とします。
   // これは in 演算子の LHS オペランドに対して暗黙的に行われます
   // b. kPresentをHasPropertyの呼び出し結果とする
   // 引数 Pk を持つ O の内部メソッド。
   // このステップはcと組み合わせることができます
   // c. kPresentがtrueの場合、
   (k が O の場合) {
    // i. kValueをGet内部呼び出しの結果とする
    // 引数 Pk を持つ O のメソッド。
    k値 = O[k];
    // ii. T を引数としてコールバックの内部メソッド Call を呼び出す
    // kValue、k、および O を含む this 値と引数リスト。
    コールバック。呼び出し(T、kValue、k、O);
   }
   // d. k を 1 増やします。
   関数
  }
  // 8. undefined を返す
 };
}

上記のポリフィルの setp 7 から、次の手順を簡単に理解できます。

Array.prototype.forEach = 関数 (コールバック) {
 // これは配列を表します
 for (let index = 0; index < this.length; index++) {
  // 各エントリごとにコールバックを呼び出します
  コールバック(this[index], index, this);
 };
};

これは、この非同期関数を実行する for ループと同等であるため、並列で実行され、すべての出力結果が一度に生成されます (1、4、9)。

定数main = 非同期() => {
 コンソールにログ出力します。
 const nums = getNumbers() を待機します。
 // nums.forEach(async (x) => {
 // const res = await doMulti(x);
 // コンソールログ(res);
 // });
 for (let index = 0; index < nums.length; index++) {
  (非同期x => {
   定数 res = doMulti(x) を待機します
   コンソール.log(res)
  })(数値[インデックス])
 }
 コンソールログ('終了');
};

3. 解決策

今、私たちは問題を明確に分析しました。前のソリューションでは、forEach の代わりに for-of ループを使用しました。実際には、forEach を変更することもできます。

const asyncForEach = async (配列、コールバック) => {
 for (let index = 0; index < array.length; index++) {
  コールバックを待機します(配列[インデックス]、インデックス、配列);
 }
}
定数main = 非同期() => {
 コンソールにログ出力します。
 const nums = getNumbers() を待機します。
 asyncForEach(nums, async x => {を待つ
  定数 res = doMulti(x) を待機します
  コンソール.log(res)
 })
 コンソールログ('終了');
};
主要();

IV. Eslint の問題

この時点で、Eslint は別のエラー「no-await-in-loop」を報告しました。この点に関しては、Eslint の公式ドキュメント https://eslint.org/docs/rules/no-await-in-loop でも説明されています。

良い文章:

非同期関数 foo(things) {
 定数結果 = [];
 (const もののもの) {
  // 良い例: すべての非同期操作がすぐに開始されます。
  results.push(bar(thing));
 }
 // すべての非同期操作が実行されているので、ここですべてが完了するまで待機します。
 baz を返します(Promise.all(results) を待機します)。
}

悪い書き方:

非同期関数 foo(things) {
 定数結果 = [];
 for (const もののもの) {
  // 悪い例: 各ループの繰り返しは、非同期操作全体が完了するまで遅延されます
  results.push(await bar(thing));
 }
 baz(結果)を返します。
}

実は、上記の 2 つの書き方には良い、悪いという違いはありません。この 2 つの書き方の結果はまったく異なります。 Eslint が推奨する「良い書き方」では、非同期操作を実行するときに順序がありませんが、「悪い書き方」では順序があります。使用する具体的な書き方は、ビジネス ニーズに基づいて決定する必要があります。

したがって、ドキュメントの「使用しないべき場合」セクションでは、Eslint は、順次実行が必要な場合はこのルールを無効にできることも述べています。

多くの場合、ループの反復は実際には互いに独立していません。たとえば、1 つの反復の出力が別の反復の入力として使用されることがあります。または、ループを使用して、失敗した非同期操作を再試行することもできます。または、ループを使用して、コードが過剰な量のリクエストを並行して送信するのを防ぐこともできます。このような場合は、ループ内で await を使用するのが合理的であり、標準の ESLint 無効化コメントを使用してルールを無効にすることをお勧めします。

上記は私の個人的な経験です。参考になれば幸いです。また、123WORDPRESS.COM を応援していただければ幸いです。間違いや不備な点がありましたら、遠慮なくご指摘ください。

以下もご興味があるかもしれません:
  • JavaScript forEach における無効な戻り値の問題の解決
  • async/awaitを使用すると、非同期操作を同期的に実行できます。
  • mybatis バッチ更新 (update foreach) の失敗の問題を解決する
  • foreach バッチ挿入例外を使用して mybatis の問題を解決する

<<:  MySQLサービスが起動しても接続されない問題の解決策

>>:  Nginx を使用して DoNetCore を Alibaba Cloud にデプロイする方法

推薦する

vue3 コンポーネント通信方法の概要と例

vue3コンポーネントの通信モードは次のとおりです。小道具$放出$expose / 参照$属性vモデ...

トランジションコンポーネントのアニメーション効果を使用した Vue サンプルコード

トランジションドキュメントアドレスは、フェードインとフェードアウト効果を実現するための背景ポップアッ...

Vueの使用に関する深い理解

目次Vueのコアコンセプトを理解するVueの双方向バインディングの原理と実装を探るVue 双方向バイ...

MySQL における主キーが 0 であることと主キーの自己選択制約の関係についての詳しい説明 (詳細)

序文この記事は主にMySQLの主キー0と主キー自己排除制約の関係を紹介し、皆さんの参考と学習のために...

Linux での MySQL 8.0.25 のインストールと設定のチュートリアル

LinuxにMySQL 8.0.25をインストールするための最新のチュートリアルを参考にしてください...

CSS3 のカラー値 RGBA とグラデーションカラーの使用方法の紹介

CSS3以前は、グラデーション画像は背景画像としてのみ使用できました。 CSS3 のグラデーション構...

CSSスコープ(スタイル分割)の使用の概要

1. CSSスコープの使用(スタイル分割) Vue では、CSS スタイルを現在のコンポーネントでの...

CentOS8.0 で FTP サーバーをインストールして設定する方法

CentOS8.0-1905 のリリース後、FTP サーバーを CentOS の新しいバージョンに移...

HTMLタグを閉じるのを忘れないでください

Web 標準に準拠した Web ページの構築は、jb51.net が常に全員と議論しているトピックで...

Linux システム (Centos6.5 以上) のインストール JDK チュートリアル分析

記事の構成1. 準備2. Java JDK8.0をインストールする3. 環境変数を設定する3. イン...

MySQL 4.1/5.0/5.1/5.5/5.6の主な違い

バージョン間でのコマンドの違い: innodb ステータスを表示\G mysql-5.1 エンジン ...

Tomcatがセッションを管理する方法の例

ConcurrentHashMapを学習しましたが、どのように適用すればよいかわかりませんか? To...

Webフロントエンド開発エンジニアが習得すべきコアスキル

Web フロントエンド開発に含まれる内容は、主に W3C 標準の構造、動作、パフォーマンスです。では...

タブ切り替え効果を実現するJavaScript

この記事では、タブ切り替え効果を実現するためのJavaScriptの具体的なコードを参考までに紹介し...

CSS3 3Dクールキューブ変形アニメーションの実装

私はコーディングが大好きです。コーディングすると幸せになります!みなさんこんにちは、Counterで...