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)
}
await
はasync
な関数でしか利用できないことには注意する。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アソシエイト
コメント