生JSでAsync/Awaitも使わずに非同期関数の同期的ループ

Node.jsで非同期関数を同期的にループ実行するためのモジュールはいくつかありますが、今回は外部モジュールを一切使わないピュアなJavaScriptだけで且つAsync/Awaitも使わずにそれを実現する方法です。このテクニックを使う場面は主に、古き良きCallbackスタイルでコーディングされているシステムになると思いますが、様々な理由でAsync/Awaitを敢えて使いたくない(or 使えない)場面でも使えるテクニックかと思います。

非同期処理を同期的にループするテクニック

下の例は convertToUpperCase() という非同期関数を用意されたパラメータ配列の数だけ同期的にループ実行しています。ちなみにここではイテレーターとジェネレーターを使っています:

// 非同期関数を同期っぽくループ!
const ite = function*() {
  for (let len = params.length, i = 0; i < len; i++) {
    const param = params[i];
    // 非同期関数にyieldを付けて実行:
    yield convertToUpperCase(param, (err, res) => { // 1) ここでyieldで止めて、、、
      if (err) {
        // Implement any error handling
        console.error("an execution failed:", err);
      } else {
        // Implement handling a result
        console.log(`an execution result: ${param} => ${res}`);
      }
      // NOTE: Node.jsを使っていて処理スピードを重視する場合は setImmediate() を使うと良い:
      setTimeout(() => { ite.next(); }, 0); // 2) ここでyieldを解除!
    });
  }
  console.log("all executions have done!");
}();
ite.next(); // fire immediately!

全体のコードは以下になります:

/** UpperCaseを返すだけの非同期関数 */
function convertToUpperCase(param, cb) {
  const ret = (typeof param === "string") ? param.toUpperCase() : null;
  return cb && cb(null, ret);
}

// テキトーなパラメータ一覧を用意
const params = (function() {
  const arr = [];
  for (let len = 123, i = 97; i < len; i++) { // pushing "a" ... "z" into array.
    arr.push(String.fromCharCode(i));
  }
  return arr;
}());

// 非同期関数を同期っぽくループ!
const ite = function*() {
  for (let len = params.length, i = 0; i < len; i++) {
    const param = params[i];
    // 非同期関数にyieldを付けて実行:
    yield convertToUpperCase(param, (err, res) => { // 1) ここでyieldで止めて、、、
      if (err) {
        // Implement any error handling
        console.error("an execution failed:", err);
      } else {
        // Implement handling a result
        console.log(`an execution result: ${param} => ${res}`);
      }
      // NOTE: Node.jsを使っていて処理スピードを重視する場合は setImmediate() を使うと良い:
      setTimeout(() => { ite.next(); }, 0); // 2) ここでyieldを解除!
    });
  }
  console.log("all executions have done!");
}();
ite.next(); // fire immediately!

実行結果:

an execution result: a => A
an execution result: b => B
an execution result: c => C
an execution result: d => D
an execution result: e => E
an execution result: f => F
an execution result: g => G
an execution result: h => H
an execution result: i => I
an execution result: j => J
an execution result: k => K
an execution result: l => L
an execution result: m => M
an execution result: n => N
an execution result: o => O
an execution result: p => P
an execution result: q => Q
an execution result: r => R
an execution result: s => S
an execution result: t => T
an execution result: u => U
an execution result: v => V
an execution result: w => W
an execution result: x => X
an execution result: y => Y
an execution result: z => Z
all executions have done!

ちゃんと順番に実行されていることがわかります。

デメリット?

繰り返し実行する関数のどれかで処理が引っかかってしまうと後の関数処理が全て止まってしまうことがデメリットとしてありますが、これはこのテクニックのデメリットというよりも直列処理全般に言えることです。もし実行順序を気にする必要がないのであれば、敢えて同期的ループを使う必要はないでしょう。

以上

PR:時事ネタチェックの時間節約!
芽萌丸プログラミング部 @programming
プログラミング関連アカウント。Web標準技術を中心に書いていきます。フロントエンドからサーバサイドまで JavaScript だけで済ませたい人たちの集いです。