JavaScript Tips | 配列ユーティリティ:順次処理

JavaScript JavaScript
スポンサーリンク

テーマの整理:「順次処理」とは何か

ここでいう「順次処理」は、配列の要素を「必ず 1 件ずつ、順番に」処理していくパターンのことです。
特に「処理が非同期(async)」なときに、順番をきちんと守りたい場面で使います。

同期処理なら forforEach で自然と順番になりますが、
非同期処理を mapPromise.all で一気に投げると、「順番」も「同時実行数」も制御しづらくなります。
そこで、「配列を順番に処理するためのユーティリティ」を用意しておくと、業務コードがかなり安定します。


なぜ「順次処理」ユーティリティが必要になるのか

順次処理が欲しくなる典型的な理由は、だいたい次のようなものです。

API のレート制限が厳しく、一気に大量に叩きたくない。
ログやファイルを書き込む順番を保証したい。
前の処理結果を見てから、次の処理内容を決めたい。

こういうときに Promise.all で全部並列にしてしまうと、
「速いけど危ない・制御しづらい」状態になります。
逆に、「1 件ずつ順番にやる」と決めてしまえば、挙動がとても分かりやすくなります。


基本ユーティリティ 1:forEachSeries(順番に処理するだけ)

実装と考え方

まずは、「配列の各要素に対して、非同期処理を順番に実行するだけ」の一番シンプルなユーティリティです。

async function forEachSeries(array, asyncFn) {
  if (!Array.isArray(array)) {
    return;
  }
  if (typeof asyncFn !== "function") {
    return;
  }

  for (let i = 0; i < array.length; i++) {
    await asyncFn(array[i], i);
  }
}
JavaScript

ここでの重要ポイントをかみ砕きます。

asyncFn は「要素を 1 つ受け取って処理する async 関数」です。
for ループの中で、毎回 await asyncFn(...) してから次の要素に進みます。
つまり、「前の処理が終わるまで、次の処理は絶対に始まらない」ことが保証されます。

戻り値は特に使わず、「順番に処理を流したいだけ」のときに向いています。

例題:ログを順番に書き出す

async function writeLog(line) {
  await new Promise((r) => setTimeout(r, 200));
  console.log("write:", line);
}

async function main() {
  const lines = ["A", "B", "C"];

  await forEachSeries(lines, (line) => writeLog(line));

  console.log("done");
}

main();
JavaScript

ログの出方を見ると、「A → B → C」の順番が必ず守られているはずです。
ここで Promise.all を使ってしまうと、順番は保証されません。


基本ユーティリティ 2:mapSeries(順番に処理して結果を集める)

実装と考え方

次に、「順番に処理しつつ、その結果を配列として集めたい」パターンです。
これは、以前出てきた asyncMapSeries と同じ発想です。

async function mapSeries(array, asyncMapper) {
  if (!Array.isArray(array)) {
    return [];
  }
  if (typeof asyncMapper !== "function") {
    return array.slice();
  }

  const result = [];
  for (let i = 0; i < array.length; i++) {
    const value = await asyncMapper(array[i], i);
    result.push(value);
  }
  return result;
}
JavaScript

重要ポイントはこうです。

asyncMapper は「要素を変換して結果を返す async 関数」です。
毎回 await してから結果を resultpush します。
処理順も、結果の順番も、元の配列と同じになります。

例題:ユーザー ID を順番に API で取得する

async function fetchUser(id) {
  console.log("start", id);
  await new Promise((r) => setTimeout(r, 300));
  console.log("end", id);
  return { id, name: `User-${id}` };
}

async function main() {
  const ids = [1, 2, 3];

  const users = await mapSeries(ids, (id) => fetchUser(id));

  console.log(users);
}

main();
JavaScript

ログを見ると、「1 の開始→終了 → 2 の開始→終了 → 3 の開始→終了」という順番で動いているのが分かります。
「順番を守りたい API 呼び出し」には、この形がとても相性が良いです。


応用ユーティリティ:順次処理+途中で止める(早期終了)

シナリオ

「配列を順番に処理していき、ある条件を満たしたらそこで止めたい」というケースもよくあります。

例えば、

最初に成功した結果だけ欲しい。
最初にエラーになったところで止めたい。

といったパターンです。

実装例:findSeries(順番に処理して、最初に条件を満たした結果を返す)

async function findSeries(array, asyncPredicate) {
  if (!Array.isArray(array)) {
    return undefined;
  }
  if (typeof asyncPredicate !== "function") {
    return undefined;
  }

  for (let i = 0; i < array.length; i++) {
    const item = array[i];
    const ok = await asyncPredicate(item, i);
    if (ok) {
      return item;
    }
  }

  return undefined;
}
JavaScript

