はじめに
async/awaitで①並列実行, ②直列実行, **③即時実行(待受無し)**の書き方をまとめました。(ECMAScript 2017前提)
記事等では、**(A) for-of(iterableのループ)を使う方法と、(B) Array.prototype(配列のメソッド)**を使う方法に2分されますが、①並列実行, ②直列実行, **③即時実行(待受無し)**で、(A)と(B)のどちらを使うべきか(どちらが可読性が高いか)を考えます。
(1) 複数文の繰り返し処理
(1-A) for-of(iterableのループ)
(1-A-①) 並列実行
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
(async () => {
  console.log('START');
  let secSleeps = [];
  for ( let v of [3, 2, 1] ) {
    secSleeps.push( (async () => {
      console.log(`  START sleep ${v}s`);
      await secSleep(v);
      console.log(`  END sleep ${v}s`);
    })());
  }
  await Promise.all(secSleeps);
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   START sleep 2s
//   START sleep 1s
//   END sleep 1s
//   END sleep 2s
//   END sleep 3s
// END
(1-A-②) 直列実行
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
(async () => {
  console.log('START');
  for ( v of [3, 2, 1] ) {
    console.log(`  START sleep ${v}s`);
    await secSleep(v);
    console.log(`  END sleep ${v}s`);
  }
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   END sleep 3s
//   START sleep 2s
//   END sleep 2s
//   START sleep 1s
//   END sleep 1s
// END
(1-A-③) 即時実行(待受無し)
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
(async () => {
  console.log('START');
  for ( let v of [3, 2, 1] ) {
    (async() => {
      console.log(`  START sleep ${v}s`);
      await secSleep(v);
      console.log(`  END sleep ${v}s`);
    })();
  }
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   START sleep 2s
//   START sleep 1s
// END
//   END sleep 1s
//   END sleep 2s
//   END sleep 3s
(1-B) Array.prototype(配列のメソッド)
(1-B-①) 並列実行
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
(async () => {
  console.log('START');
  await Promise.all([3, 2 ,1].map( async v => {
    console.log(`  START sleep ${v}s`);
    await secSleep(v);
    console.log(`  END sleep ${v}s`);
  }));
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   START sleep 2s
//   START sleep 1s
//   END sleep 1s
//   END sleep 2s
//   END sleep 3s
// END
(1-B-②) 直列実行
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
(async () => {
  console.log('START');
  await [3, 2 ,1].reduce( (promise, v) => {
    return promise.then( async () => {
      console.log(`  START sleep ${v}s`);
      await secSleep(v);
      console.log(`  END sleep ${v}s`);
    });
  }, Promise.resolve());
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   END sleep 3s
//   START sleep 2s
//   END sleep 2s
//   START sleep 1s
//   END sleep 1s
// END
(1-B-③) 即時実行(待受無し)
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
(async () => {
  console.log('START');
  [3, 2 ,1].forEach( async v => {
    console.log(`  START sleep ${v}s`);
    await secSleep(v);
    console.log(`  END sleep ${v}s`);
  });
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   START sleep 2s
//   START sleep 1s
// END
//   END sleep 1s
//   END sleep 2s
//   END sleep 3s
(2) 単独式の繰り返し処理
(2-A) for-of(iterableのループ)
(2-A-①) 並列実行
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const secSleepWithLog = async sec => {
  console.log(`  START sleep ${sec}s`);
  await secSleep(sec);
  console.log(`  END sleep ${sec}s`);
}
(async () => {
  console.log('START');
  let secSleeps = [];
  for ( let v of [3, 2, 1] ) {
    secSleeps.push(secSleepWithLog(v));
  }
  await Promise.all(secSleeps);
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   START sleep 2s
//   START sleep 1s
//   END sleep 1s
//   END sleep 2s
//   END sleep 3s
// END
(2-A-②) 直列実行
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const secSleepWithLog = async sec => {
  console.log(`  START sleep ${sec}s`);
  await secSleep(sec);
  console.log(`  END sleep ${sec}s`);
}
(async () => {
  console.log('START');
  for ( v of [3, 2, 1] ) {
    await secSleepWithLog(v);
  }
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   END sleep 3s
//   START sleep 2s
//   END sleep 2s
//   START sleep 1s
//   END sleep 1s
// END
(2-A-③) 即時実行(待受無し)
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const secSleepWithLog = async sec => {
  console.log(`  START sleep ${sec}s`);
  await secSleep(sec);
  console.log(`  END sleep ${sec}s`);
}
(async () => {
  console.log('START');
  for ( let v of [3, 2, 1] ) {
    secSleepWithLog(v);
  }
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   START sleep 2s
//   START sleep 1s
// END
//   END sleep 1s
//   END sleep 2s
//   END sleep 3s
(2-B) Array.prototype(配列のメソッド)
(2-B-①) 並列実行
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const secSleepWithLog = async sec => {
  console.log(`  START sleep ${sec}s`);
  await secSleep(sec);
  console.log(`  END sleep ${sec}s`);
}
(async () => {
  console.log('START');
  await Promise.all([3, 2 ,1].map(secSleepWithLog));
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   START sleep 2s
//   START sleep 1s
//   END sleep 1s
//   END sleep 2s
//   END sleep 3s
// END
(2-B-②) 直列実行
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const secSleepWithLog = async sec => {
  console.log(`  START sleep ${sec}s`);
  await secSleep(sec);
  console.log(`  END sleep ${sec}s`);
}
(async () => {
  console.log('START');
  await [3, 2 ,1].reduce( (promise, v) => {
    return promise.then( async () => secSleepWithLog(v) );
  }, Promise.resolve());
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   END sleep 3s
//   START sleep 2s
//   END sleep 2s
//   START sleep 1s
//   END sleep 1s
// END
(2-B-③) 即時実行(待受無し)
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const secSleepWithLog = async sec => {
  console.log(`  START sleep ${sec}s`);
  await secSleep(sec);
  console.log(`  END sleep ${sec}s`);
}
(async () => {
  console.log('START');
  [3, 2 ,1].forEach(secSleepWithLog);
  console.log('END');
})();
// =>
// START
//   START sleep 3s
//   START sleep 2s
//   START sleep 1s
// END
//   END sleep 1s
//   END sleep 2s
//   END sleep 3s
結果の比較
評価は完全な主観になりますが、不必要な無名関数呼び出しや、記載の長さを判断基準としてます。
| 処理内容 | (A) for-of (iterableのループ)  | 
(B) Array.prototype (配列のメソッド)  | 
|---|---|---|
| 1-① 複数文の並列実行 | ★☆☆ | ★★★ | 
| 1-② 複数文の直列実行 | ★★★ | ★☆☆ | 
| 1-③ 複数文の即時実行(待受無し) | ★☆☆ | ★★★ | 
| 2-① 単独式の並列実行 | ★☆☆ | ★★★ | 
| 2-② 単独式の直列実行 | ★★★ | ★☆☆ | 
| 2-③ 単独式の即時実行(待受無し) | ★★☆ | ★★★ | 
思ったことを何点か
- 並列実行はArray.prototype.mapを使う方がよい
 - 直列実行はfor-ofを使う方がよい
 - for-ofで記載を統一(即時実行等を行う)場合は、非同期処理単位で関数化しておけば即時関数が減り、可読性が上がる
 - Array.prototype.reduceを使う記載は、今回のケースではなく、単独式に引数がなければもう少し簡略化して書けた。
 
また、今回のケースでは、以下の2点注意事項があると思っている。
- 実行結果の戻り値を使わないケースのため、戻り値を使用する場合はもう少し複雑になる。
 - for-ofとArray.prototypeでは、for-of側はiterableであれば使用できるが、Array.prototypeはArrayのみ。
 
オレオレPromiseHandler作成
今回のケースでは、await Promise.all([3, 2 ,1].map(secSleepWithLog));の記載が個人的には書きやすい点と、可読性を考えて並列実行はArray.prototypeを使用し、直列実行はfor-ofを用いる場合、並列実行⇔直列実行の変更が難しいと感じた。
また、iterableが引数で、直列実行の戻り値も利用でき、かつ**並列/直列/即時を切替え可能(同じインターフェース)**の関数郡があるとよいのでは?と考え、下記の関数を定義してみる。
PromiseHandler.parallel(並列実行)
PromiseHandler.parallel(iterable, async function callback(currentValue) {})
※ 返値のPromiseは、引数として渡されたiterableのすべての値(Promiseではない値も)を含んだ配列でresolveされます。
PromiseHandler.serial(直列実行)
PromiseHandler.serial(iterable, async function callback(currentValue) {})
※ 返値のPromiseは、引数として渡されたiterableのすべての値(Promiseではない値も)を含んだ配列でresolveされます。
PromiseHandler.immediate(即時実行(待受無し))
PromiseHandler.immediate(iterable, async function callback(currentValue) {})
※ 返値のPromiseは、undefinedでresolveされます。
PromiseHandleの実装(コード)
const PromiseHandler = {
  parallel: async (iterable, callback) => {
    let array = [];
    for ( let v of iterable ) {
      array.push(callback(v));
    }
    return await Promise.all(array);
  },
  serial: async (iterable, callback) => {
    let array = [];
    for ( let v of iterable ) {
      array.push(await callback(v));
    }
    return array;
  },
  immediate: async (iterable, callback) => {
    for ( let v of iterable ) {
      callback(v);
    }
  },
}
(2-C) オレオレPromiseHandler
作成したオブジェクトを使用して、(2) 単独式の繰り返し処理の、①並列実行, ②直列実行, ③即時実行(待受無し)を書いてみる。((1) 複数文の繰り返し処理は省略)
なお、戻り値の制御も確認するために、戻り値の確認も入れている。
(2-C-①) 並列実行
// PromiseHandlerが作成してある前提
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const secSleepWithLog = async sec => {
  console.log(`  START sleep ${sec}s`);
  await secSleep(sec);
  console.log(`  END sleep ${sec}s`);
  return `"ret=${sec}"`; //戻り値確認用に追加
}
(async () => {
  console.log('START');
  let ret = await PromiseHandler.parallel([3, 2 ,1], secSleepWithLog);
  console.log(`END ${ret.join(",")}`);
})();
// =>
// START
//   START sleep 3s
//   START sleep 2s
//   START sleep 1s
//   END sleep 1s
//   END sleep 2s
//   END sleep 3s
// END "ret=3","ret=2","ret=1"
(2-C-②) 直列実行
// PromiseHandlerが作成してある前提
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const secSleepWithLog = async sec => {
  console.log(`  START sleep ${sec}s`);
  await secSleep(sec);
  console.log(`  END sleep ${sec}s`);
  return `"ret=${sec}"`; //戻り値確認用に追加
}
(async () => {
  console.log('START');
  let ret = await PromiseHandler.serial([3, 2 ,1], secSleepWithLog);
  console.log(`END ${ret.join(",")}`);
})();
// =>
// START
//   START sleep 3s
//   END sleep 3s
//   START sleep 2s
//   END sleep 2s
//   START sleep 1s
//   END sleep 1s
// END "ret=3","ret=2","ret=1"
(2-C-③) 即時実行(待受無し)
// PromiseHandlerが作成してある前提
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const secSleepWithLog = async sec => {
  console.log(`  START sleep ${sec}s`);
  await secSleep(sec);
  console.log(`  END sleep ${sec}s`);
  return `"ret=${sec}"`; //戻り値確認用に追加
}
(async () => {
  console.log('START');
  let ret = await PromiseHandler.immediate([3, 2 ,1], secSleepWithLog);
  console.log(`END ${ret}`);
})();
// =>
// START
//   START sleep 3s
//   START sleep 2s
//   START sleep 1s
// END undefined
//   END sleep 1s
//   END sleep 2s
//   END sleep 3s
オレオレPromiseHandlerの評価
繰り返しますが、評価は主観です
| 処理内容 | (A) for-of (iterableのループ)  | 
(B) Array.prototype (配列のメソッド)  | 
(C) オレオレPromiseHandler | 
|---|---|---|---|
| 2-① 単独式の並列実行 | ★☆☆ | ★★★ | ★★★★★ | 
| 2-② 単独式の直列実行 | ★★★ | ★☆☆ | ★★★★★ | 
| 2-③ 単独式の即時実行(待受無し) | ★★☆ | ★★★ | ★★★★★ | 
①並列実行, ②直列実行, ③即時実行(待受無し)について、実際のケースではserial以外はそんなにメリットが多くない気がするがすべて使用するプロパティ名を変えるだけで切り替え可能で、記法もシンプルになった!
(参考) generatorでserial処理が可能なことの確認
// PromiseHandlerが作成してある前提
const secSleep = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const secSleepWithLog = async sec => {
  console.log(`  START sleep ${sec}s`);
  await secSleep(sec);
  console.log(`  END sleep ${sec}s`);
  return `"ret=${sec}"`; //戻り値確認用に追加
}
const increseGenerator = function*() {
  let v = 1;
  while(true) {
    yield v;
    v = v * 2;
  }
}
(async () => {
  console.log('START');
  let ret = await PromiseHandler.serial(increseGenerator(), secSleepWithLog);
  console.log(`END ${ret.join(",")}`);
})();
// =>
// START
//   START sleep 1s
//   END sleep 1s
//   START sleep 2s
//   END sleep 2s
//   START sleep 4s
//   END sleep 4s
//   START sleep 8s
//   END sleep 8s
//   START sleep 16s
//   END sleep 16s
//   START sleep 32s
// ...無限に続く
さいごに
async/await記法はネストが浅くかけて可読性も高いと思っているが、for-ofで小括弧をつけ忘れたり、今回のように順番に丁寧に書いても、たまにアレってなるので、個人用メモを兼ねている。
参考記事(Qiita)
for-of, Promise.all, reduceを使用した記事
for-ofとPromise.allを使用した記事
async/awaitを、Array.prototype.forEachで使う際の注意点、という話
Promise.allを使用した記事
map
for-ofによるpush
async/await で複数の非同期処理を待つときに注意したいこと
reduceを使用した記事
JavaScript/TypeScript で Promise を直列実行する
関数定義
all時の関数呼び出しをラップ(()を省略)
async-await内で並列処理
※ TypeScript