JavaScriptにおける非同期処理は、モダンなWebアプリケーション開発において欠かせない技術です。この記事では、基本的なPromiseの概念から実践的な実装パターンまで、包括的に解説します。
1. 非同期処理の基礎概念
なぜ非同期処理が必要なのか
JavaScriptはシングルスレッドで動作するため、重い処理を同期的に実行するとUIがブロックされてしまいます。非同期処理により、以下のメリットが得られます:
- レスポンシブなUI:ユーザーインターフェースが固まることなく、スムーズな操作が可能
- 効率的なリソース利用:I/O待機時間中に他の処理を実行
- スケーラビリティの向上:複数の処理を並行して実行
従来のコールバック地獄からの脱却
// 従来のコールバック地獄の例
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
getFinalData(c, function(d) {
// 処理が深くネストしてしまう
console.log(d);
});
});
});
});
2. Promiseの詳細解説
Promiseの基本構造
Promiseは非同期処理の結果を表現するオブジェクトで、以下の3つの状態を持ちます:
- Pending(保留中):初期状態、まだ実行されていない
- Fulfilled(履行済み):処理が正常に完了
- Rejected(拒否済み):処理が失敗
const myPromise = new Promise((resolve, reject) => {
// 非同期処理
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve("処理が成功しました");
} else {
reject(new Error("処理が失敗しました"));
}
}, 1000);
});
myPromise
.then(result => console.log(result))
.catch(error => console.error(error));
Promise.allとPromise.allSettledの使い分け
// Promise.all - 全て成功した場合のみ成功
const fetchAllData = async () => {
try {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(res => res.json()),
fetch('/api/posts').then(res => res.json()),
fetch('/api/comments').then(res => res.json())
]);
return { users, posts, comments };
} catch (error) {
console.error('いずれかのAPIでエラーが発生:', error);
}
};
// Promise.allSettled - 個別の成功/失敗を処理
const fetchDataWithErrorHandling = async () => {
const results = await Promise.allSettled([
fetch('/api/users').then(res => res.json()),
fetch('/api/posts').then(res => res.json()),
fetch('/api/comments').then(res => res.json())
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`API ${index} 成功:`, result.value);
} else {
console.log(`API ${index} 失敗:`, result.reason);
}
});
};
3. async/awaitの実践的な使い方
エラーハンドリングのベストプラクティス
// 包括的なエラーハンドリング
async function robustApiCall(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
if (error instanceof TypeError) {
console.error('ネットワークエラー:', error.message);
} else if (error instanceof SyntaxError) {
console.error('JSONパースエラー:', error.message);
} else {
console.error('その他のエラー:', error.message);
}
throw error; // 上位レイヤーでの処理のため再スロー
}
}
// リトライ機能付きAPI呼び出し
async function apiCallWithRetry(url, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await robustApiCall(url);
} catch (error) {
lastError = error;
if (i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000; // 指数バックオフ
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`${maxRetries}回のリトライ後も失敗: ${lastError.message}`);
}
並行処理と逐次処理の使い分け
// 並行処理(高速だが負荷が高い)
async function processConcurrently(items) {
const promises = items.map(async (item) => {
return await processItem(item);
});
return await Promise.all(promises);
}
// 逐次処理(安全だが時間がかかる)
async function processSequentially(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
// バッチ処理(並行数を制限)
async function processBatches(items, batchSize = 5) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(item => processItem(item))
);
results.push(...batchResults);
}
return results;
}
4. 高度な非同期パターン
カスタムPromiseの実装
class CustomPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(callback => callback(value));
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(callback => callback(reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
return new CustomPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
try {
const result = onFulfilled ? onFulfilled(this.value) : this.value;
resolve(result);
} catch (error) {
reject(error);
}
} else if (this.state === 'rejected') {
try {
const result = onRejected ? onRejected(this.reason) : this.reason;
resolve(result);
} catch (error) {
reject(error);
}
} else {
this.onFulfilledCallbacks.push((value) => {
try {
const result = onFulfilled ? onFulfilled(value) : value;
resolve(result);
} catch (error) {
reject(error);
}
});
this.onRejectedCallbacks.push((reason) => {
try {
const result = onRejected ? onRejected(reason) : reason;
resolve(result);
} catch (error) {
reject(error);
}
});
}
});
}
}
Generator関数を使った非同期制御
function* asyncGenerator() {
try {
const user = yield fetch('/api/user');
const posts = yield fetch(`/api/posts/${user.id}`);
const comments = yield fetch(`/api/comments/${posts[0].id}`);
return { user, posts, comments };
} catch (error) {
console.error('Generator内でエラー:', error);
}
}
async function runGenerator(generator) {
const iterator = generator();
let result = iterator.next();
while (!result.done) {
try {
const data = await result.value;
result = iterator.next(data.json ? await data.json() : data);
} catch (error) {
result = iterator.throw(error);
}
}
return result.value;
}
5. Web Workersを使った重い処理の分離
メインスレッドをブロックしない計算処理
// worker.js
self.onmessage = function(e) {
const { data, operation } = e.data;
switch (operation) {
case 'fibonacci':
const result = fibonacci(data.n);
self.postMessage({ result });
break;
case 'sort':
const sorted = data.array.sort((a, b) => a - b);
self.postMessage({ result: sorted });
break;
}
};
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// main.js
class WorkerManager {
constructor(workerScript) {
this.worker = new Worker(workerScript);
this.taskId = 0;
this.pendingTasks = new Map();
this.worker.onmessage = (e) => {
const { taskId, result, error } = e.data;
const task = this.pendingTasks.get(taskId);
if (task) {
this.pendingTasks.delete(taskId);
if (error) {
task.reject(new Error(error));
} else {
task.resolve(result);
}
}
};
}
async execute(operation, data) {
return new Promise((resolve, reject) => {
const taskId = ++this.taskId;
this.pendingTasks.set(taskId, { resolve, reject });
this.worker.postMessage({
taskId,
operation,
data
});
});
}
terminate() {
this.worker.terminate();
}
}
// 使用例
const workerManager = new WorkerManager('worker.js');
async function heavyCalculation() {
try {
const result = await workerManager.execute('fibonacci', { n: 40 });
console.log('フィボナッチ計算結果:', result);
} catch (error) {
console.error('Worker実行エラー:', error);
}
}
6. パフォーマンス最適化のテクニック
非同期処理のデバッグとプロファイリング
// 実行時間測定デコレータ
function measureTime(target, propertyName, descriptor) {
const method = descriptor.value;
descriptor.value = async function(...args) {
const start = performance.now();
try {
const result = await method.apply(this, args);
const end = performance.now();
console.log(`${propertyName} 実行時間: ${end - start}ms`);
return result;
} catch (error) {
const end = performance.now();
console.log(`${propertyName} エラー (${end - start}ms):`, error);
throw error;
}
};
return descriptor;
}
class ApiService {
@measureTime
async fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
}
// メモリリークを防ぐAbortController
class RequestManager {
constructor() {
this.activeRequests = new Set();
}
async fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
this.activeRequests.add(controller);
try {
const response = await fetch(url, {
signal: controller.signal
});
return await response.json();
} finally {
clearTimeout(timeoutId);
this.activeRequests.delete(controller);
}
}
cancelAllRequests() {
this.activeRequests.forEach(controller => controller.abort());
this.activeRequests.clear();
}
}
7. 実践的な応用例
リアルタイム通信とWebSocket
class WebSocketManager {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.eventListeners = new Map();
}
async connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectAttempts = 0;
resolve();
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.emit(data.type, data.payload);
} catch (error) {
console.error('メッセージパースエラー:', error);
}
};
this.ws.onclose = () => {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, Math.pow(2, this.reconnectAttempts) * 1000);
}
};
this.ws.onerror = (error) => {
reject(error);
};
});
}
on(event, callback) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event).push(callback);
}
emit(event, data) {
const listeners = this.eventListeners.get(event);
if (listeners) {
listeners.forEach(callback => callback(data));
}
}
send(type, payload) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type, payload }));
}
}
}
まとめ
モダンJavaScriptの非同期処理は、Webアプリケーションの品質とパフォーマンスを大きく左右する重要な技術です。Promise、async/await、Web Workersなどの技術を適切に組み合わせることで、ユーザー体験を向上させ、保守性の高いコードを書くことができます。
今回紹介したパターンやテクニックを実際のプロジェクトで活用し、継続的に学習を深めていくことをお勧めします。非同期処理をマスターすることで、より高度なWebアプリケーション開発が可能になるでしょう。
よく参考にさせていただいているEngineerCompassさんのサイトは下記からどうぞ。

Engineer Compass -
コメント