これは「非同期版の find」です。
順番に判定していき、最初に true になった要素を返します。

例題:最初に「有効」と判定されたサーバーだけ使う

async function isHealthyServer(url) {
  console.log("check:", url);
  await new Promise((r) => setTimeout(r, 300));
  return url.includes("primary");
}

async function main() {
  const servers = [
    "https://backup-1.example.com",
    "https://primary.example.com",
    "https://backup-2.example.com",
  ];

  const server = await findSeries(servers, (url) => isHealthyServer(url));

  console.log("use:", server);
}

main();
JavaScript

ここでは、「順番にサーバーをチェックして、最初に OK だったものだけ使う」という流れを、
findSeries で素直に表現できています。


順次処理ユーティリティを使うときの重要な意識ポイント

「順番が意味を持つか?」を必ず自分に問う

順次処理を選ぶか、並列処理を選ぶかは、性能にも影響する大事な判断です。
そこで、毎回自分にこう問いかけてほしいです。

この処理は、前の結果に依存しているか。
外部サービスやレート制限の都合で、同時実行数を抑えたいか。
ログやファイルの「順番」が意味を持つか。

どれか 1 つでも「はい」なら、順次処理ユーティリティを使う価値が高いです。

「for + await」を“パターン”として覚える

順次処理の本質は、実はとてもシンプルです。

for (...) {
  await ...
}
JavaScript

これを毎回生で書くのではなく、
forEachSeriesmapSeries のような名前付きユーティリティにしておくと、
「これは順次処理なんだな」と一目で分かるようになります。

戻り値が Promise になることを忘れない

forEachSeriesmapSeriesfindSeries も、async function なので戻り値は Promise です。
呼び出し側は必ず await する必要があります。

await forEachSeries(...);
const result = await mapSeries(...);
const found  = await findSeries(...);
JavaScript

ここを忘れて「配列だと思っていたら Promise だった」というのは、非同期あるあるなので、
「順次処理ユーティリティ=必ず await」とセットで覚えておくと安全です。


手を動かして「順次処理」の感覚をつかむ

次のコードをそのまま実行して、ログの出方を眺めてみてください。

async function forEachSeries(array, asyncFn) {
  if (!Array.isArray(array)) return;
  if (typeof asyncFn !== "function") return;

  for (let i = 0; i < array.length; i++) {
    await asyncFn(array[i], i);
  }
}

async function mapSeries(array, asyncMapper) {
  if (!Array.isArray(array)) return [];
  if (typeof asyncMapper !== "function") return array.slice();

  const result = [];
  for (let i = 0; i < array.length; i++) {
    const value = await asyncMapper(array[i], i);
    result.push(value);
  }
  return result;
}

async function findSeries(array, asyncPredicate) {
  if (!Array.isArray(array)) return undefined;
  if (typeof asyncPredicate !== "function") return undefined;

  for (let i = 0; i < array.length; i++) {
    const item = array[i];
    const ok = await asyncPredicate(item, i);
    if (ok) return item;
  }
  return undefined;
}

async function demo() {
  const items = [1, 2, 3];

  console.log("=== forEachSeries ===");
  await forEachSeries(items, async (n) => {
    console.log("start", n);
    await new Promise((r) => setTimeout(r, 300));
    console.log("end", n);
  });

  console.log("=== mapSeries ===");
  const doubled = await mapSeries(items, async (n) => {
    await new Promise((r) => setTimeout(r, 200));
    return n * 2;
  });
  console.log(doubled);

  console.log("=== findSeries ===");
  const found = await findSeries(items, async (n) => {
    await new Promise((r) => setTimeout(r, 200));
    return n >= 2;
  });
  console.log(found);
}

demo();
JavaScript

「必ず 1 → 2 → 3 の順に動いていること」「結果の順番も元の配列と同じであること」を、
ログと出力で確認してみてください。


まとめ:順次処理ユーティリティで「安全な非同期フロー」を標準化する

配列ユーティリティとしての「順次処理」は、
非同期処理を「速さ」ではなく「安全さ・分かりやすさ」優先で設計するための道具です。

プロジェクトに例えば次のような関数を置いておくイメージです。

export async function forEachSeries(...) { ... }
export async function mapSeries(...) { ... }
export async function findSeries(...) { ... }
JavaScript

そして、「順番が意味を持つ処理は必ずこれを通す」と決めておく。
それだけで、非同期コードのバグが減り、挙動が読みやすくなり、
「どこで並列にしてよくて、どこは順次でなきゃいけないか」がチーム全体で共有しやすくなります。

タイトルとURLをコピーしました