|
| 1 | +# 第10章: 非同期処理(2)- Async/Await と Fetch API |
| 2 | + |
| 3 | +前回(第9章)では、JavaScriptの非同期処理の要である `Promise` について学びました。しかし、`.then()` チェーンが長く続くと、コードの可読性が下がる(いわゆる「コールバック地獄」に近い状態になる)ことがあります。 |
| 4 | + |
| 5 | +第10章では、この課題を解決するために導入された **Async/Await** 構文と、現代的なHTTP通信の標準である **Fetch API** について解説します。他の言語で同期的なコード(ブロッキング処理)に慣れ親しんだ方にとって、Async/Await は非常に直感的で扱いやすい機能です。 |
| 6 | + |
| 7 | +## Async/Await 構文 |
| 8 | + |
| 9 | +`async` と `await` は、ES2017で導入された `Promise` の**シンタックスシュガー(糖衣構文)**です。これを使うことで、非同期処理をあたかも「同期処理」のように上から下へと流れるコードとして記述できます。 |
| 10 | + |
| 11 | +### `async` 関数 |
| 12 | + |
| 13 | +関数宣言の前に `async` キーワードを付けると、その関数は自動的に **Promiseを返す** ようになります。値を `return` した場合、それは `Promise.resolve(値)` と同じ意味になります。 |
| 14 | + |
| 15 | +```js-repl:1 |
| 16 | +> async function getMessage() { return "Hello, Async!"; } |
| 17 | +undefined |
| 18 | +> // async関数は常にPromiseを返す |
| 19 | +> getMessage() |
| 20 | +Promise { 'Hello, Async!' } |
| 21 | +
|
| 22 | +> // 通常のPromiseと同じくthenで値を取り出せる |
| 23 | +> getMessage().then(v => console.log(v)) |
| 24 | +Promise { <pending> } |
| 25 | +Hello, Async! |
| 26 | +``` |
| 27 | + |
| 28 | +### `await` 式 |
| 29 | + |
| 30 | +`async` 関数の内部(またはモジュールのトップレベル)でのみ使用できるキーワードです。 |
| 31 | +`await` は、右側の Promise が **Settled(解決または拒否)されるまで関数の実行を一時停止** します。Promiseが解決されると、その結果の値を返して実行を再開します。 |
| 32 | + |
| 33 | +これは、C\# の `async/await` や Python の `asyncio` に慣れている方にはおなじみの挙動でしょう。 |
| 34 | + |
| 35 | +```js-repl:2 |
| 36 | +> function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } |
| 37 | +undefined |
| 38 | +> async function run() { |
| 39 | +... console.log("Start"); |
| 40 | +... await delay(1000); // 1秒待機(ここで実行が一時停止) |
| 41 | +... console.log("End"); |
| 42 | +... } |
| 43 | +undefined |
| 44 | +> run() |
| 45 | +Promise { <pending> } |
| 46 | +// (1秒後に表示) |
| 47 | +Start |
| 48 | +End |
| 49 | +``` |
| 50 | + |
| 51 | +## try...catch によるエラーハンドリング |
| 52 | + |
| 53 | +生の `Promise` では `.catch()` メソッドを使ってエラーを処理しましたが、Async/Await では、他の言語と同様に標準的な `try...catch` 構文を使用できます。これにより、同期エラーと非同期エラーを同じ構文で扱えるようになります。 |
| 54 | + |
| 55 | +```js:async_try_catch.js |
| 56 | +// ランダムに成功・失敗する非同期関数 |
| 57 | +function randomRequest() { |
| 58 | + return new Promise((resolve, reject) => { |
| 59 | + setTimeout(() => { |
| 60 | + const success = Math.random() > 0.5; |
| 61 | + if (success) { |
| 62 | + resolve("Success: データ取得完了"); |
| 63 | + } else { |
| 64 | + reject(new Error("Failure: サーバーエラー")); |
| 65 | + } |
| 66 | + }, 500); |
| 67 | + }); |
| 68 | +} |
| 69 | + |
| 70 | +async function main() { |
| 71 | + console.log("処理開始..."); |
| 72 | + try { |
| 73 | + // awaitしているPromiseがrejectされると、例外がスローされる |
| 74 | + const result = await randomRequest(); |
| 75 | + console.log(result); |
| 76 | + } catch (error) { |
| 77 | + // ここでエラーを捕捉 |
| 78 | + console.error("エラーが発生しました:", error.message); |
| 79 | + } finally { |
| 80 | + console.log("処理終了"); |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +main(); |
| 85 | +``` |
| 86 | + |
| 87 | +```js-exec:async_try_catch.js |
| 88 | +処理開始... |
| 89 | +エラーが発生しました: Failure: サーバーエラー |
| 90 | +処理終了 |
| 91 | +``` |
| 92 | + |
| 93 | +*(※注: 実行結果はランダムで成功する場合もあります)* |
| 94 | + |
| 95 | +## Fetch API によるHTTPリクエスト |
| 96 | + |
| 97 | +JavaScript(特にブラウザ環境や最近のNode.js)でHTTPリクエストを行うための標準APIが `fetch` です。以前は `XMLHttpRequest` という扱いづらいAPIが使われていましたが、現在は `fetch` が主流です。 |
| 98 | + |
| 99 | +`fetch` 関数は `Promise` を返します。 |
| 100 | + |
| 101 | +基本的な流れは以下の通りです: |
| 102 | + |
| 103 | +1. `fetch(url)` を実行し、レスポンスヘッダーが届くのを待つ。 |
| 104 | +2. Responseオブジェクトを受け取る。 |
| 105 | +3. Responseオブジェクトからメソッド(`.json()`, `.text()`など)を使ってボディを読み込む(これも非同期)。 |
| 106 | + |
| 107 | +```js:fetch_basic.js |
| 108 | +// 外部APIからJSONデータを取得する例 |
| 109 | +// (Node.js 18以上ではfetchが標準で使用可能です) |
| 110 | + |
| 111 | +async function getUserData(userId) { |
| 112 | + const url = `https://jsonplaceholder.typicode.com/users/${userId}`; |
| 113 | + |
| 114 | + try { |
| 115 | + // 1. リクエスト送信 (ネットワークエラー以外はrejectされない) |
| 116 | + const response = await fetch(url); |
| 117 | + |
| 118 | + // 2. HTTPステータスコードの確認 |
| 119 | + if (!response.ok) { |
| 120 | + throw new Error(`HTTP Error: ${response.status}`); |
| 121 | + } |
| 122 | + |
| 123 | + // 3. レスポンスボディをJSONとしてパース (これもPromiseを返す) |
| 124 | + const data = await response.json(); |
| 125 | + |
| 126 | + console.log(`Name: ${data.name}`); |
| 127 | + console.log(`Email: ${data.email}`); |
| 128 | + |
| 129 | + } catch (error) { |
| 130 | + console.error("Fetch failed:", error.message); |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +getUserData(1); |
| 135 | +``` |
| 136 | + |
| 137 | +```js-exec:fetch_basic.js |
| 138 | +Name: Leanne Graham |
| 139 | +Email: Sincere@april.biz |
| 140 | +``` |
| 141 | + |
| 142 | +### JSONデータの送信 (POST) |
| 143 | + |
| 144 | +データを送信する場合は、第2引数にオプションオブジェクトを渡します。 |
| 145 | + |
| 146 | +```js-repl:3 |
| 147 | +> const postData = { title: 'foo', body: 'bar', userId: 1 }; |
| 148 | +> await fetch('https://jsonplaceholder.typicode.com/posts', { |
| 149 | +... method: 'POST', |
| 150 | +... headers: { 'Content-Type': 'application/json' }, |
| 151 | +... body: JSON.stringify(postData) |
| 152 | +... }).then(res => res.json()) |
| 153 | +{ title: 'foo', body: 'bar', userId: 1, id: 101 } |
| 154 | +``` |
| 155 | + |
| 156 | +## Promise.all() と Promise.race() |
| 157 | + |
| 158 | +Async/Await は便利ですが、単純に `await` を連発すると、処理が**直列(シーケンシャル)**になってしまい、パフォーマンスが落ちる場合があります。複数の独立した非同期処理を行う場合は、並列実行を検討します。 |
| 159 | + |
| 160 | +### 直列実行(遅いパターン) |
| 161 | + |
| 162 | +```javascript |
| 163 | +// Aが終わってからBを開始する |
| 164 | +const user = await fetchUser(); |
| 165 | +const posts = await fetchPosts(); |
| 166 | +``` |
| 167 | + |
| 168 | +### Promise.all() による並列実行 |
| 169 | + |
| 170 | +複数のPromiseを配列として受け取り、**全て完了するのを待って**から結果の配列を返します。一つでも失敗すると全体が失敗(reject)します。 |
| 171 | + |
| 172 | +```js:promise_all.js |
| 173 | +const wait = (ms, value) => new Promise(r => setTimeout(() => r(value), ms)); |
| 174 | + |
| 175 | +async function parallelDemo() { |
| 176 | + console.time("Total Time"); |
| 177 | + |
| 178 | + // 2つの処理を同時に開始 |
| 179 | + const p1 = wait(1000, "User Data"); |
| 180 | + const p2 = wait(1000, "Post Data"); |
| 181 | + |
| 182 | + try { |
| 183 | + // 両方の完了を待つ |
| 184 | + const [user, post] = await Promise.all([p1, p2]); |
| 185 | + console.log("Result:", user, "&", post); |
| 186 | + } catch (e) { |
| 187 | + console.error(e); |
| 188 | + } |
| 189 | + |
| 190 | + // 本来なら直列だと2秒かかるが、並列なので約1秒で終わる |
| 191 | + console.timeEnd("Total Time"); |
| 192 | +} |
| 193 | + |
| 194 | +parallelDemo(); |
| 195 | +``` |
| 196 | + |
| 197 | +```js-exec:promise_all.js |
| 198 | +Result: User Data & Post Data |
| 199 | +Total Time: 1.008s |
| 200 | +``` |
| 201 | + |
| 202 | +### Promise.race() |
| 203 | + |
| 204 | +複数のPromiseのうち、**最も早く完了(または失敗)したもの**の結果だけを返します。タイムアウト処理の実装などによく使われます。 |
| 205 | + |
| 206 | +```js-repl:4 |
| 207 | +> const fast = new Promise(r => setTimeout(() => r("Fast"), 100)); |
| 208 | +> const slow = new Promise(r => setTimeout(() => r("Slow"), 500)); |
| 209 | +> await Promise.race([fast, slow]) |
| 210 | +'Fast' |
| 211 | +``` |
| 212 | + |
| 213 | +## この章のまとめ |
| 214 | + |
| 215 | + * **Async/Await**: `Promise` をベースにした糖衣構文。非同期処理を同期処理のように記述でき、可読性が高い。 |
| 216 | + * **Error Handling**: 同期コードと同じく `try...catch` が使用可能。 |
| 217 | + * **Fetch API**: モダンなHTTP通信API。`response.ok` でステータスを確認し、`response.json()` でボディをパースする2段構えが必要。 |
| 218 | + * **並列処理**: 独立した複数の非同期処理は `await` を連続させるのではなく、`Promise.all()` を使用して並列化することでパフォーマンスを向上させる。 |
| 219 | + |
| 220 | +## 練習問題 |
| 221 | + |
| 222 | +### 問題1: ユーザー情報の取得と表示 |
| 223 | + |
| 224 | +以下の要件を満たす関数 `displayUserSummary(userId)` を作成してください。 |
| 225 | + |
| 226 | +1. `https://jsonplaceholder.typicode.com/users/{userId}` からユーザー情報を取得する。 |
| 227 | +2. `https://jsonplaceholder.typicode.com/users/{userId}/todos` からそのユーザーのTODOリストを取得する。 |
| 228 | +3. 上記2つのリクエストは、**パフォーマンスを考慮して並列に実行**すること。 |
| 229 | +4. 取得したデータから、「ユーザー名」と「完了済み(completed: true)のTODOの数」を出力する。 |
| 230 | +5. 通信エラー時は適切にエラーメッセージを表示する。 |
| 231 | + |
| 232 | +```js:practice10_1.js |
| 233 | +``` |
| 234 | + |
| 235 | +```js-exec:practice10_1.js |
| 236 | +``` |
| 237 | + |
| 238 | +### 問題2: タイムアウト付きFetch |
| 239 | + |
| 240 | +指定したURLからデータを取得するが、一定時間内にレスポンスが返ってこない場合は「タイムアウト」としてエラーにする関数 `fetchWithTimeout(url, ms)` を作成してください。 |
| 241 | +*ヒント: `fetch` のPromiseと、指定時間後に reject するPromiseを `Promise.race()` で競走させてください。* |
| 242 | + |
| 243 | +```js:practice10_2.js |
| 244 | +``` |
| 245 | + |
| 246 | +```js-exec:practice10_2.js |
| 247 | +``` |
0 commit comments