【デザイン基礎|実務向け】非同期の壁を越える!AsyncIteratorでデータストリームをスマートに制御する実務テクニック

こんにちは、シニア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

コメント

タイトルとURLをコピーしました