ウェブアプリケーションでスムーズなユーザー体験を提供するためには、JavaScriptの非同期処理が非常に重要です。
非同期処理とは、ある作業が完了するのを待たずに次の作業が始まることを言います。
これにより、アプリケーションがより速く感じられ、ユーザーが長時間待たされることが少なくなります。
しかし、非同期処理は便利ですが、処理の順序が入れ替わってしまう問題があります。
この記事では、setTimeoutを用いて非同期処理で起こり得る順序の問題を模擬し、どのようにしてこれを理解し管理するかを解説します。非同期処理をどのようにコントロールするか、具体的な手法としてコールバック、Promise、そしてasync/awaitの使用方法を紹介します。
非同期処理の一般的な問題
今回は、setTimeoutを使用して非同期処理の一例を紹介しますが、これは実際のウェブアプリケーションにおけるサーバーへの問い合わせやデータダウンロードのような処理を模擬しています。
setTimeoutを用いることで、これらの時間がかかる操作が完了するのを待たずに、次の処理が進行してしまうことを再現します。
上から、
・処理命令開始
・タスク1完了
・タスク2完了
・処理命令終了
の順番に表示するプログラムです。
<button onclick="startTasks()">処理を開始</button>
<div id="output"></div>
// ボタンを押したら開始する処理
function startTasks() {
updateOutput("処理命令開始");
task1();
task2();
updateOutput("処理命令終了");
}
// 500ミリ秒遅延する処理
function task1() {
setTimeout(() => {
updateOutput("タスク1完了");
}, 500);
}
// 100ミリ遅延する処理
function task2() {
setTimeout(() => {
updateOutput("タスク2完了");
}, 100);
}
// テキストを画面に表示する処理
function updateOutput(message) {
const output = document.getElementById('output');
output.innerHTML += '<p>' + message + '</p>';
}
See the Pen Untitled by kokusyo (@kokusyo) on CodePen.
処理の順番が
・処理命令開始
・処理命令終了
・タスク2完了
・タスク1完了
という風に入れ替わってしまったことが分かります。
このように非同期処理は便利ですが、処理の順序が入れ替わってしまう問題があり、混乱を招くことがあります。
今回はこの非同期処理を適切な順序で実行する方法を解説していきます。
コールバック
古い方法ですが、ある処理が終わったら次に何をするかを関数として書きます。
コールバック関数を適切に配置することで、一つのタスクが完了した後に次のタスクを開始するように設計することが可能です。
これにより、タスクが期待通りの順序で完了するようになります。
コールバック関数
コールバック関数は、あるタスクが終わったあとに実行される特別な命令(関数)です。
これを使うことで、タスクが完了するのを待って、次に何をすべきかをプログラムに教えることができます。
See the Pen Untitled by kokusyo (@kokusyo) on CodePen.
処理の順番が想定通りに出力されました。
・処理命令開始
・タスク1完了
・タスク2完了
・処理命令終了
ソースは次のとおりです。
HTMLに関しては同じなので諸略しています。
// ボタンを押したら開始する処理
function startTasks() {
updateOutput("処理命令開始");
task1(() => {
task2(() => {
updateOutput("処理命令終了");
});
});
}
// 500ミリ秒遅延する処理
function task1(callback) {
setTimeout(() => {
updateOutput("タスク1完了");
callback(); // ここでtask2を呼び出します
}, 500);
}
// 100ミリ秒遅延する処理
function task2(callback) {
setTimeout(() => {
updateOutput("タスク2完了");
callback(); // コールバックを呼び出して「処理命令終了」が表示されるようにします
}, 100);
}
// テキストを画面に表示する処理
function updateOutput(message) {
const output = document.getElementById('output');
output.innerHTML += '<p>' + message + '</p>';
}
コールバックは便利ですが、多用するとコードが読みにくくなる「コールバック地獄」という状況を引き起こす可能性があります。ネストされたコールバックが多くなると、めちゃめちゃよ見づらいです。
より現代的な方法を次のPromiseでご紹介します。
Promise
非同期処理が終わるのを「約束」するオブジェクトを使います。.then()を使って次の処理を繋げることができます。
Promiseの導入により、JavaScriptでの非同期処理のコードを書く際の複雑さが減り、より効率的で保守しやすいアプリケーション開発が可能になりました。
See the Pen Untitled by kokusyo (@kokusyo) on CodePen.
// ボタンを押したら開始する処理
function startTasks() {
updateOutput("処理命令開始");
// task1を実行して、Promiseが解決されるのを待ちます。
task1().then(() => {
// task1が完了したら、task2を実行します。
// task2もPromiseを返すので、これが解決されるのを待ちます。
return task2();
}).then(() => {
// task2の完了後、最終的に「処理命令終了」を表示します。
updateOutput("処理命令終了");
});
}
// 500ミリ秒遅延する処理
function task1() {
// return new Promise(resolve => {...}) - この行は新しいPromiseオブジェクトを生成して返します。
// resolveは、Promiseが成功したときに呼び出す関数です。
return new Promise(resolve => {
setTimeout(() => {
updateOutput("タスク1完了");
resolve();
}, 500);
});
}
// 100ミリ遅延する処理
function task2() {
return new Promise(resolve => {
setTimeout(() => {
updateOutput("タスク2完了");
resolve();
}, 100);
});
}
function updateOutput(message) {
const output = document.getElementById('output');
output.innerHTML += '<p>' + message + '</p>';
}
then( )
then()
メソッドは、Promise
オブジェクトに関連付けられており、非同期処理が成功した後に実行したい処理をスケジュールするために使用されます。非同期処理が正常に完了した場合、then
に渡された関数が呼び出されます。
次はもっと直感的に非同期処理を書けるようにするための機能であるasync/awaitをご紹介。
async/await
asyncとawaitは、非同期処理をより同期的に書くための構文で、Promiseに基づいています。これにより、非同期コードが直感的で読みやすくなります。
このasync/awaitの実装では、非常に直感的に非同期処理を順序よく実行しています。await
を使ってtask1
とtask2
の完了を順番に待ち、その後で「処理命令終了」を表示しています。
Promiseより簡潔なコードになってるのが分かると思います。
See the Pen Untitled by kokusyo (@kokusyo) on CodePen.
// ボタンを押したら開始する処理
async function startTasks() {
// 処理の開始を示すメッセージを出力
updateOutput("処理命令開始");
await task1(); // task1の完了を待つ
await task2(); // task1の完了後にtask2の完了を待つ
updateOutput("処理命令終了"); // すべてのタスクが完了したことを示すメッセージを出力
}
// 500ミリ秒遅延する処理
function task1() {
return new Promise(resolve => {
setTimeout(() => {
updateOutput("タスク1完了");
resolve(); // Promiseを解決してtask1の終了を示す
}, 500);
});
}
// 100ミリ遅延する処理
function task2() {
return new Promise(resolve => {
setTimeout(() => {
updateOutput("タスク2完了");
resolve(); // Promiseを解決してtask2の終了を示す
}, 100);
});
}
function updateOutput(message) {
const output = document.getElementById('output');
output.innerHTML += '<p>' + message + '</p>';
}
async/awaitについては、こちらの記事で解説しているのでこちらもご覧くださいませ。
avaScriptの非同期処理は、コールバックからPromise、そしてasync/awaitに進化してきました。
これらのテクニックを適切に使い分けることで、コードの管理が容易になり、エラーハンドリングも向上します。
非同期処理の深い理解は、効率的なウェブアプリケーションを構築するための鍵となるので徐々に覚えていきましょう。