はじめに
なんとなくわかった気になっていたJavaScriptの同期/非同期処理ですが、先日複雑な処理において非常に悩まされ。。
この機会に、改めてまとめてみました。
大前提
-
同期処理:あるタスクが実行している間、他のタスクの処理は中断される(上から順に関数が実行される)
-
非同期処理:あるタスクがDB等に仕事を任せている間に、他のタスクが別の処理を実行できる
-
コールスタック:関数は呼び出されるとコールスタックに追加される
-
タスクキュー:実行待ちの非同期処理の行列で、入った順番で実行される
コールスタックに関数が積まれている間は、タスクキューは待ちの状態
非同期処理を行う関数(コールバック版)
function select_from_sampleTable(param, callback) {
const result = ・・・省略・・・; //select処理
if (result.length !== 0) {
callback(result.length, null);
} else {
callback(null, '該当データなし');
}
}
//呼び出す
select_from_sampleTable(1, function(num, error) {
if (num !== null) {
console.log('取得件数は' + num + '件です。');
} else {
console.log(error);
}
});
実行の順序は、
・スクリプトが実行される
・select_from_sampleTableがコールスタックに追加される(push)
・コールスタックからselect_from_sampleTableが実行される
・select_from_sampleTableのコールバック関数がタスクキューに登録される
・select_from_sampleTableの実行が終了し(pop)、コールスタックが空になる
・タスクキューに入ったコールバック関数が実行される
コードは省きますが、このような非同期処理が複数連なるとネストが深くなり可読性が非常に悪くなってしまう。(コールバック地獄!)
これを解消するために登場したのが「Promise」
非同期処理を行う関数(Promise版)
Promiseは以下3つの状態を持つ。
- pending:非同期処理の実行中の状態を表す
- fulfilled :非同期処理が正常終了した状態を表す → resolveが実行される
- rejected:非同期処理が異常終了した状態を表す → rejectが実行される
new Promise((resolve, reject) => {
//同期処理
}).then(() => {
// resolveの実行を待って非同期処理(正常終了すればこの処理が行われる)
}).catch(() => {
// rejectの実行を待って非同期処理(異常終了すればこの処理が行われる)
}).finally({
// resolveかrejectの実行を待って非同期処理(then or catchの後必ず行われる)
});
先程のコールバック版と同じ処理をPromiseで書くと以下のようになる。
function select_from_sampleTable(param) {
const result = ・・・省略・・・; //select処理
return new Promise(function(resolve, reject) { //Promiseを返す
if (result.length !== 0) {
resolve(result.length);
} else {
reject('該当データなし');
}
});
}
//呼び出す
select_from_sampleTable(5)
.then(num => {
console.log('取得件数は' + num + '件です。');
}).catch(error => {
console.log(error);
}).finally(() => {
console.log('処理を終了します。');
});
非常に分かりやすくなりました。
また、この処理を複数回行う場合、先ほどのコールバック版ではコールバック地獄に陥るが、Promise版では以下のようにすっきりと書くことができる。
※例えば、引数に金額を渡すとりんごを買っておつりを返してくれる関数
promiseBuyApple(1000)
.then(change => {
return promiseBuyApple(change);
}).then(change => {
return promiseBuyApple(change);
}).then(change => {
console.log('残金は' + change + '円です。');
}).catch(error => {
console.log('所持金が足りません。');
}).finally({
console.log('お買い物終了');
});
さらに同期処理的に書くことができるのが「async, await」
async, await
- async:関数の前にasyncが宣言された非同期関数(async function)はPromiseを返す
- await:呼び出す(Promiseを返す)関数の前にawaitを指定すると、Promiseの結果が返されるまで待つ(次の処理に進まない)
async function内でしか使えない
まず、以下はPromiseでの処理。
function getServerStatusCode() {
return new Promise(function(resolve, reject) {
axios.get("https://sample")
.then(response => resolve(response.status))
.catch(error => reject(error.response.status));
});
}
//呼び出す
getServerStatusCode()
.then(statusCode => console.log("OK:" + statusCode))
.catch(statusCode => console.error("NG: " + statusCode));
これをasync, awaitで書くと以下。
async function getServerStatusCode() {
try {
return (await axios.get("https://sample")).status;
} catch (err) {
throw err.response.status;
}
}
getServerStatusCode()
.then(statusCode => console.log("OK:" + statusCode))
.catch(statusCode => console.error("NG: " + statusCode));
直感的にかなり分かりやすくなりました。
また、上述のりんごを買う関数も、async, await を使うとより分かりやすく書くことができる。
async function buyApple() {
try {
let change = await promiseBuyApple(1000);
change = await promiseBuyApple(change);
change = await promiseBuyApple(change);
console.log('残金は' + change + '円です。');
} catch (err) {
console.log('所持金が足りません。');
}
console.log('お買い物終了');
}
//呼び出す
buyApple();
まとめ
改めてまとめると、頭が整理されました。
この認識間違っているよ、こう考えるといいよなどありましたら教えていただけると嬉しいです。