はじめに
JavaScriptの非同期処理メモ
参考
まとめ
- 前提としてJavaScript はシングルスレッドで、長時間ブロックすると UI を止めてしまうため、ブラウザ/Node が非同期処理を別スレッドで行い、完了通知をイベントループで扱う設計になっている。その結果、JavaScript が処理を待たずに次へ進むように見える
- だが、非同期処理を複数考える場合、終了の順番によって、結果が常にランダムになる(終わる順番が保証されていない) -> 非同期処理を順番に実行したい
- 非同期処理を順番に実行
-
コールバックで実現->Promiseのthenで実現->async/awaitで実現
-
- TypeScriptで非同期処理を扱う場合は
async/awaitを利用する -
async/awaitはPromiseのシンタックスシュガーなため、Promiseは理解する
非同期処理とは
時間のかかる処理(通信処理とかディスクアクセス処理とか)を待っている間に、他の処理を先に進める仕組み
await に到達した瞬間、その関数の「続きの処理」が一旦キャンセルされ、
イベントループの “待機キュー” に移される
その間、JS の実行スレッドは空く
→ 他の JS が実行できる(時間のかかる処理の完了を待たずに、次の処理へ進む)
→ React も UI も止まらない
**① 非同期処理(fetch, setTimeout, I/O)は
JavaScript の外側(ブラウザ / Node)が担当する**
ネットワーク通信(fetch、XHR)
タイマー(setTimeout, setInterval)
ファイル操作(Node の fs)
DNS、ソケット通信(Node)
これら 実際に重い処理は JavaScript が実行していない。
WebAPI や libuv が別スレッドで実行している
JS(シングルスレッド)は、非同期処理を "依頼" するだけ
const promise = fetch("/api") // ここで JS はすぐ次へ進む
fetch の実処理は ブラウザの別スレッドで進む。
JS(1スレッド)
-------------------------------------------------
foo()
↓
await fetch() ← ここで foo の続きだけ中断
↓
JS スレッドは空く(他のコードが動ける)
-------------------------------------------------
ブラウザ(複数スレッド)
-------------------------------------------------
ネットワークスレッドで HTTP 通信を処理
-------------------------------------------------
通信完了!
→ Promise resolve をイベントループに通知
-------------------------------------------------
JS (再開)
-------------------------------------------------
foo の続き(await の下のコード)を実行
OS
└─ Chrome
タブA
└─ JavaScriptスレッド
タブB
└─ JavaScriptスレッド
タブC
└─ JavaScriptスレ
非同期処理に順序を付けたい場合
非同期処理関数のコールバック関数
- 非同期処理の完了をコールバック関数で検知する仕組み
- 非同期処理を順番に実行する場合、非同期処理にコールバック関数を渡していた。そして、コールバック関数の中に次の非同期処理とそのコールバックをネストする形で記述して順番通りに非同期処理を実行していた。コールバック関数を使わないと、非同期処理が終わった順番で実行されてしまう。
- このコールバック関数の利用方法(コールバック関数の引数の数、型など)が各APIで違っていて、毎回調べる必要があった
しかし、非同期処理を重ねていくとネストがどんどん深くなってコードが見づらくなり、流れも追いにくくなります。これがコールバック地獄です。
Promise
Promise は「非同期で処理を走らせて、結果があとで手に入る」ってだけの仕組み。
-
Promiseを使った非同期処理の関数は、コールバック関数を引数に取るのではなくPromiseオブジェクトを返します。Promise は返された瞬間にもう実行されている
-
コールバック関数を使う場合は、API毎にコールバック関数をどのように渡すのか毎回調べる必要があったが、Promiseの使い方さえ覚えれば非同期処理を受け取れるようになった
非同期処理を扱う使い方がそもそも便利になった↑ -
もちろん、thenをつなげることで簡潔に非同期処理を順番に連鎖させることができるようになった
Promise は、その非同期処理が 完了するまで待機するための仕組み です。具体的には、非同期処理の結果を 後で使うことができる ようにするためのオブジェクトです。
Promise は以下の3つの状態を持っています
-
Pending(保留中): 非同期処理がまだ終わっていない状態
-
Fulfilled(完了): 非同期処理が成功した状態。結果の値を持っている
-
Rejected(拒否): 非同期処理が失敗した状態。エラーメッセージなどの理由を持っている
Promiseをイメージする
Promiseは非同期処理そのものを表している
const p = new Promise<number>((resolve) => {
// setTimeoutは非同期処理関数
// ライブラリもここに非同期処理が記述されていて、その中でresolve()を利用しているはず
setTimeout(() => {
resolve(100);
}, 3000);
});
p.then((num) => {
console.log(num);
});
- new Promiseされた時点で、引数の関数は実行されている
- resolveは内部的に用意される関数で、非同期処理が終わったらresolveを呼び出されることを求められている。なのでここでは3秒後にresolve()している
- resolve()が呼び出されると、Promiseが成功に解決される
new Promise (...) → Pending状態
↓ (時間が経つ)
resolve() 呼ばれる → Fulfilled状態に変化
↓
.then() の中身がキューに積まれ、後で実行
console.log("A")
Promise.resolve("B").then(console.log)
console.log("C")
// A -> C -> B
同期処理が終わったあとに then が実行される
- イメージ
- 例えばfetchはPromiseを返すが、非同期処理が引数のURLの通信で、それは即座に実行されていて、通信が終わればresolve()を実行していて、Promiseのthenが呼べる状態になっているはず、、
function fetchDataFromAPI(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then((response) => {
if (!response.ok) {
reject('APIリクエストが失敗しました'); // ステータスコードが 200 以外の場合はエラー
} else {
return response.json(); // JSON データをパース
}
})
.then((data) => {
resolve(data); // APIからのデータを成功として返す
})
.catch((error) => {
reject(`エラー: ${error.message}`); // エラーが発生した場合
});
});
}
fetchDataFromAPI('https://jsonplaceholder.typicode.com/posts/1')
.then((result) => {
console.log('データ取得成功:', result); // 成功した場合の処理
})
.catch((error) => {
console.error('データ取得失敗:', error); // 失敗した場合の処理
});
fetchがPromiseを返すイメージ
非同期処理で返す値の型がPromise
function fetch(url, options) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(options.method || 'GET', url)
for (const key in options.headers) {
xhr.setRequestHeader(key, options.headers[key])
}
xhr.onload = () => {
const response = {
ok: xhr.status >= 200 && xhr.status < 300,
status: xhr.status,
json: () => Promise.resolve(JSON.parse(xhr.responseText)),
text: () => Promise.resolve(xhr.responseText),
}
resolve(response)
}
xhr.onerror = () => reject(new TypeError('Network request failed'))
xhr.send(options.body)
})
}
Promise<T>
将来 T が返ってくる非同期な値
さらに便利になったasync/await
-
非同期処理をPromiseを意識しないで記述できるようになった。なので非同期処理を書く場合は大抵の場合
async/awaitを記述すると思われる -
async/awaitを使うと、より同期的な処理の書き方で非同期処理を順番に記述できるようになった -
エラー処理もtyr-catch処理で記述することができる
-
asyncもawaitも
Promiseを返すので、Promiseを理解していることが大事 -
awaitを使うと、asyncの関数が中断される、asyncの関数の次から実行される
function exampleFunction = async() => {
// asyncの中でawaitを書くことで同期的に処理をかけるようになる
console.log("Start of async function");
const result = await new Promise((resolve) => {
setTimeout(() => {
resolve("Resolved after 2 seconds");
}, 2000);
});
console.log(result);
console.log("End of async function");
}
console.log("Before calling async function");
exampleFunction();
console.log("After calling async function");
表示
Before calling async function
Start of async function
After calling async function
Resolved after 2 seconds
End of async function