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 getCoffeeCallback(num, func) {
setTimeout(() => {
if (typeof num === "number") {
return func(`Here are your ${num} coffees!`, null);
} else {
return func(null, `${num} is not a number!`);
}
}, 3000);
}
getCoffeeCallback(2, (error, result) => console.log(error ? error : result));
getCoffeeCallback("butterfly", (error, result) => console.log(error ? error : result));
今回のコードは 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 の構文に対して別のアップグレードが提供されました。
仕組みは次のとおりです。
- async キーワードを使用して、getCoffeeAsync を非同期関数として定義します。
- これにより、後で await キーワードを使用して、定義した非同期関数を呼び出すことができます。
- await キーワードの次の行の処理は、await 行の処理が終了(解決)するまで 待機します。
- 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 で一般的なことです。
- 最初の例では、別のコールバック関数を返すプロセスを取ります。これを処理するには、すぐに呼び出される無名関数にレスポンスを渡し、その結果に対して JSON.parse() を呼び出し実行します(うわぁ…)。
- 後の 2 つの例では、別の Promise を返します!これを処理するには、前の Promise からのレスポンスに対して .json() というメソッドを呼び出します。次に、別の .then() メソッドをチェーンするか、別の await を使用して結果を取得します。
将来、API を使用する場合には、おそらく JSON を扱う必要があるので、.json() メソッドを覚えておくようにしましょう!
どれを使うべきなのか
Async/Await または Promise を使用するようにしましょう。JavaScript の構文は、妥当な理由があってアップグレードされていきます。
結論
どちらを使うべきでしょうか?一般的には、async/await または promises を使うべきです。構文の弱点についての知識が必要ですが、JavaScript 構文がアップグレードされるには、相応の理由があります!
async/await(及び、処理が少ない範囲では promises)の方が簡潔でエラーの処理が良く、デバッグが簡単です。
あなたのコードで、それらの使用を検討してみてください!
Code Chrysalis は日本・東京にあるプログラミングブートキャンプです。3 ヶ月間で強靭なソフトウェアエンジニアになるイマーシブは、オンサイトではもちろんリモートでも受講可能です。
👋 いつでもお気軽にお問い合わせください!
Twitter - YouTube - Instagram - Facebook - LinkedIn