はじめに
今回は自分がちょっと昔、非同期処理を学び始めた際につまずきやすいと感じた、Promiseやasync/awaitといった概念を、寿司作りのプロセスを通して解説していきます。
JavaScriptやTypeScriptを始めるにあたり、APIからのデータ取得などにより非同期処理は避けて通れない重要な概念です。非同期処理とは、プログラムが一部のタスクをバックグラウンドで実行し、そのタスクが完了するのを待たずに次のタスクに移ることを指します。しかし、実際にコードを書いていても 「この処理は同期的に動作しているのか、非同期的に動作しているのか?」 と混乱してしまうことがありました。
ある日、寿司を食べながら寿司職人の仕事を考えていたら、その混乱がすっきり解消される瞬間がありました。 その体験を基に、TypeScriptにおける非同期処理を例を交えながら紹介します。
実行環境
すぐにTypeScriptを動かす環境を持っていない人は、Playgroundを使うと簡単に動作確認ができます。
寿司作りのレシピ
今回は、寿司作りの過程を用いて非同期処理の概念を解説します。まずは使用するレシピを見てみましょう。
- 寿司飯を炊く (約15秒)
- マグロをさばく (約10秒)
- サーモンを準備する (約5秒)
- エビを準備する (約3秒)
- 寿司を握る (寿司飯とネタが準備でき次第)
これらの工程を順番にひとつづつ行うと、合計で33秒かかります。しかし、非同期処理を使うことで効率的に作業を進めることができます。
非同期処理とは
非同期処理とは、複数の処理を同時に進行させつつ、それぞれが終了するタイミングで結果を得るための処理方法です。この概念を理解することで、効率的なプログラムを書くことができます。
JavaScriptやTypeScriptでは、この非同期処理を扱うために Promise
というオブジェクトが提供されています。
寿司飯を炊こう〜Promiseとは〜
Promise
は非同期処理の結果を表すオブジェクトです。Promiseは「解決」(成功した場合)または「拒否」(エラーが発生した場合)の状態になると、それに応じた処理を.then
や.catch
メソッドを使って行うことができます。
以下に「寿司飯を炊く」という非同期処理をPromiseを使って書いた例を示します。
function prepareRice(): Promise<string> {
return new Promise((resolve) => {
console.log("寿司飯を炊きます...");
setTimeout(() => {
console.log("寿司飯が炊き上がりました。");
resolve("寿司飯");
}, 15000);
});
}
prepareRice().then((rice) => {
console.log(`準備したもの: ${rice}`);
});
実行結果:
寿司飯を炊きます...
// 15秒後
寿司飯が炊き上がりました。
準備したもの: 寿司飯
このコードでは、寿司飯を炊くのに15秒かかると仮定しています。非同期処理はsetTimeout
で表現され、setTimeout
の処理が終了するとresolve
が呼ばれてPromiseが「解決」します。その後、.then
メソッドが実行され、console.log
により"準備したもの: 寿司飯"と出力されます。
寿司飯を炊こう〜async/awaitとは〜
async/await
は Promise
をより直感的に書くための構文です。上のコードを async/await
を使って書き換えてみましょう。
function prepareRice(): Promise<string> {
return new Promise((resolve) => {
console.log("寿司飯を炊きます...");
setTimeout(() => {
console.log("寿司飯が炊き上がりました。");
resolve("寿司飯");
}, 15000);
});
}
async function prepareSushi() {
console.log("寿司を作ります...");
const rice = await prepareRice();
console.log(`準備したもの: ${rice}`);
}
prepareSushi();
寿司を作ります...
寿司飯を炊きます...
// 15秒後
寿司飯が炊き上がりました。
準備したもの: 寿司飯
awaitは非同期処理(Promise)の前に置くことで、その非同期処理の結果を得るまで待つことができます。
寿司ネタを作って全部揃ったら寿司を作ろう〜Promise.allとは〜
寿司を握り始めるためには、それぞれのネタと寿司飯を準備できるのを待つ必要があります。(全ての非同期処理が終了するのを待つ必要があります。)そのためにPromise.allを使います。
Promise.allは複数のPromiseを同時に実行し、全てが解決されたときに結果を配列として返す関数です。どれか一つでも拒否(エラー)された場合、Promise.all自体が拒否されます。
以下に寿司を握るまでの全工程をコードにした例を示します。
function prepareRice(): Promise<string> {
return new Promise((resolve) => {
console.log("寿司飯を炊きます...");
setTimeout(() => {
console.log("寿司飯が炊き上がりました。");
resolve("寿司飯");
}, 15000);
});
}
function prepareNeta(neta: string, time: number): Promise<string> {
return new Promise((resolve) => {
console.log(`${neta}を準備します...`);
setTimeout(() => {
console.log(`${neta}が準備できました。`);
resolve(neta);
}, time);
});
}
async function prepareSushi() {
console.log("寿司を作ります...");
const rice = prepareRice();
const maguro = prepareNeta("マグロ", 10000);
const salmon = prepareNeta("サーモン", 5000);
const ebi = prepareNeta("エビ", 3000);
const ingredients = await Promise.all([rice, maguro, salmon, ebi]);
console.log(`寿司を握ります: ${ingredients.join(", ")}で寿司を作りました!`);
}
prepareSushi();
寿司を作ります...
寿司飯を炊きます...
マグロを準備します...
サーモンを準備します...
エビを準備します...
// 3秒後
エビが準備できました。
// 2秒後
サーモンが準備できました。
// 5秒後
マグロが準備できました。
// 5秒後
寿司飯が炊き上がりました。
寿司を握ります:寿司飯, マグロ, サーモン, エビで寿司を握りました!
このコードでは、寿司飯と3つのネタを準備するためのPromiseを同時に生成し、それぞれのPromiseが解決するのをPromise.allで待っています。全てが解決されたら、寿司を握る処理を実行します。これにより、非同期に行える作業は同時に始まり、全てが完了するのを効率的に待つことができます。
お寿司作りと非同期処理
お寿司作りを通して非同期処理を説明してみました。お寿司を食べる際には是非、寿司職人の創意工夫と、非同期処理を思い出してみてください。