非同期処理 (発展)
await、async
データベースへの接続・別のウェブサイトからの画像のダウンロード・ファイルの読み書き など、JavaScript外部の処理に時間のかかる操作は多数存在します。 それぞれの待ち時間のたびに処理を止めていては、処理が完了するのににとてつもない時間がかかってしまいます。 適切に最適化されたウェブサイトは「非同期処理」というものを利用して、読み込み時間を効果的に短縮しています。
JavaScript では、Promise
オブジェクトと、await
、async
というキーワードを使うことで、操作を非同期的に処理することが可能です。
ファイルの読み取りを非同期的に実行する readFile
関数を例にとって、非同期処理を書いてみましょう。
まずは次の 2 つのファイルを作り、main.mjs
を Node.js
で実行してみてください。
Sample Text
import * as fs from "node:fs/promises";
const text = fs.readFile("sample.txt", { encoding: "utf8" });
console.log(text);
上のコードを実行しても、Promise { <pending> }
と表示され、読み取ったファイルを使うことができません。
これは、fs.readFile
関数が Promise
オブジェクトというものを返す関数だからです。Promise
オブジェクトとは何でしょうか?
Promise
オブジェクトは、ファイルの読み取りのような処理を非同期的に処理するための、「完了に時間のかかる処理を完了しないまま扱う」オブジェクトです。
Promise
オブジェクトには、「状態」と「結果」の 2 種類の内部プロパティがあります。
Promise
オブジェクトの「状態」には「待機中」「成功」「失敗」の 3 種類の状態があり、「結果」には成功または失敗した時にその結果が代入されます。
上の例で Promise { <pending> }
と表示されたのは、text
がファイルから読み取った文字列ではなく、待機中 (pending) の Promise
オブジェクトだったからです。
では、Promise
オブジェクトの「結果」を取得するにはどのようにしたらいいのでしょうか?
import * as fs from "node:fs/promises";
const promise = fs.readFile("sample.txt", { encoding: "utf8" });
const awaitText = await promise;
console.log(awaitText);
とすると「結果」である Sample Text
という文字列を得ることができます。
このように Promise
オブジェクトに await
演算子を適用すると、処理を一時停止して、Promise
オブジェクトの状態が「成功」(fulfilled) になるまで文字通り「待つ」ことができるため、sample.txt
の中身を出力することができるのです。
しかしこのままでは、3 行目で全体の処理が止まってしまっているので、目的だった非同期処理ができません。
そこで、 async
キーワードをつけた関数を導入します。
import * as fs from "node:fs/promises";
async function logFile() {
const text = await fs.readFile("sample.txt", { encoding: "utf8" });
console.log(text);
}
logFile();
console.log("Doing another work...");
このように、関数の宣言の前に async
キーワードを付けると、「この関数は非同期的に処理する」と宣言することができます。
これにより、logFile
関数で処理を一時停止したときにメインの処理に戻って Doing another work...
と表示することができます。
Promise
オブジェクトの状態が「失敗」(rejected) になると、await
演算子は エラーを投げます。
try ~ catch
文を用いるとエラーを処理することができます。
import * as fs from "node:fs/promises";
try {
const text = await fs.readFile("bar.txt", { encoding: "utf8" });
// 存在しないファイルを読もうとすると「失敗」になる
console.log(text);
} catch (error) {
console.log("File: bar.txt might not exist.");
console.log(`Error message: ${error}`);
}
フロントエンド側の JavaScript や CommonJS では、await
は async
キーワードをつけた関数内部でしか適用できません。
フロントエンドなどで await
async
を使うには、上記のように非同期の関数に名前を付けて定義する以外にも、無名関数をその場で実行する即時関数というものを利用する方法もあります。
(async () => {
console.log("Start");
const text = await fs.readFile("sample.txt", { encoding: "utf8" });
console.log("End");
console.log(text);
})();
console.log("Async process");
練習問題
データベース?
3 秒かけて id からユーザーのデータを取得する Promise
オブジェクトを返すモジュール
const users = [
{ name: "田中", age: 18 },
{ name: "鈴木", age: 20 },
{ name: "佐藤", age: 19 },
{ name: "高橋", age: 21 },
{ name: "工藤", age: 17 },
];
/* 新しい Promise オブジェクトを作成して返しています。
詳しくは Promise コンストラクタの節で説明するので、
今は深く理解する必要はありません。 */
export default function fetchUserData(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (users[id]) resolve(users_db_side[id]);
else reject("User not found!");
}, 3000);
});
}
の fetchUserData
関数を使用して、田中さんの名前と年齢を画面に表示してみましょう。
解答例: データベース?
import fetchUserData from "./database.mjs";
async function showData(id) {
const user = await fetchUserData(id);
console.log(
`id: ${id} の人の名前は ${user.name} 、年齢は ${user.age} 歳です。`,
);
}
showData(0);
console.log("接続中...");
追加問題
データベース実装のコードを書き換えて、名前からユーザーを検索できるようにしてみましょう。
解答例:
async 関数の返り値
async
に処理すると宣言した関数の返り値は何になるのでしょうか?
// fetchUserData 関数の実装は省略
async function asyncFunction() {
const suzuki = await fetchUserData(1);
return suzuki;
}
console.log(asyncFunction());
実行すると、Promise { <pending> }
と表示されます。
実は、async
で宣言した関数はそれ自体が新しい Promise
オブジェクトを返すのです。
複数の非同期処理
import fetchUserData from "./database.mjs";
async function repeatAwait() {
for (let i = 0; i < 5; i += 1) {
const user = await fetchUserData(i);
console.log(user);
}
}
repeatAwait();
このコードを実行すると分かりますが、このように書くだけでは 5 個の処理を非同期的に処理できません。 なぜでしょうか?
これは、await
キーワードの、時間のかかる処理をその場で待つ性質によります。
await
キーワードの時点で処理が一時停止するので、同じ関数の中にawait
を連ねるだけでは結局 5 個の処理を待つことになってしまいます。
代わりに、このように書くと各処理を非同期的に待機できます。
import fetchUserData from "./database.mjs";
async function logUser(id) {
const user = await fetchUserData(id);
console.log(user);
}
for (let i = 0; i < 5; i++) {
logUser(id);
}
logUser
関数は 非同期的に処理する と宣言されているため、このように書くことで 5 個の操作を非同期的に処理することが可能になります。
Promise.all
非同期処理の結果を画面に表示したいだけなら上のように書けばいいですが、全ての非同期処理の結果を利用して別の処理を行いたいときもあります。
そんな時は、 Promise.all
関数を使うと、複数の Promise
オブジェクトをひとつの Promise
オブジェクトにまとめることができます。
上にある例で例えると、
import fetchUserData from "./database.mjs";
async function promiseAll() {
const array = [0, 1, 2, 3, 4];
// 配列を Promise オブジェクトに map する
const promiseArray = array.map((x) => fetchUserData(x));
// Promise.all(配列) とすると、Promise オブジェクトの配列を
// 1 つの Promise オブジェクトにまとめられる
const users = await Promise.all(promiseArray);
// ここに全ての結果を使う処理を書くことができる
// 例: 平均年齢を得る
let sumAge = 0;
for (const user of users) {
sumAge += user.age;
}
console.log(`ユーザーの平均年齢は ${sumAge / users.length} 歳です。`);
}
promiseAll();