Async Iterator: پیمایش داده‌های ناهمزمان با for await...of

تا این‌جا با Iterator و Generatorهایی آشنا شدید که مقادیر را به‌صورت همزمان (synchronous) و بلافاصله تولید می‌کنند. اما در دنیای واقعی، بسیاری از منابع داده—مثل نتایج صفحه‌به‌صفحه‌ی یک API یا خواندن یک فایل بزرگ به‌صورت قطعه‌قطعه—به‌طور ذاتی ناهمزمان هستند. برای پیمایش این نوع منابع، جاوااسکریپت پروتکل موازی دیگری معرفی کرده است: Async Iterator.

چرا Iterator معمولی برای داده‌ی ناهمزمان کافی نیست؟

پروتکل Iterator معمولی انتظار دارد متد next() بلافاصله یک شیء { value, done } برگرداند. اما اگر تولید هر مقدار نیازمند یک عملیات ناهمزمان مثل fetch باشد، این مقدار به‌صورت آنی در دسترس نیست. راه‌حل این است که متد next() به‌جای مقدار مستقیم، یک Promise از آن شیء برگرداند.

Symbol.asyncIterator

درست همان‌طور که Symbol.iterator یک شیء را Iterable می‌کند، Symbol.asyncIterator یک شیء را Async Iterable می‌کند. تفاوت اصلی در این است که متد next() بازگشتی از آن، یک Promise است، نه یک شیء ساده:

const asyncRange = {
  from: 1,
  to: 3,
  [Symbol.asyncIterator]() {
    let current = this.from;
    const last = this.to;

    return {
      next() {
        return new Promise((resolve) => {
          setTimeout(() => {
            if (current <= last) {
              resolve({ value: current++, done: false });
            } else {
              resolve({ value: undefined, done: true });
            }
          }, 500);
        });
      },
    };
  },
};

پیمایش با for await...of

برای پیمایش یک شیء Async Iterable، از حلقه‌ی for await...of استفاده می‌شود که فقط داخل یک تابع async قابل استفاده است. این حلقه به‌طور خودکار پیش از هر تکرار، منتظر resolve شدن Promise برگشتی از next() می‌ماند:

پیمایش یک منبع ناهمزمان
خروجی کنسول اینجا نمایش داده می‌شود...

Async Generator: ساخت آسان‌تر با function* async

همان‌طور که Generator ساخت Iterator معمولی را ساده کرد، Async Generator همین کار را برای Async Iterator انجام می‌دهد. کافی‌ست کلیدواژه‌ی async پیش از function* اضافه شود و درون آن از await استفاده کرد:

async function* fetchPages(baseUrl, totalPages) {
  for (let page = 1; page <= totalPages; page++) {
    const response = await fetch(`${baseUrl}?page=${page}`);
    const data = await response.json();
    yield data;
  }
}

async function loadAll() {
  for await (const pageData of fetchPages("/api/products", 3)) {
    console.log(pageData);
  }
}

loadAll();

این الگو در پروژه‌های واقعی بسیار کاربردی است؛ به‌خصوص برای اجرای درخواست‌های صفحه‌به‌صفحه (pagination) بدون آنکه لازم باشد تمام صفحات را از قبل و هم‌زمان بارگذاری کرد. هر صفحه فقط زمانی درخواست می‌شود که حلقه واقعاً به آن نیاز داشته باشد.

جدول مقایسه‌ی Iterator و Async Iterator

ویژگی Iterator Async Iterator
متد شناسایی Symbol.iterator Symbol.asyncIterator
مقدار بازگشتی next() { value, done } Promise<{ value, done }>
حلقه‌ی پیمایش for...of for await...of
نوع Generator متناظر function* async function*

نکات کلیدی

  • Async Iterator برای منابعی طراحی شده که تولید هر مقدار نیازمند یک عملیات ناهمزمان است.
  • در پروتکل Async Iterable، متد next() به‌جای مقدار مستقیم، یک Promise برمی‌گرداند.
  • حلقه‌ی for await...of فقط داخل توابع async قابل استفاده است و منتظر resolve شدن هر Promise می‌ماند.
  • Async Generator با ترکیب async و function*، ساخت Async Iterator را به همان اندازه‌ی Generator معمولی ساده می‌کند.

جمع‌بندی

Async Iterator و حلقه‌ی for await...of، پیمایش منابع داده‌ای که به‌طور طبیعی ناهمزمان هستند—مثل درخواست‌های صفحه‌به‌صفحه به سرور—را به همان سادگی پیمایش یک آرایه‌ی معمولی تبدیل می‌کنند. Async Generator نیز با ترکیب مفاهیمی که پیش‌تر جداگانه دیده بودید، یعنی async/await و Generator، ابزاری قدرتمند برای تولید و مصرف داده‌ی ناهمزمان به‌صورت جریانی فراهم می‌کند.