「こう書けば動く」という状態でJavaScript/TypeScriptの非同期処理を実装してきました。
今回は「よく分からず使っている」を「なんとなく分かる」にするため、できるだけ簡単な例を作成して基本を整理します。
本記事で掲載しているコードブロックは、そのままブラウザの開発者ツール(F12)のコンソールで実行できます。ぜひ確認してみてください。

同期処理と非同期処理
同期処理とは、最初のコードから次のコードへと順番に実行されていくことです。
料理人がラーメンとチャーハンを作る際に、ラーメンを作り終えてからチャーハンを作るイメージです。
const makeRamen = () => {
console.log("ラーメンを作り始める");
console.log("ラーメンを作り終えた");
}
const makeFriedRice = () => {
console.log("チャーハンを作り始める");
console.log("チャーハンを作り終えた");
}
makeRamen();
makeFriedRice();
ラーメンを作り始める
ラーメンを作り終えた
チャーハンを作り始める
チャーハンを作り終えた
ですがラーメンを作ってる途中にチャーハンも作り始めた方が効率的です。
const makeRamen = () => {
console.log("ラーメンを作り始める");
setTimeout(() => {
console.log("ラーメンを作り終えた");
}, 1000); // 1秒後にラーメンを作り終える
}
const makeFriedRice = () => {
console.log("チャーハンを作り始める");
setTimeout(() => {
console.log("チャーハンを作り終えた");
}, 1000); // 1秒後にチャーハンを作り終える
}
makeRamen();
makeFriedRice();
ラーメンを作り始める
チャーハンを作り始める
# 1秒待つ
ラーメンを作り終えた
チャーハンを作り終えた
このように、処理が終わるのを待たずに次の処理を行うことを非同期処理と言います。
非同期処理の利点
この「実行順序が縛られない」という特徴は、ユーザー体験を向上しつつ、通信などの重たい処理を行う上で非常に重要になります。
例えば、モーダルボタンをクリックした時に以下の2つの処理を行いたい場合を考えます。
- データの通信
- モーダル画面の表示
この時、同期処理の場合はデータの通信を終えてからでないとレンダリングされません。
しかし、非同期処理の場合、データの通信完了を待たずにレンダリングすることができます。
その結果、モーダル表示を高速に行うことができ、ユーザー体験を向上させることができます。
非同期処理を行う機能
同期処理と非同期処理の違いがなんとなく分かってきたところで、非同期処理を使うための代表的な機能を整理します。
- setTimeout
- Promise
- async / await
setTimeout
setTimeoutは上記のラーメンとチャーハンの例でも出てきましたが、第2引数で受け取った時間だけ待った後に第1引数で受け取った処理を行う関数です。
処理を行わない間、別の処理を行うことができます。
const handleModalClick = () => {
setTimeout(() => {
console.log("データ通信完了");
},2000);
console.log("モーダル表示処理");
}
handleModalClick();
console.log("再レンダリング");
モーダル表示処理
再レンダリング
# 2秒待つ
データ通信完了
Promise
Promiseは非同期処理を簡単に記述できるよう用意されたオブジェクトです。
上記のモーダルボタンの例をPromiseで実装すると以下のようになります。
const handleModalClick = () => {
new Promise((resolve, reject) => {
setTimeout(() => {
console.log('データ通信完了');
}, 2000);
resolve();
});
console.log("モーダル表示処理");
}
handleModalClick();
console.log("再レンダリング");
モーダル表示処理
再レンダリング
# 2秒待つ
データ通信完了
一見コード量が増えただけのように見えますが、Promiseは複数の非同期処理をつなげて処理することを想定して作られています。これをPromiseチェーンと言います。
例えばデータを取得するのを待ってから変数にデータを格納したい場合を考えます。
const handleModalClick = () => {
new Promise((resolve, reject) => {
setTimeout(() => {
const response = {
id: 0,
data: "dummy data"
}
console.log('データ取得完了');
resolve(response);
}, 2000);
}).then((response) => {
const fetchedData = response.data;
console.log("データ格納完了")
});
console.log("モーダル表示処理");
}
handleModalClick();
console.log("再レンダリング");
モーダル表示処理
再レンダリング
# 2秒待つ
データ取得完了
データ格納完了
このようにresolveを使って正常終了したことを伝えつつ、データを次の非同期処理に渡すことができます。
thenの他にもerrorやfinallyがありますが今回は省略します。
async / await
async / awaitは、Promiseをさらに簡単に記述するために用意されたものです。
例えば、複数のPromiseチェーンを繋ぐ場合を考えます。
async/awaitを使わない場合(Promiseチェーン)です。
const fetchData1 = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Data 1 fetched");
resolve("inaka");
}, 1000);
});
};
const fetchData2 = (data1) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Data 2 fetched");
resolve(data1 + " de");
}, 1000);
});
};
const fetchData3 = (data2) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Data 3 fetched");
resolve(data2 + " Mac");
}, 1000);
});
};
fetchData1()
.then((data1) => {
console.log(data1);
return fetchData2(data1);
})
.then((data2) => {
console.log(data2);
return fetchData3(data2);
})
.then((data3) => {
console.log(data3);
console.log("All data fetched");
})
.catch((error) => {
console.error("Error fetching data:", error);
});
Data 1 fetched
inaka
# 1秒待つ
Data 2 fetched
inaka de
# 1秒待つ
Data 3 fetched
inaka de Mac
All data fetched
次にasync/awaitを使う場合です。
const fetchData1 = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Data 1 fetched");
resolve("inaka");
}, 1000);
});
};
const fetchData2 = (data1) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Data 2 fetched");
resolve(data1 + " de");
}, 1000);
});
};
const fetchData3 = (data2) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Data 3 fetched");
resolve(data2 + " Mac");
}, 1000);
});
};
const fetchAllData = async () => {
try {
const data1 = await fetchData1();
console.log(data1);
const data2 = await fetchData2(data1);
console.log(data2);
const data3 = await fetchData3(data2);
console.log(data3);
console.log("All data fetched");
} catch (error) {
console.error("Error fetching data:", error);
}
};
fetchAllData();
Data 1 fetched
inaka
# 1秒待つ
Data 2 fetched
inaka de
# 1秒待つ
Data 3 fetched
inaka de Mac
All data fetched
Promiseチェーンの場合、コードがネストしてしまい、処理の流れを追うのが難しくなります。特に複雑な非同期処理を扱うときには、then
の中にさらにthen
が続くため、可読性が低下します。
async/awaitを使う場合、非同期処理が同期処理のように記述できるため、コードがシンプルで読みやすくなります。エラーハンドリングもtry/catch
ブロックを使って行うことができ、処理の流れを把握しやすくなります。
このように、async/awaitを使うことで、複雑な非同期処理をより簡潔で理解しやすいコードにすることができます。
まとめ
今回は、「よく分からず使っていた」非同期処理を「なんとなく分かる」にするために簡単に実行可能な例を作成してまとめました。
ユーザー体験を向上させるために必要なんだということが分かっただけでも収穫です。
下記の記事により詳しい(もっと分かりやすい)解説が載っているのでぜひ参考にしてみてください。
参考文献


コメント