JavaScriptでforEachのasync/awaitなしでも配列の順番通りの実行結果を得る

自分の写真結城永人 -

サイトの複数の要素にJavaScriptで共通の処理を行うとき、forEach()メソッドがとても便利で、良く使っている。

const atpss = document.querySelectorAll("article.post");
atpss.forEach(atps => {
// 全てのarticle.postへの共通の処理
});

forEach()メソッドは反復処理のfor文のように数を気にせず、いつでもそれだけで配列を扱えるから簡単だし、サイトの要素を取り込むquerySelectorAll()メソッドによるノードリスト(配列と似たもの)にも対応しているから使い易い。

しかしasync/await、またはPromiseの非同期処理を組み込むことができない。

いい換えると複数の要素に対する複数の処理を前のものが終わってから後のものへ順番に移行しながらやって行くことがそのままでは完全には無理だった。

他のページを読み込むなど、時間が長くかかる処理だと顕著だけれども複数の要素を順番通りに呼び出してもその通りに終わらず、たとえ配列の順番は変わらないとしても実行結果の順番が変わるかも知れない。

配列を順番通りに処理できる二つの方法

様々な字体の「Alternative」(代わり)の文字が折り重なっている。

forEach()メソッドに配列を順番通りに処理できるasync/awaitを組み込む代替案が二つ上げられる。

どちらも配列を順番通りに取り込むだけではなく、実行結果もそのように得られてバラ付くことがない。

代替案①for文を使う直列処理

通常のforによる反復処理だとasync/awaitが動作して配列と実行結果の両方の順序が確保される。

for文とasync/awaitは直列処理で配列は一つずつ実行されてその度に個々の結果を出すので、最終的に配列の順番通りの実行結果が得られる。

サンプルのプログラム

本稿の見出しを全て取得して順番通りに表示する。

    ※左の数字がプログラムの実行結果、右の数字が実際の見出しの順番になる。

    JavaScript

    // 本稿の全ての見出しの取得
    const fhds = document.querySelectorAll(".heading");
    // タグの生成と書き出し
    function addition1(item) {
    f_list.insertAdjacentHTML("beforeend", "<li>" + item.dataset.number + " " + item.innerHTML + "</li>"); }
    // 複数の要素の直列処理
    async function serial() {
    for (const fhd of fhds) {
    await addition1(fhd); }}
    // 実行/消去ボタン
    f_run.addEventListener("click", () => {
    if (f_list.firstChild) f_list.innerHTML = "";
    serial(); });
    f_erase.addEventListener("click", () => {
    f_list.innerHTML = ""; });

    forEachはasync/awaitが利かないけれども反復処理の通常のfor文は大丈夫なんだ。

    forとasync/awaitの組み合わせは配列を一つずつ実行する直列処理だから使うのはそうした場合に特に向いていて欠かせない。

    代替案②Promise.allを使う並列処理

    Promise.all()メソッドをmap()メソッドの配列に使うと実行結果の新しい配列をPromiseオブジェクト(解決済みのPromise、async/awaitが効いたもの)で得られるので、それをさらにforEach()メソッドなどにかけて素早く出すと元の配列に関して順番通りの実行結果が得られることになる。

    サンプルのプログラム

    本稿の見出しを全て取得して順番通りに表示する。

      ※左の数字がプログラムの実行結果、右の数字が実際の見出しの順番になる。

      JavaScript

      // 本稿の全ての見出しの取得
      const shds = document.querySelectorAll(".heading");
      // 最後の書き出しのみ
      function addition2(item) {
      s_list.insertAdjacentHTML("beforeend", item); }
      // 複数の要素のタグの生成を含む並列処理
      function parallel() {
      Promise.all([...shds].map(shd => {
      return shd = "<li>" + shd.dataset.number + " " + shd.innerHTML + "</li>"; })).then(results => results.forEach(addition2)); }
      // 実行/消去ボタン
      s_run.addEventListener("click", () => {
      if (s_list.firstChild) s_list.innerHTML = "";
      serial(); });
      s_erase.addEventListener("click", () => {
      s_list.innerHTML = ""; });

      ※ノードリストはmap()メソッドで扱えないからスプレッド構文([...変数])などを使って展開できるようにする。

      配列をmapで作り直すのをPromise.allで待ち構える。並列処理なので、この時点での実行結果の順番はバラバラになっている。全て終わってPromiseオブジェクトの新しい配列を得られたらforEachなどで素早く反復処理するほどに元の配列に関して順番通りの実行結果を得られる。

      配列の実行結果の順番を変えてしまうような重たい処理をPromise.allのかかったmapのところで済ませるのがコツというか、そのように使わなくては順番通りの実行結果は得難いし、又、mapのところで最終的な実行結果を出さないようにプログラムを調整しなくてはならないのも注意を要する。

      forEachはasync/awaitが利かなくて配列の順番通りの実行結果が必ずしも得られないけれどもfor文かPromise.allを使った二つの代替案で十分に対処できる。

      それぞれを比較してみると前者は直列処理で、処理速度は遅いけれども配列を一つずつ処理するから実行結果の順番を確実に守ることができる。後者は並列処理で、処理速度が速いから配列の数が物凄く多いとか他のページを読み込むなんて時間が長くかかるプログラムに有力なんだ。

      参考:大量のPromiseを捌く手段

      コメント