非同期 JavaScriptチュートリアル:
非同期 JavaScript
JavaScriptのようなシングルスレッド言語では非同期コードを明示的に処理する必要があります。そうでなければ、長時間実行される処理により同期とブロッキングで説明したパフォーマンスの問題を引き起こします。
このチュートリアルでは、スケーラブルなJavaScriptアプリケーションを書くために、非同期処理の3つの方法(コールバック、Promises、および Async/Await)を理解することに焦点を当てます。
> これはコードクリサリスのイマーシブ・ブートキャンプのプレコースの必読書で、フルスタックソフトウェア・エンジニアリングコースを受講する前に、すべての学生が完了させなければならない一連の課題、プロジェクト、評価、作業をまとめたものです。
始める前に
- Node.jsをインストールする必要があります。Node.jsとは、ブラウザ上だけではなく、コンピュータ上でJavaScriptを実行できるようにするプログラムです。これは、特にファイルシステムを制御できることを意味します。もしあなたが初めてNodeを使うのであれば、[Node School]をチェックしてみてください。
- これはチュートリアルはハンズオンなので、自分でコードを実行する必要があります。
- `fs`モジュールについて学ぶには、Synchronous and Blocking JavaScriptを読むことをおすすめします。
- ドキュメントを読んで学ぶことは、ソフトウェアエンジニアにとって非常に重要なスキルです。このチュートリアルを進めながら、[Node.js documentation]に目を通す練習をしてください。実際に使用するNode.jsのバージョンと、ドキュメントのバージョンに注意してください。
高階関数:高次の目的のために
理論的解釈
JavaScript などのシングルスレッド言語では、非同期コードは明示的に処理する必要があります。そうしないと、長時間実行される処理により、[同期とブロッキング]で説明したパフォーマンスの問題が発生します。
このレッスンでは、スケーラブルな JavaScript アプリケーションを作成できるように、非同期呼び出しを処理する 3 つの方法(コールバック、Promises、および Async/Await)を理解することに焦点を当てます。
バックグラウンド
同期処理の実行中に、振る舞いを抽象化するために高階関数がどのように役立つかはすでに見てきましたが、高階関数の最も重要な利用ケースは、*非同期処理* を行うときです。
なぜ非同期処理を扱うことが重要なのか?JavaScript はシングルスレッドで動作する言語であるため、一度に 1 つのことしかできません [同期とブロッキング]を復習してください)。これは、アプリケーションが非同期処理に依存している場合に起こるうる問題です。
開発者が JavaScript の非同期処理のコードを扱う最も一般的なケースは、アプリケーションプログラミングインターフェイス(API) を使用する場合です。サーバを学習するときに独自の API を構築し、サードパーティーの API を扱うことも今後学習する予定です。
一般的に言って、API はさまざまなコンポーネント間で明確に定義された通信方法のまとまりです。 API は**リクエストを受け取り**
(例:「天気は?」)、**レスポンスを送信します**(「22 ℃ です!」)。
この通信には時間がかかる場合があります。また、他のコードがリクエストに対するレスポンスに依存している場合、問題が起こる可能性があります。そのため、JavaScript にはこの状況に対処する方法が用意されています。
非同期処理を扱わない方法
以下に、非同期処理のコードを扱う 3 つの事例を示します。これらの例では、実際には API を使用していないため、[setTimeout] を使用して遅延を発生させています。このメソッドは、関数と数値(`n`)の 2 つの引数を持ちます。次に、`n` ミリ秒後に関数を呼び出します。
何が起こるかを示す前に、やってはいけないことを以下に示します。以下の関数を見てみましょう。3 秒後に結果が返されるはずです。コードをコンソールに貼り付けて実行するとどうなりますか?
```
function getCoffee(num) {
setTimeout(() => {
if (typeof num === "number") {
return `Here are your ${num} coffees!`;
} else {
return `${num} is not a number!`;
}
}, 3000);
}
console.log(getCoffee(2));
console.log(getCoffee("butterfly"));
```
`undefined` を返します。`getCoffee()` がレスポンスを受け取る前に、`console.log()` が実行されてしまいます。これは問題ですね。
オプション 1:コールバック
ES6 以前は、非同期処理のコードはコールバックを介して処理されていました。
以下の例では、`getCoffeeCallback` は、コーヒーの数とコールバック関数を引数に持つ関数として定義されています。さらに、このコールバック関数は `error` と `result` の 2 つの引数を持ちます。リクエストの成否に応じて、結果もしくはエラーの場合に呼び出されるコールバック関数(その他の引数は null)を返します。
コンソールで、以下のコードを実行してみましょう!
```
function getCoffeePromise (num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (typeof num === "number") {
resolve(`Here are your ${num} coffees!`);
} else {
reject(`${num} is not a number!`);
}
}, 3000);
});
};
getCoffeePromise(2).then(result => console.log(result)).catch(error => console.log(error));getCoffeePromise("butterfly")
.then(result => console.log(result))
.catch(error => console.log(error));
```
今回のコードは `undefined` を返しません。どうしてでしょうか?その理由は、[イベントループ]と呼ばれるもののおかげです。結論から言うと、`getCoffeeCallback` に渡された関数がキューに追加されました。JavaScript のイベントループはそのキューと連携して、適切なタイミングでコールバック関数を実行します。
もう 1 つの例を見てみましょう。以前に、`readFileSync` を使用してファイルを同期的に読み込んだことを覚えていますか?ファイルの読み取りは長時間の処理となる *可能性がある* ため、Node は同期と非同期の両方の実装を提供しています。非同期のバージョンは `readFile` になります。
```
const fs = require('fs');
const result = fs.readFile('index.js', 'utf8');
console.log(result);
```
`readFile` を使用すると、結果として `undefined` が得られます。
[Node.js ドキュメント - fs.readFile]をチェックすると、その理由がわかります:`readFile` には `error` パラメータと `result` パラメータを渡すコールバック関数が 3 番目の引数として必要です。
`getCoffeeCallback` 関数と同じように、必要な引数(コールバック関数です!)を渡しましょう。
```
const fs = require('fs');
fs.readFile('index.js', 'utf8', (error, result) => console.log(error, result));
```
わーい!動きましたね。
オプション 2: Promises
ES6 では、非同期処理を行うための非常に優れた方法、Promises を標準化しました。
以下のリファクタリングされたコードを見てみましょう。高階関数 `getCoffeePromise` は、`getCoffeeCallback` 関数とほとんど同じに見えると気付くでしょう。ただし、今回はコールバック関数を渡さずに、`Promise`(`new` キーワードで作成)を返します。この Promise には、2 つの引数(`resolve` と呼ばれる関数と `reject` と呼ばれる関数)を持つ関数が渡されます。
`getCoffeePromise` の呼び出し方も少し異なります。コールバック関数を渡さない代わりに、別の方法でレスポンスを処理する必要があります。つまり、`.then()`と `.catch()`をチェーンさせる必要があります。
- `.then()` は、`resolve` 関数に渡されたものをすべて出力します。
- `.catch()` は、`reject` 関数に渡されたものをすべて出力します。
コンソールで、以下のコードを実行してみましょう!
```
function getCoffeePromise (num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (typeof num === "number") {
resolve(`Here are your ${num} coffees!`);
} else {
reject(`${num} is not a number!`);
}
}, 3000);
});
};
getCoffeePromise(2).then(result => console.log(result)).catch(error => console.log(error));
getCoffeePromise("butterfly")
.then(result => console.log(result))
.catch(error => console.log(error));
```
注:Promise を呼び出す場合、最初の例のように 1 行で記述することも、2 番目の例のように改行して複数行で記述することもできます。2 番目の例のように改行して複数行で記述する場合は、チェーンの間にスペース、コメント、セミコロンを追加しないようにしましょう!
[Promises]の詳細については、こちらを参照してください。
オプション 3:async / await
ES7 では、Promises の構文に対して別のアップグレードが提供されました。
仕組みは次のとおりです。
1. `async` キーワードを使用して、`getCoffeeAsync` を非同期関数として定義します。
2. これにより、後で `await` キーワードを使用して、定義した非同期関数を呼び出すことができます。
3. `await` キーワードの次の行の処理は、`await` 行の処理が終了(解決)するまで *待機します*。
4. `try` および `catch` キーワードを使用して、非同期処理の成功と失敗をハンドリングすることができます。
このアップグレードのすばらしい点は、`await` キーワードにあります。通常、コールバックまたはチェーンされた Promise メソッドの外部で `console.log()` を書き込むと、`undefined` が返されます。一方で、`await` キーワードは、非同期処理が終わるまでコードの実行を**停止**するため、上述の問題は発生しません!
それでは、以下のコードをブラウザで試してみてください!
```
const getCoffeeAsync = async function(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (typeof num === "number") {
resolve(`Here are your ${num} coffees!`);
} else {
reject(`${num} is not a number!`);}
}, 3000);
});
}
const start = async function(num) {
try {
const result = await getCoffeeAsync(num);
console.log(result);
} catch (error) {
console.error(error);
}
}
start(2);
start("butterfly");
```
注:この例では、`getCoffeeAsync` 関数内で、いまだに Promise を返していることに気付くでしょう。残念ながら、`setTimeout` は明示的に async/await をまだサポートしていません!そのため、上記のようなラッパーを用意してコードを引き続き動作させる必要があります。
[Async/Await]の詳細については、こちらを参照してください。
3 つの方法(コールバック、Promise、async/await)の比較
[Pokemon API] に対して同じリクエストを行うために、非同期 JavaScript を処理する 3 つの方法がどのように活用されるか見てみましょう。
Pokemon の API 呼び出しを行うには、[XMLHttpRequest](コールバックを使用)、または、[Fetch](Promises を使用)を使います。
それでは、ブラウザでテストしてみましょう!
1. Callback
```
function request(callback) {
const xhr = new XMLHttpRequest();
xhr.open("GET", "<https://pokeapi.co/api/v2/ability/4/>", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
(function(response) {
callback(JSON.parse(response));
})(xhr.responseText);
} else {
callback(xhr.status);
}
};
xhr.send();
}
request((error, result) => console.log(error ? error : result));
```
2. Promise
```
fetch("<https://pokeapi.co/api/v2/ability/4/>")
.then(response => response.json())
.then(jsonResponse => console.log(jsonResponse))
.catch(error => console.log(error));
```
3. Async / Await
```
async function request() {
const response = await fetch("<https://pokeapi.co/api/v2/ability/4/>");
const jsonResponse = await response.json();
console.log(jsonResponse);
}
request();
```
それぞれの方法には、ちょっとした違いがあるように見えます。どうしてでしょうか?
これらのリクエストでは、JSON をレスポンスとして返しますが、これらのレスポンス処理には時間がかかります。これは、多くの API で一般的なことです。
1. 最初の例では、別のコールバック関数を返すプロセスを取ります。これを処理するには、すぐに呼び出される無名関数にレスポンスを渡し、その結果に対して `JSON.parse()` を呼び出し実行します(うわぁ…)。
2. 後の 2 つの例では、別の Promise を返します!これを処理するには、前の Promise からのレスポンスに対して `.json()` というメソッドを呼び出します。次に、別の `.then()` メソッドをチェーンするか、別の `await` を使用して結果を取得します。
将来、API を使用する場合には、おそらく JSON を扱う必要があるので、`.json()` メソッドを覚えておくようにしましょう!
どれを使うべきなのか
Async/Await または Promise を使用するようにしましょう。JavaScript の構文は、妥当な理由があってアップグレードされていきます。
結論どちらを使うべきでしょうか?一般的には、async/awaitまたはpromisesを使うべきです。[構文の弱点]についての知識が必要ですが、JavaScript構文がアップグレードされるには、相応の理由があります!
async/await(及び、処理が少ない範囲ではpromises)の方が簡潔でエラーの処理が良く、デバッグが簡単です。
あなたのコードで、それらの使用を検討してみてください!