こんにちは、シニアWebデザイナーの皆さん。日々の開発で、非同期処理と格闘する場面は少なくないですよね。Promiseやasync/awaitが登場して以来、コードは格段に読みやすくなりましたが、それでもまだ「これがもっとスマートにできたら…」と感じる領域があるのではないでしょうか?
特に、大量のデータを逐次的に処理したい場合や、リアルタイムに流れてくるデータストリームを扱いたい場合、従来の非同期処理だけでは表現が複雑になったり、パフォーマンス上の課題を抱えたりすることがあります。
そこで今回ご紹介したいのが、JavaScriptの比較的新しい強力な機能、AsyncIteratorです。これを使えば、非同期のデータストリームを同期的なループのように扱い、コードを劇的にシンプルに、そして効率的に書けるようになります。
AsyncIteratorとは何か?同期Iteratorとの違い
まず、AsyncIteratorの前に、通常の(同期)Iteratorについて軽くおさらいしましょう。
JavaScriptのIteratorは、配列やMapなどのコレクションをfor...ofループで順に処理するためのプロトコルです。[Symbol.iterator]() メソッドが呼ばれると、{ value: T, done: boolean } の形を持つオブジェクトを返す next() メソッドを持つイテレータが返されます。
AsyncIteratorは、このIteratorの非同期版です。
主な違いは以下の点です。
[Symbol.asyncIterator]()メソッドを持つオブジェクトがAsyncIterableです。- その
next()メソッドは、Promiseを返します。このPromiseは、解決されると{ value: T, done: boolean }の形を持つオブジェクトを返します。
つまり、非同期処理の結果を待ってから次のデータに進む、という逐次的な処理を表現できるわけです。そして、これを扱うための強力なシンタックスシュガーが、おなじみの for await...of ループです。
簡単な例を見てみましょう。
async function createAsyncGenerator() {
console.log('データ1の生成を開始...');
yield await new Promise(resolve => setTimeout(() => resolve('データ1'), 1000)); // 1秒待つ
console.log('データ2の生成を開始...');
yield await new Promise(resolve => setTimeout(() => resolve('データ2'), 1000)); // 1秒待つ
console.log('データ3の生成を開始...');
yield await new Promise(resolve => setTimeout(() => resolve('データ3'), 1000)); // 1秒待つ
}
async function processDataStream() {
console.log('--- AsyncIterator処理開始 ---');
for await (const data of createAsyncGenerator()) {
console.log(`受信データ: ${data}`);
// ここでUIを更新したり、次の処理を行ったりできる
}
console.log('--- AsyncIterator処理完了 ---');
}
processDataStream();
// 出力例:
// --- AsyncIterator処理開始 ---
// データ1の生成を開始...
// (1秒後)
// 受信データ: データ1
// データ2の生成を開始...
// (1秒後)
// 受信データ: データ2
// データ3の生成を開始...
// (1秒後)
// 受信データ: データ3
// --- AsyncIterator処理完了 ---
このコードを見てください。まるで同期的なループのように見えませんか?しかし実際には、それぞれの yield のところでPromiseの解決を待っています。これがAsyncIteratorの威力です。
実務で役立つAsyncIteratorのユースケース
では、具体的な実務でAsyncIteratorがどのように役立つかを見ていきましょう。
1. ページネーションAPIからのデータ逐次取得とUI更新
多くのWebアプリケーションでは、APIから大量のデータを取得する際にページネーションが用いられます。従来のasync/awaitでは、すべてのページをフェッチし終えてからUIを更新するか、再帰的に次のページを呼び出すロジックを書く必要がありました。
AsyncIteratorを使えば、これをデータストリームとして扱い、ユーザー体験を向上させることができます。
// 仮想のAPI呼び出し関数 (例: 商品リスト取得)
async function fetchProductsApi(page = 1, limit = 10) {
console.log(`API: ページ ${page} の商品を取得中...`);
return new Promise(resolve => {
setTimeout(() => {
const products = Array.from({ length: limit }).map((_, i) => `商品${(page - 1) limit + i + 1}`);
const hasNextPage = page < 3; // 仮に3ページまであるとする
resolve({
data: products,
nextPage: hasNextPage ? page + 1 : null,
});
}, 500); // 擬似的なネットワーク遅延
});
}
// Async Generatorでページネーションをラップ
async function productStream(initialPage = 1, limit = 10) {
let currentPage = initialPage;
let hasNext = true;
while (hasNext) {
const response = await fetchProductsApi(currentPage, limit);
yield response.data; // 取得した商品リストの各要素を一つずつyield
currentPage = response.nextPage;
hasNext = currentPage !== null;
}
}
// 使い方:UIに逐次表示するイメージ
async function displayProductsOnUI() {
const productListElement = document.createElement('ul'); // 仮想のUI要素
document.body.appendChild(productListElement); // ページに追加
console.log('--- 商品リストのUI表示を開始 ---');
for await (const product of productStream()) {
console.log(`UI表示: ${product}`);
const li = document.createElement('li');
li.textContent = product;
productListElement.appendChild(li);
// ユーザーは、すべての商品がロードされるのを待たずに、
// ロードされた商品から順に見始めることができる!
await new Promise(resolve => setTimeout(resolve, 100)); // UI更新のデモ用に少し待つ
}
console.log('--- 全ての商品を表示しました ---');
}
displayProductsOnUI();
この例では、APIから次のページが取得されるたびに、そのデータを即座にUIに反映できます。これにより、ユーザーは全データが揃うのを待つことなく、インタラクティブにコンテンツを閲覧し始めることができます。特に無限スクロールのようなUIパターンでは、非常に有効なアプローチです。また、一度に全データをメモリに読み込む必要がないため、メモリ効率も向上します。
2. WebSocketからのリアルタイムデータ処理
WebSocketは、チャットアプリやリアルタイム通知、株価表示など、サーバーとクライアント間で双方向のデータストリームを確立する際に使われます。WebSocketで受信したメッセージを一つずつ処理する際にも、AsyncIteratorが活躍します。
// 仮想のWebSocket APIを模倣するクラス
class MockWebSocket {
constructor(url) {
this.url = url;
this.listeners = new Set();
this.messageIndex = 0;
this.messages = ['Hello', 'World', 'from', 'WebSocket', 'Stream!'];
console.log(`WebSocket(${url})に接続中...`);
setTimeout(() => this.startSendingMessages(), 500);
}
addEventListener(event, callback) {
if (event === 'message') this.listeners.add(callback);
}
removeEventListener(event, callback) {
if (event === 'message') this.listeners.delete(callback);
}
startSendingMessages() {
this.intervalId = setInterval(() => {
if (this.messageIndex < this.messages.length) {
const message = this.messages[this.messageIndex++];
this.listeners.forEach(cb => cb({ data: message }));
} else {
clearInterval(this.intervalId);
console.log('WebSocket: 全メッセージ送信完了。');
// 実際のWebSocketではここで 'close' イベントを発行する
}
}, 1000); // 1秒ごとにメッセージを送信
}
close() {
clearInterval(this.intervalId);
console.log('WebSocket接続を閉じました。');
}
}
// Async GeneratorでWebSocketのメッセージストリームをラップ
async function webSocketMessageStream(url) {
const ws = new MockWebSocket(url);
let messageQueue = [];
let resolveNextMessage = null; // next()が呼ばれたときに解決されるPromiseのresolve関数
const messageHandler = (event) => {
messageQueue.push(event.data);
if (resolveNextMessage) {
resolveNextMessage(); // next()呼び出しを解決
resolveNextMessage = null;
}
};
ws.addEventListener('message', messageHandler);
try {
while

コメント