こんにちは。@Esperna です。
@asip2k25 さんのコメントでresolveしたものに対してわざわざ配列で個別に代入しなくて良いことに気づきました。ありがとうございます。
背景
callbackを使ったコードって読めなくはないのですが、callback処理の内容を覚えておかなければならず、読みづらいです。また、ネストも深くなりがち。読みにくいのでPromiseとかasync/await使って直したいなと思うことがあります。Promiseとかasync/awaitはJavascriptPrimerを読んで、実際のcallbackのコードを書き直そうと思うとデグレが怖く二の足を踏んでしまいがち。最近はGPTの使いこなしネタが多く、そちらに追従して効率上げることも大事だなと思う一方、ちゃんと分かって書けることは大事だと思って、車輪の再発明ながら自身の理解を深めるために書きます。
というわけで、Javascriptのcallback処理をPromise, async/awaitで書き直してみようと思います。
callbackのコード例
function asyncTask(callback) {
const TIMEOUT = 10;
setTimeout(() => {
var data = 0;
if (Math.random() > 0.5) {
data = 1;
callback(null, data);
} else {
callback(new Error("Invalid Data"), data);
}
}, TIMEOUT);
}
asyncTask((err, data) => {
if (err === null) {
console.log(`successfully finished task. Data: ${data}`);
} else {
console.error(`error occured: ${err}: ${data}`);
}
});
console.log("called asyncTask");
これは非同期処理であるasyncTaskを実行するコードです。
asyncTaskはcallbackを引数に取ります。
引数には無名関数(アロー関数)を渡していて、
この無名関数はエラーがない場合
called asyncTask
successfully finished task. Data: 1
というコンソールログが表示されます。
エラーがある場合は
called asyncTask
error occured: Error: Invalid Data: 0
というコンソールログが表示されます。
一方、asyncTaskという関数は内部でsetTimeoutをコールすることで、
非同期処理を行うようにしていて、エラーがない場合とある場合を実現するのに
Math.random()を使ってランダムにそれぞれのケースを発生させてます。
Promiseを使ってcallbackを書き直す
下記の通りです。
function asyncTask() {
return new Promise((resolve) => {
const TIMEOUT = 10;
setTimeout(() => {
var err = null;
var data = 0;
if (Math.random() > 0.5) {
data = 1;
} else {
err = new Error(`Invalid Data: ${data}`);
}
resolve({ err, data });
}, TIMEOUT);
});
}
asyncTask().then((res) => {
if (res.err === null) {
console.log(`successfully finished task. Data: ${res.data}`);
} else {
console.error(`error occured: ${res.err}`);
}
});
console.log("called asyncTask");
callback版との違いを見てみると
- Promise版の方が以下の理由でコードが長くなっています
- asyncTaskがPromiseを返すようにする必要がある
Javascriptで複数の戻り値(err,data)を返すのに配列を使っている
- resolveで略記プロパティ名 (ES2015)を使っています
- callbackの呼び出しがなくなっています
- 元のコードのcallbackのネストが浅いから効果が見えにくい
async/awaitを使ってみる
function asyncTask() {
return new Promise((resolve) => {
const TIMEOUT = 10;
setTimeout(() => {
var err = null;
var data = 0;
if (Math.random() > 0.5) {
data = 1;
} else {
err = new Error(`Invalid Data: ${data}`);
}
resolve({ err, data });
}, TIMEOUT);
});
}
async function doAsyncTask() {
const { err, data } = await asyncTask();
if (err === null) {
console.log(`successfully finished task. Data: ${data}`);
} else {
console.error(`error occured: ${err}`);
}
}
doAsyncTask();
console.log("called asyncTask");
Promise版との違いは
- asyncTask.then()という書き方をasync/awaitという書き方に置き換えられスッキリしました
- 一方でasync/awaitの形にするために無名関数ではなくdoAsyncTaskというasync functionを追加しているので少しコード行数は増えています
- err,dataに値を入れるのに分割代入を使っています
Promiseでresolveだけでなくrejectも使ってみる
function asyncTask() {
return new Promise((resolve, reject) => {
const TIMEOUT = 10;
setTimeout(() => {
var data = 0;
if (Math.random() > 0.5) {
data = 1;
resolve(data);
} else {
reject(new Error(`Invalid Data: ${data}`));
}
}, TIMEOUT);
});
}
async function doAsyncTask() {
try {
const data = await asyncTask();
console.log(`successfully finished task. Data: ${data}`);
} catch (err) {
console.error(`error occured: ${err}`);
}
}
doAsyncTask();
console.log("called asyncTask");
-
Promise版、async/await版を比べると配列で返って来た戻り値を再代入することがなくなったのでシンプルになりました個人的にはerr, data = await asyncTask();
と書けると嬉しいのだが、javascriptでは配列にしないと書けないという認識
- Promise版、async/await版を比べるとerr,dataを引数で受け取るのではなく、関数内部でasyncTask()の戻り値で受け取るので結合度の観点では悪くなってますね
- callback版と比べると
- 正常系のcallbackがresolveに、異常系のcallbackがrejectに置き換わってます
- asyncTaskの引数で渡される無名関数がasync function doAsyncTaskとなっています
- asyncTask関数内のif/elseがasync function doAsyncTask内のtry/catchに置き換わってます
所感
- callbackのネストが浅いとcallbackをasync/awaitにしてもコードの可読性が良くなったように見えにくい
- async/awaitによりcallbackがなくなり、callback処理を覚えておく必要がなくなるので読みやすくなる
- テストコードも書きやすくなる
- resolve,rejectを使うとコード行数もそこまで変わらずスッキリ書けて読みやすい
- callbackをasync/awaitに書き直そうと思ったら、callbackで渡す関数をasync functionにしつつ、callbackを受け取る非同期関数をasync function内でawaitしてあげればOK