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 にデプロイする方法

推薦する

nginx+php-fpm サービスの HTTP ステータス コード 502 の詳細な分析

弊社の Web プロジェクトの 1 つでは、新しい都市の増加によりトラフィックと DB 負荷が増加し...

Nginx Httpモジュールシリーズにおけるautoindexモジュールの具体的な使用法

ブラウザ モジュールの主な機能は、http リクエスト ヘッダーの「User-Agent」の値とブラ...

Ubuntu16.04 インストール mysql5.7.22 グラフィックチュートリアル

VMware12.0+Ubuntu16.04+MySQL5.7.22 インストールチュートリアルの詳...

単一の MySQL テーブル内の行数が 500 万を超えてはいけないのはなぜですか?

今日は、興味深いトピックについてお話ししましょう。データベースとテーブルを分割することを検討する前に...

CSS を使用して 3 つのステップでショッピング モールのカード クーポンを作成する

今日は618日、主要なショッピングモールはすべてプロモーション活動を行っています。今日は、次のように...

WindowsでのNginxの起動や停止などの基本操作コマンドの詳しい説明

Windows で Nginx を使用するには、Nginx サービスの起動、停止、Nginx のリロ...

フレックスレイアウトは左のテキストオーバーフローを実現し、右のテキストの適応を省略します

テキストの長さに応じて、左側のテキストの幅を自動調整できる状況を実現したい。1行が表示できない場合、...

ページデザインにおけるテーブルとdivの適切な適用についての簡単な説明

この記事の冒頭で、以前書いた入門記事の間違いを訂正したいと思います。初心者を再び誤解させないように、...

CSS3の新しい背景プロパティの詳細な説明

これまで、CSS の背景の属性には、color、image、repeat、attachment、po...

CSSスタイルで実現されるHTML背景色のグラデーション効果

エフェクトのスクリーンショット:実装コード:コードをコピーコードは次のとおりです。 <!DOC...

APP (IOS、Android) を呼び出すモバイル H5 の記述例

iOS 1. URLスキームこのソリューションは基本的に、WeChat、QQ 組み込みブラウザ、QQ...

docker公式mysqlイメージのカスタム構成の詳細な説明

インストール時間を節約するために、公式の mysql docker イメージを使用して mysql ...

HTMLポップアップdivはモバイルの中央揃えを実現するのに非常に便利です

コードをコピーコードは次のとおりです。 <!DOCTYPE html PUBLIC "...

Dockerコマンドは一般ユーザーが実行できるように実装されている

dockerをインストールすると、通常はdockerユーザーグループが作成されます。ステップ2: 現...

Docker がデータベースのデプロイに適さない 7 つの理由のまとめ

Docker は過去 2 年間で非常に人気が高まっています。開発者はすべてのアプリケーションとソフト...