Blog#113: なぜあなたのコードが動かないのか? JavaScriptで「async/await」と「forEach」を使う真実

image.png

こんにちは、私はTuanと申します。東京からフルスタックWeb開発者です。 将来の有用で面白い記事を見逃さないように、私のブログをフォローしてください。

async function processData() {
  const data = [1, 2, 3, 4, 5];
  data.forEach(async (num) => {
    await doSomething(num);
  });
}

このコードがうまくいかない理由を調べましょう。「async/await」と「forEach」をJavaScriptで使うということについて、間違った情報を聞いたことがありますか? 私たちのチームのメンバーが最近そう聞いて、なぜそうなのか気になりました。この記事では、この話題をもっと深く見て、なぜこれが一般的な誤解なのかを説明します。

問題

次のコードを見てみましょう:

async function processData() {
  const data = [1, 2, 3, 4, 5];
  data.forEach(async (num) => {
    await doSomething(num);
  });
}

このコードがうまくいかない理由を調べましょう。「async/await」と「forEach」をJavaScriptで使うことはできないの?

async function testAsync(v) {
  await new Promise((resolve) => {
    setTimeout(resolve, 100);
  });
  return v + 1;
}

const data = [];
const params = [0, 1, 2];
params.forEach(async (v) => {
  const res = await testAsync(v);
  console.log(res);
  data.push(res);
});
console.log(data);

このコードでは、値を受け取って100ms後にその値に1を足した非同期関数「testAsync」があります。次に空の配列「data」と数字の配列「params」があります。「forEach」メソッドを使って「params」をループして、それぞれの値に「testAsync」を呼び出します。そして結果をログに出力し、「data」配列にプッシュします。期待する結果は「data」配列が[1, 2, 3]になるということですが、実際は空の配列です。

ここで混乱が始まります。「forEach」のコールバック内で「await」を使うことで、非同期関数が完了するまで次のイテレーションに進むことを待つという考えですが、そうではありません。

この誤解

このコードを書いている人が「await」の仕組みを誤解している問題があります。「await」は「async」関数の中でしか使えません。上の例では、「forEach」に渡されたコールバック関数に「async」を使用していますが、外側の関数に「async」を使うとは違います。「forEach」に渡されたコールバック関数は「async」関数ではないので、「await」は意図した通りに機能しません。

これが誤解の元です。多くの人が「forEach」に渡されたコールバック関数に「async」を使うことで、その関数を非同期関数にしていると思っていますが、実際はそうではありません。コールバック関数はまだ同期関数なので、その中で「await」は機能しません。

答えは?

この問題を解決するために何をすればいいですか?解決策の1つは、「forEach」の代わりに「for」ループを使うことです。この場合、「await」が正しく機能します。

for (const v of params) {
  const res = await testAsync(v);
  console.log(res);
  data.push(res);
}
console.log(data);

このコードは、[1, 2, 3]という期待される結果を得ることができます。

別の解決策として、「map」メソッドを使って「Promise.all」を使って、すべてのプロミスが完了するのを待つことができます。

const data = await Promise.all(params.map(async (v) => {
  const res = await testAsync(v);
  return res;
}));
console.log(data);

これも[1, 2, 3]という期待される結果を得ることができます。

結論

結論として、JavaScriptで「async/await」と「forEach」を使うのは一般的な誤解です。問題は、人々が「await」がどのように機能するかを誤解していて、「async」を「forEach」に渡されたコールバック関数に使うことでその関数を非同期にしていると考えていることです。しかし、コールバック関数はまだ同期的であり、「await」はその中では機能しません。この問題を解決するために、通常の「for」ループまたは「Promise.all」を使用した「map」メソッドを使用することができます。この記事があなたの混乱を解消し、JavaScriptで「async/await」と「forEach」がどのように機能するかをよりよく理解するのに役立つことを願っています。

「async/await」と「forEach」を使うのは間違い! 「for」ループや「Promise.all」を使って、全部完了するまで待つことができるよ!

ボーナス

ボーナスとして、JavaScriptで「async/await」と「forEach」をもっと直感的に使う方法の例をご紹介します。

Array.prototype.forEachAsync = async function(callback, thisArg) {
  const promises = [];
  this.forEach(function(...args) {
    promises.push(callback.call(this, ...args));
  }, thisArg);
  return await Promise.all(promises);
}
async function sampleAsync(v) {
  await new Promise((resolve) => {
    setTimeout(resolve, 1000);
  });
  return v + 1;
}

const data = [];
const params = [0, 1, 2];
await params.forEachAsync(async (v) => {
  const res = await sampleAsync(v);
  console.log(res);
  data.push(res);
});
console.log(data);

この例では、Arrayプロトタイプに「forEachAsync」関数を追加しています。この関数は、渡されたすべてのコールバックが解決したときに解決するプロミスを返すので、通常の「forEach」関数と同じように動作します。その結果、最後のコードスニペットでは、予想される結果「1 2 3 [1, 2, 3]」が得られます。

このforEachAsyncの実装にも、前のコード例と同様の問題がある可能性があることに注意する必要があります。つまり、渡されたコールバックが非同期でないか、非同期関数内でawaitを使用している場合です。

このアプローチは、Arrayプロトタイプを変更することで予期しない結果や他のコードとの名前衝突を引き起こす可能性があるため、良い実践とは考えられません。以前に述べたようなより明示的なソリューションを使用するほうがよいでしょう。

最後

いつもお世話になっています。この記事を楽しんで、新しいことを学べたら嬉しいです。

次の記事でお会いしましょう!この記事が気に入ったら、私を応援するために「いいね!」を押して登録してください。ありがとうございました。

NGUYỄN ANH TUẤN

Xin chào, mình là Tuấn, một kỹ sư phần mềm đang làm việc tại Tokyo. Đây là blog cá nhân nơi mình chia sẻ kiến thức và kinh nghiệm trong quá trình phát triển bản thân. Hy vọng blog sẽ là nguồn cảm hứng và động lực cho các bạn. Hãy cùng mình học hỏi và trưởng thành mỗi ngày nhé!

Đăng nhận xét

Mới hơn Cũ hơn