Promiseとasync/awaitで複数の非同期処理の完了を待機する

Promiseとasync/awaitで複数の非同期処理の結果を待機する方法についてのメモです。

非同期処理をループで複数実行した場合に、
すべての非同期処理の完了を待つのに困ったので調べてみました。

下記のような、非同期処理があった場合、
完了後に処理を行いたい場合はcallbackで行う必要があります。

let doAsyncJob = (data) => {
  // do async job
  setTimeout(() => {
    console.log("wait " + data + " sec.");
  }, data);
};

完了後にcallbackを実行。

asyncJob.js

let doAsyncJobWithCallback = (data, callback) => {
  // do async job
  setTimeout(() => {
    console.log("wait " + data + " sec.");
    callback();
  }, data);
};

doAsyncJobWithCallback(200, () => console.log("done"));

下記の様にループで非同期処理を複数回実行して、すべて完了後に処理を行いたい場合
普通にコールバックを渡してもだめなので何らかの工夫が必要です。

asyncLoop.js

const datas = [200, 30, 100, 80];

let func = (datas) => {
  for (let i = 0; i < datas.length; i++) {
    let data = datas[i];
    doAsyncJob(data);
  }
};

let doAsyncJob = (data) => {
  // do async job
  setTimeout(() => {
    console.log("wait " + data + " sec.");
  }, data);
};

func(datas);
クロージャを使用

クロージャでカウンタを持たせて、全ての非同期処理が終わったかどうかを確認して、
全て完了していればcallbackを実行するようにします。

asyncLoopWithClosure.js

const datas = [200, 30, 100, 80];

let loop = (max, callback) => {
  let count = 0;
  return () => {
    count++;
    if (count >= max) {
      callback();
      return;
    }
  };
};

let funcWithClosure = (datas, callback) => {
  let myLoop = loop(datas.length, callback);
  for (let i = 0; i < datas.length; i++) {
    let data = datas[i];
    doAsyncJob(data, myLoop);
  }
};

let doAsyncJob = (data, loop) => {
  // do async job
  setTimeout(() => {
    console.log("wait " + data + " sec.");
    loop();
  }, data);
};

funcWithClosure(datas, () => console.log("all done!!"));

実行結果

wait 30 sec.
wait 80 sec.
wait 100 sec.
wait 200 sec.
all done!!
再帰を使用

再帰を使って完了した非同期処理の数をカウントするようにしています。
全て完了していればcallbackを実行するようにします。

asyncLoopWithRecursive.js

const datas = [200, 30, 100, 80];

recursiveFunc = (datas, callback) => {
  let count = 0;
  let done = 0;

  let func = (count) => {
    let data = datas[count];
    // do async job
    setTimeout(() => {
      console.log("wait " + data + " sec.");
      if (++done >= datas.length) {
        callback();
      }
    }, data);

    if (count >= datas.length - 1) {
      return;
    }

    func(count + 1);
  };

  func(count);
};

recursiveFunc(datas, () => console.log("all done!!"));

実行結果

wait 30 sec.
wait 80 sec.
wait 100 sec.
wait 200 sec.
all done!!
Promiseを使用

Promiseを使用すると完了時の処理を記述することが出来ます。
成功時にresolve()、失敗時にreject()を呼ぶことで、完了時にどちらか一方が呼び出されます。
then()メソッドで完了時のコールバックを指定できます。

ループ処理のように複数のPromiseの完了を待機する場合、
Promise.all()ですべてのPromiseが成功した時、またはいずれかが失敗した場合のPromiseを取得できます。

asyncLoopWithPromiseAll.js

const datas = [200, 30, 100, 80];

let funcWithPromise = (datas) => {
  return Promise.all(datas.map((data) => {
    return doAsyncJobWithPromise(data);
  }));
};

let doAsyncJobWithPromise = (data) => {
  return new Promise((resolve, reject) => {
    // do async job
    setTimeout(() => {
      console.log("wait " + data + " sec.");
      resolve();
    }, data);
  });
};

funcWithPromise(datas)
  .then(() => {console.log("all done!!")});

実行結果

wait 30 sec.
wait 80 sec.
wait 100 sec.
wait 200 sec.
all done!!
async/awaitを使用

async/awaitを使用するともう少し同期的に書けそうです。

asyncが付与されたfunctionが呼ばれるとPromiseを返します。

async function xxx(arg) {
  // do something
}

let xxx = async (arg) => {
  // do something
}

awaitでPromiseの完了を待つことが出来ます。
そのため、awaitの後続処理を同期的に処理を記述できます。
awaitはasync functionからだけ呼ぶことが出来ます。

async function xxx(arg) {
  return new Promise(resolve => {
    // do something
  });
}

async function exec() {
  await xxx(arg);
  // 後続処理
  doAfterFunction();
}

exec();

async/awaitはES6に含まれていないので、babelでトランスパイルする必要があります。
.babelrcでes2017を指定。

.babelrc

{
    "presets": ["es2017"]
}

バベる。

babel asyncLoopWithAsyncAwait.js --out-dir dist

async/awaitを使うと、後処理のcallbackを無くすことが出来ました。

asyncLoopWithAsyncAwait.js

const datas = [200, 30, 100, 80];

let funcWithAsync = async (datas) => {
  return await Promise.all(datas.map(function(data){
    return doAsyncJobWithPromise(data);
  }));
};

let doAsyncJobWithPromise = (data) => {
  return new Promise((resolve, reject) => {
    // do async job
    setTimeout(() => {
      console.log("wait " + data + " sec.");
      resolve();
    }, data);
  });
};

let exec = async () => {
  await funcWithAsync(datas);
  console.log("all done!!");
}

exec();

実行結果

wait 30 sec.
wait 80 sec.
wait 100 sec.
wait 200 sec.
all done!!

終わり。

github.com