📒 はじめに
Javascriptを扱う際によく聞く非同期処理とは一体何をしているのでしょうか.忘備録としてまとめてみます.
Promiseやasync / awaitについても触れていきます
例についてJSやTSでごちゃごちゃしているので注意.
📒 自己紹介
初めまして.趣味でweb開発を勉強している273*(ツナサンド) / Kei.と申します.関西の大学生です.最近はフルスタック開発やツール制作を行なっています.まだまだ初心者です.
📒 本題
こちらに本記事で登場したコードをまとめています.
非同期処理の基本概念
非同期処理とは,ある処理を実行している間に他の処理を並行して実行することができる仕組みです.
- 同期処理 vs 非同期処理:
同期処理: 順次実行される.
非同期処理: 時間のかかる処理が完了する前に他の処理が進む.
// demo1.ts
// 同期処理
function heavyTaskSync(){
console.log("同期タスク開始");
for (let i = 0; i < 1e9; i++) {
// 重い処理の模擬
}
console.log("同期タスク終了");
}
// 非同期処理
function heavyTaskAsync() {
console.log("非同期タスク開始");
return new Promise((resolve) => {
setTimeout(() => {
console.log("非同期タスク終了");
resolve();
}, 3000); // 3秒の遅延を模擬
});
}
console.log("開始")
heavyTaskSync();
// heavyTaskAsync();
console.log("終了")
heavyTaskSync()では
"開始"
"同期タスク開始"
"同期タスク終了"
"終了"
heavyTaskAsync()では
"開始"
"非同期タスク開始"
"終了"
"非同期タスク終了"
の様に出力されましたね.
両方とも上から実行されていますが,heavyTaskAsync()では"終了"と出力された後に "非同期タスク終了" と出力されましたね.heavyTaskAsync()を読み込んで処理が終わるのを待たず,並行で処理に進んでいます.これが非同期処理です.
身近で例えると,「料理」とかも非同期処理ですね.逆に同期処理は「洗濯」とかでしょうか?
なぜ非同期処理が重要なのか
- JavaScriptはシングルスレッドのため,もしAPIにデータを取得しに行くなど,サーバーとの通信が発生する処理を行う場合,通信の待ち時間なども含めてスレッドが占有されてしまい,他の処理が行えなくなります。そのような問題を解消するため,サーバーとの通信が発生する処理は非同期処理を利用します.
- (大量のデータを取得するなど,重い処理を同期処理で行うとページがフリーズしているように見えてしまいます(クリックやスクロールが効かなくなるなど).非同期でデータの取得を行うことで,取得中でも問題なくページの操作ができます.近頃のwebサービスには必ず導入されています.リアルタイムで動作するアプリケーションには必須になってきますね.)
- 他にも,CPUやネットワークのリソースを効率的に利用できるメリットもあります.
Promise
「非同期処理」と調べるとまず初めに出てくるのはPromiseですよね.非常に重要な概念です.Promiseの正体は「非同期で処理を行って完了したら値を返す」「将来,値を返す」です.Promise自体は値を返しません. 値を取得する方法は以下で説明しています.
Promiseは,以下のいずれかの状態を持ちます.
待機 (pending): 初期状態,成功も失敗もしていない.
履行 (fulfilled): 処理が成功して完了したことを意味する.
拒否 (rejected): 処理が失敗したことを意味する.
ざっくりとですが,非同期の関数の処理が始まったか,成功したか,失敗したかです.データフェッチで例えると,データの取得処理が成功したか,失敗したかです.公式ドキュメントの以下の図がわかりやすいですね.
そのため,非同期処理の結果に応じて,エラー処理などの処理を分岐させることができます.fulfilledの時はデータをパースしたり,rejectedの時は画面にエラーを表示したり....
実装は以下のように行います.
// demo2.ts
const asyncFunction = () => {
return new Promise((resolve, reject) => {
// 成功
resolve("処理が成功しました" );
// 失敗
reject();
});
};
asyncFunction()
.then((msg) => {
console.log(msg); // "処理が成功しました" が表示される
})
.catch((error) => {
console.error("エラー:", error); // エラーが表示される
});
Promiseの引数にresolve, reject<コールバック関数>を指定します.関数名は好きなように設定できます.一般的にはresolve,rejectが多い気がしますが,公式ドキュメントには resolveFunc や rejectFunc と書いてあります.中の処理に応じてresolveかrejectを呼び出します.処理が終わって Promise で返したい値が完成した時(fulfilledの時)に, resolve の引数にその値を入れて呼ぶことで,値をきちんと 解決 したものとして返すことができます.この時,初めて非同期処理後の値を取得できます.失敗した時(rejectedの時)にrejectの引数に値を入れて呼ぶことで,処理が失敗したものとして返すことができます.
これらの処理が終わったあとに,さらに連続して処理を行いたい場面もあります(データを変数に入れたりコンソールに出したり).
その場合はPromiseチェーンで繋いであげることで実装できます.主に使われるのはthen(), catch(), **finally() ** のメソッドです.
.then() / .catch(): はチェーンとして実行され,値やエラーを次の .then() / .catch() に渡す.
finally(): 最後に実行され,結果の値やエラーに関係なく呼び出される.
以下のように繋げて使用します.
// demo3.ts
const somePromise = (num: number): Promise<string> =>
new Promise((resolve, reject) => {
console.log(`入力:${num}`);
if (num >= 10) {
resolve("10以上です.順:");
} else {
reject("10より小さいです.順:");
}
})
.then((val) => {
// 処理 A
const item_a = val + "A->";
return item_a;
})
.then((val) => {
// 処理 B
const item_b = val + "B->";
return item_b;
})
.catch((err) => {
// エラー処理 C
const item_c = err + "C->";
return item_c;
})
.then((val) => {
// 処理 D
const item_d = val + "D";
return item_d;
})
.finally(() => {
// 終了処理 E
console.log("結果");
});
const item: number = ⬜️
somePromise(item).then((val) => {
console.log(val);
});
resolveの引数に入った値はそのままPromiseチェーンのthenメソッドの引数に引き継がれます.rejectも同様にcatchメソッドに引き継がれます.これにより非同期で処理を続行できます.
ではこのコードですが,⬜️に7が入った場合と12が入った場合はどのように出力されるでしょうか.
動かして試してみましょう.
それぞれ以下のように出力されましたね.
入力:7
結果
10より小さいです.順:C->D
入力:12
結果
10以上です.順:A->B->D
値が引き継がれているのが確認できました.finally()は処理に関係なく最後に実行されます.
このようにして非同期で処理をおこうことができます.
async / await
とろこで,demo3.ts
をパッと見てみてどんな処理が行われているか理解できますか?Promiseはコールバック地獄を解決するために開発されたものですが,Promiseチェーンが複雑化すると,コードの見通しが悪く,理解に時間がかかってしまいますね.さらにエラーハンドリングも面倒になります.
Promiseを使った非同期処理をさらに簡潔で読みやすく記述できるように開発された構文糖衣がasync / await
です.
demo3.ts
をasync / awaitを使用して書き換えてみると以下のようになります.
// demo4.ts
const somePromise = async (num: number): Promise<string> => {
console.log(`入力:${num}`);
if (num >= 10) {
return "10以上です.順:"
} else {
throw "10より小さいです.順:";
}
};
const processWithAwait = async (num: number) => {
let val: string; // 結果を格納する変数
try {
// somePromise を実行し、成功した場合に値を取得
val = await somePromise(num);
// 処理 A
val += "A->";
// 処理 B
val += "B->";
// 処理 D
val += "D";
} catch (err: any) {
// エラー処理 C
val = err + "C->";
// 処理 D
val += "D";
} finally {
console.log("結果");
console.log(val);
}
};
processWithAwait(⬜️);
動かして試してみましょう.
try-catchを使用し,try ブロック内で非同期処理を実行,エラーが発生した場合は catch ブロックでエラーを取得して値を生成します.finallyで終了処理を記述しています.
値の処理部分もvalに直接追加しているように非常に簡潔になりましたね.processWithAwait
の中身を見ていただきたいのですが,変数valに非同期処理のsomePromise
(後述)を代入していますね.普通にlet val = somePromise(num);
でもいい気がします.ここで,思い出していただきたいのは,Promiseはあくまで「将来,値を返す」でしたね.直接valに代入すると「将来,値を返すもの(Promiseを返す)」を代入していることになります.そのまま非同期処理後の値を取ってくるわけではないのです.async / await
を使用する方法では,例のようにawaitを使用することで非同期処理後の値を取得することができます.awaitには非同期処理が終わるのを待って,その結果を受け取るという機能があります.値を受け取らないと,次の処理(valに値を追加)には移れないわけです.awaitを消して実行すると[object Promise]A->B->D
と出力されるのが確認できます.
ではどこでもawaitをつければいいかと思いますが,使用できる場所が限られています.awaitはasync関数の中でしか使用できません.引数宣言の前にasync
をつけてあげます.
asyncには,その関数の返り値を常にPromiseで返すようにし(非同期関数にする),awaitを使用できるようになる機能があります.なのでsomePromiseは非同期関数になっているわけですね.if文で成功パターンと失敗パターンのPromiseを返す関数です.
因みに,即時実行関数を使うことでも同期的なスコープの中に非同期処理を記述できます.個人的にあまり使う時はない気がします.普通にasync関数を定義して呼び出してあげるだけでいいですね.デモ
// demo5.ts
const asyncFunc = () => {
return new Promise((resolve) => {
setTimeout(() => resolve("データ取得完了"), 2000); // "2秒待機"
});
};
(async () => {
const result = await asyncFunc();
console.log(result);
})();
// or
const test = async () => {
const result = await asyncFunc();
console.log(result);
};
test();
console.log("データ取得開始");
Reactのコンポーネントの中で,ボタンクリック時の動作やデータ取得をする際にasync関数を多用します.
まとめ
- 非同期処理とは、処理の完了を待たずに次の処理を進めることができる処理のこと.
- 実装するにはPromiseを使用し,続きの処理はPromiseチェーンで実装する.
- async/awaitを使用することで,非同期処理を同期処理のように簡潔な記述が可能.
- asyncの役割は,関数を非同期関数にし,awaitを使用できるようにする.
- awaitの役割は,Promiseの完了を待ち,その結果を取得する.
📒 非同期処理を行うための関数やAPI
JSに標準で搭載されている非同期処理を行うための関数やAPIを紹介します.
タイマー系: setTimeout, setInterval
HTTP通信: fetch, WebSocket
ファイル操作: fs.readFile(Node.js)
Promiseベース: Promise, async/await
イベントリスナー: DOMイベントやNode.jsイベント
アニメーション制御: requestAnimationFrame
その他: process.nextTick, AbortController
📒 最後に
非同期処理について復習を兼ねて記事にしてみました.まだまだ初心者なので間違いやご指摘がありましたらコメントをいただけると幸いです.長文になりましたが最後まで読んでいただきありがとうございました!それではまたどこかで会いましょう😉
📒 参考文献