javascript callbackのパターンでasync/awaitを使う

callbackのパターンでasync/awaitを使うなどのメモ。

やりたいこと

次のような、処理が終わるとコールバックを呼ぶ関数がある:

// 定義
// numはなんでもいいただの引数
function doLongTask(num, successCallback, failCallback)

処理成功時には結果をコールバックに渡し、失敗時にはErrorオブジェクトをコールバックに渡す。

この関数をコールバックを使わないで、successCallbackで渡される結果を取得出来るようにする。

つまり:

// こうしなければいけないところを
doLongTask(num, (result) => {
  doSomething(result)
}, /*errorの方は省略*/)

// こうしたい
const result = /* doLongTaskを使って結果を取得する */

doSomething(result)

実際はオブジェクトのメソッドで、タスクキューにキューイングして処理を行うので、コールバックはタスクキューからコールスタックにpushされることになる。

例では、setTimeoutを使って擬似的に同じような状況を再現している。

簡単な例

普通に使うのであれば、とても単純。コールバックを渡す:

// 普通に使う
doLongTask((result) => {
  console.log('result here:', result)
}, (err) => {
  console.log('error occurred:', err)
})

Promiseでラップすれば、普段通りの構文で戻り値を取れる。エラーの場合に備えてtry/catchで括る必要はある:

// async/awaitを利用
try {
  const result = await new Promise((resolve,reject) => {
    doLongTask(
      (result) => resolve(result),
      (err) => reject(err)
    )
  })
} catch (e) {
  console.log(e)
}

awaitasyncな関数でしか利用できないことには注意する。Promise.then()でもできるが、結局コールバックになるのでしない。

複数回呼んで結果をまとめる

複数回実行して結果をまとめることを考えるとコールバックは使いにくかった。

コールバックを利用するなら、普通にすると外側に配列を用意することになりそう。しかし、doLongTaskが非同期的にコールバックを呼び出しているような形だと、ループ後に確認しても、配列は空のまま。

// doLongTaskが同期的にコールバックを呼び出さないと、空になる例
const result1 = [];
for (let i = 1; i < 10; i++) {
  doLongTask(i, (result) => result1.push(result));
}
console.log("multiple result1:", result1);
// result1: []になる

2つの解決法がある。

  • async/awaitを使う
  • Promise.all()を使う

async/awaitを使う場合

先の例と同じく、awaitで戻り値を待つ。エラーハンドルのためのtry/catchが必要:

async function f2(msg) {
  const results = [];
  for (let i = 1; i < 10; i++) {
    try{
      results.push(
        await new Promise((resolve) => {
          doLongTask(i, (r) => resolve(r));
        })
      );
    } catch (e) {
      console.log(e)
      result.push('error')
    }
  }
  console.log(msg, results);

  // 戻り値はPromiseでラップされていることに注意
  // return results
}

f2("multiple result2:");

個々にエラーをハンドルできるのは便利。

Promise.all()を使う場合

async/awaitと似たような表現になる:

function f3(msg) {
  const queue = [];
  for (let i = 1; i < 10; i++) {
    queue.push(
      new Promise((resolve, reject) => {
        doLongTask(i, (r) => resolve(r), (err) => reject(err));
      })
    );
    }
  }
  Promise.all(queue).catch((reason) => {
      console.log(reason);
    }).then((values) => {
    console.log(msg, values);
  });
}

f3("multiple result3:");

queueにはPromiseが入るのでエラーハンドルは、Promise.all()のコールバックとして取り付ける必要がある。 async/awaitの場合と違い、エラーが起きると結果が取得できないのはケースによっては痛い。 それに結局コールバックを使ってるのがよくない。

デモ

デモのdoLongTaskは、forループの初期化する数を0以下にすることでエラーを誘発できる。1以上ではエラーは起きない。

htmlの中身はないのでプレビュー画面の左下部分のボタンからコンソールを開いて動作を確認していただきたい。

おわり

Promise.all()でいいかと思ったが、上の例だと、valuesを他のところに持っていくのが少し大変……と思ったが、Promise.all()が返すPromiseを返しておけば、async/awaitの場合と変わらなくなる。

個人的にはあまりthenを書きまくるのは好みではないので、async/awaitを使うようにしたいと思っている。

Promiseは使わないとすぐに忘れるし、まだここに書いていなかったようなので書いた。正直もっと良い方法がありそう。 デモでは雑にsetTimeoutで遅らせているが実際は中々そんなことはしない。しかし起こっている状況的には大体同じなので問題はないだろう。

Promiseは使いようがたくさんあって面白い。async/awaitはきれいにまとまっていい。

以上です。


Amazonアソシエイト

Amazon | JavaScript with Promises: Managing Asynchronous Code | Parker, Daniel | Software Development
Amazon配送商品ならJavaScript with Promises: Managing Asynchronous Codeが通常配送無料。更にAmazonならポイント還元本が多数。Parker, Daniel作品ほか、お急ぎ便対象商品は当日お届けも可能。
タイトルとURLをコピーしました