فارسی

با کمک‌کننده‌های Async Iterator جاوا اسکریپت، پردازش استریم را متحول کنید. نحوه مدیریت کارآمد استریم‌های داده ناهمزمان با map، filter، take، drop و غیره را بیاموزید.

کمک‌کننده‌های Async Iterator در جاوا اسکریپت: پردازش قدرتمند استریم برای برنامه‌های مدرن

در توسعه مدرن جاوا اسکریپت، سروکار داشتن با استریم‌های داده ناهمزمان یک نیاز رایج است. چه در حال دریافت داده از یک API باشید، چه در حال پردازش فایل‌های بزرگ یا مدیریت رویدادهای زنده، مدیریت کارآمد داده‌های ناهمزمان بسیار حیاتی است. کمک‌کننده‌های Async Iterator جاوا اسکریپت راهی قدرتمند و زیبا برای پردازش این استریم‌ها ارائه می‌دهند و یک رویکرد تابعی و ترکیب‌پذیر برای دستکاری داده‌ها فراهم می‌کنند.

Async Iterators و Async Iterables چه هستند؟

قبل از پرداختن به کمک‌کننده‌های Async Iterator، بیایید مفاهیم اساسی را درک کنیم: Async Iterators و Async Iterables.

یک Async Iterable یک شیء است که روشی برای پیمایش ناهمزمان بر روی مقادیر خود تعریف می‌کند. این کار را با پیاده‌سازی متد @@asyncIterator انجام می‌دهد که یک Async Iterator را برمی‌گرداند.

یک Async Iterator یک شیء است که متد next() را فراهم می‌کند. این متد یک پرامیس (promise) را برمی‌گرداند که به یک شیء با دو ویژگی resolve می‌شود:

در اینجا یک مثال ساده آورده شده است:


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 500)); // شبیه‌سازی یک عملیات ناهمزمان
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  for await (const value of asyncIterable) {
    console.log(value); // خروجی: 1, 2, 3, 4, 5 (با 500 میلی‌ثانیه تأخیر بین هر کدام)
  }
})();

در این مثال، generateSequence یک تابع async generator است که دنباله‌ای از اعداد را به صورت ناهمزمان تولید می‌کند. حلقه for await...of برای مصرف مقادیر از async iterable استفاده می‌شود.

معرفی کمک‌کننده‌های Async Iterator

کمک‌کننده‌های Async Iterator عملکرد Async Iterators را گسترش می‌دهند و مجموعه‌ای از متدها را برای تبدیل، فیلتر کردن و دستکاری استریم‌های داده ناهمزمان فراهم می‌کنند. آن‌ها سبکی از برنامه‌نویسی تابعی و ترکیب‌پذیر را ممکن می‌سازند و ساخت خطوط لوله پردازش داده پیچیده را آسان‌تر می‌کنند.

کمک‌کننده‌های اصلی Async Iterator عبارتند از:

بیایید هر کمک‌کننده را با مثال بررسی کنیم.

map()

کمک‌کننده map() هر عنصر از async iterable را با استفاده از یک تابع ارائه‌شده تبدیل می‌کند. این متد یک async iterable جدید با مقادیر تبدیل‌شده برمی‌گرداند.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const doubledIterable = asyncIterable.map(x => x * 2);

(async () => {
  for await (const value of doubledIterable) {
    console.log(value); // خروجی: 2, 4, 6, 8, 10 (با 100 میلی‌ثانیه تأخیر)
  }
})();

در این مثال، map(x => x * 2) هر عدد در دنباله را دو برابر می‌کند.

filter()

کمک‌کننده filter() عناصر را از async iterable بر اساس یک شرط ارائه‌شده (تابع predicate) انتخاب می‌کند. این متد یک async iterable جدید حاوی تنها عناصری که شرط را برآورده می‌کنند، برمی‌گرداند.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);

const evenNumbersIterable = asyncIterable.filter(x => x % 2 === 0);

(async () => {
  for await (const value of evenNumbersIterable) {
    console.log(value); // خروجی: 2, 4, 6, 8, 10 (با 100 میلی‌ثانیه تأخیر)
  }
})();

در این مثال، filter(x => x % 2 === 0) تنها اعداد زوج را از دنباله انتخاب می‌کند.

take()

کمک‌کننده take() N عنصر اول را از async iterable برمی‌گرداند. این متد یک async iterable جدید حاوی تنها تعداد مشخصی از عناصر را برمی‌گرداند.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const firstThreeIterable = asyncIterable.take(3);

(async () => {
  for await (const value of firstThreeIterable) {
    console.log(value); // خروجی: 1, 2, 3 (با 100 میلی‌ثانیه تأخیر)
  }
})();

در این مثال، take(3) سه عدد اول را از دنباله انتخاب می‌کند.

drop()

کمک‌کننده drop() از N عنصر اول از async iterable صرف‌نظر کرده و بقیه را برمی‌گرداند. این متد یک async iterable جدید حاوی عناصر باقیمانده را برمی‌گرداند.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const afterFirstTwoIterable = asyncIterable.drop(2);

(async () => {
  for await (const value of afterFirstTwoIterable) {
    console.log(value); // خروجی: 3, 4, 5 (با 100 میلی‌ثانیه تأخیر)
  }
})();

در این مثال، drop(2) از دو عدد اول دنباله صرف‌نظر می‌کند.

toArray()

کمک‌کننده toArray() کل async iterable را مصرف کرده و تمام عناصر را در یک آرایه جمع‌آوری می‌کند. این متد یک پرامیس را برمی‌گرداند که به یک آرایه حاوی تمام عناصر resolve می‌شود.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const numbersArray = await asyncIterable.toArray();
  console.log(numbersArray); // خروجی: [1, 2, 3, 4, 5]
})();

در این مثال، toArray() تمام اعداد دنباله را در یک آرایه جمع‌آوری می‌کند.

forEach()

کمک‌کننده forEach() یک تابع ارائه‌شده را یک بار برای هر عنصر در async iterable اجرا می‌کند. این متد یک async iterable جدید برنمی‌گرداند، بلکه تابع را به صورت side-effectual اجرا می‌کند. این می‌تواند برای انجام عملیاتی مانند لاگ‌گیری یا به‌روزرسانی UI مفید باشد.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(3);

(async () => {
  await asyncIterable.forEach(value => {
    console.log("Value:", value);
  });
  console.log("forEach completed");
})();
// خروجی: Value: 1, Value: 2, Value: 3, forEach completed

some()

کمک‌کننده some() بررسی می‌کند که آیا حداقل یک عنصر در async iterable آزمون پیاده‌سازی‌شده توسط تابع ارائه‌شده را با موفقیت پشت سر می‌گذارد. این متد یک پرامیس را برمی‌گرداند که به یک مقدار بولی resolve می‌شود (true اگر حداقل یک عنصر شرط را برآورده کند، در غیر این صورت false).


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const hasEvenNumber = await asyncIterable.some(x => x % 2 === 0);
  console.log("Has even number:", hasEvenNumber); // خروجی: Has even number: true
})();

every()

کمک‌کننده every() بررسی می‌کند که آیا همه عناصر در async iterable آزمون پیاده‌سازی‌شده توسط تابع ارائه‌شده را با موفقیت پشت سر می‌گذارند. این متد یک پرامیس را برمی‌گرداند که به یک مقدار بولی resolve می‌شود (true اگر همه عناصر شرط را برآورده کنند، در غیر این صورت false).


async function* generateSequence(end) {
  for (let i = 2; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(4);

(async () => {
  const areAllEven = await asyncIterable.every(x => x % 2 === 0);
  console.log("Are all even:", areAllEven); // خروجی: Are all even: true
})();

find()

کمک‌کننده find() اولین عنصر در async iterable را که تابع تست ارائه‌شده را برآورده می‌کند، برمی‌گرداند. اگر هیچ مقداری تابع تست را برآورده نکند، undefined برگردانده می‌شود. این متد یک پرامیس را برمی‌گرداند که به عنصر یافت‌شده یا undefined resolve می‌شود.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const firstEven = await asyncIterable.find(x => x % 2 === 0);
  console.log("First even number:", firstEven); // خروجی: First even number: 2
})();

reduce()

کمک‌کننده reduce() یک تابع callback «کاهنده» (reducer) ارائه‌شده توسط کاربر را به ترتیب بر روی هر عنصر از async iterable اجرا می‌کند و مقدار بازگشتی از محاسبه روی عنصر قبلی را به آن پاس می‌دهد. نتیجه نهایی اجرای کاهنده بر روی تمام عناصر یک مقدار واحد است. این متد یک پرامیس را برمی‌گرداند که به مقدار انباشته‌شده نهایی resolve می‌شود.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const sum = await asyncIterable.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
  console.log("Sum:", sum); // خروجی: Sum: 15
})();

مثال‌های عملی و موارد استفاده

کمک‌کننده‌های Async Iterator در سناریوهای مختلفی ارزشمند هستند. بیایید چند مثال عملی را بررسی کنیم:

۱. پردازش داده از یک API استریمینگ

تصور کنید در حال ساخت یک داشبورد نمایش داده‌های زنده هستید که داده‌ها را از یک API استریمینگ دریافت می‌کند. API به‌طور مداوم به‌روزرسانی‌ها را ارسال می‌کند و شما باید این به‌روزرسانی‌ها را برای نمایش آخرین اطلاعات پردازش کنید.


async function* fetchDataFromAPI(url) {
  let response = await fetch(url);

  if (!response.body) {
    throw new Error("ReadableStream not supported in this environment");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        break;
      }
      const chunk = decoder.decode(value);
      // فرض بر این است که API اشیاء JSON را با newline از هم جدا می‌کند
      const lines = chunk.split('\n');
      for (const line of lines) {
        if (line.trim() !== '') {
          yield JSON.parse(line);
        }
      }
    }
  } finally {
    reader.releaseLock();
  }
}

const apiURL = 'https://example.com/streaming-api'; // با URL API خود جایگزین کنید
const dataStream = fetchDataFromAPI(apiURL);

// پردازش استریم داده
(async () => {
  for await (const data of dataStream.filter(item => item.type === 'metric').map(item => ({ timestamp: item.timestamp, value: item.value }))) {
    console.log('Processed Data:', data);
    // به‌روزرسانی داشبورد با داده‌های پردازش‌شده
  }
})();

در این مثال، fetchDataFromAPI داده‌ها را از یک API استریمینگ دریافت می‌کند، اشیاء JSON را تجزیه کرده و آن‌ها را به عنوان یک async iterable تولید (yield) می‌کند. کمک‌کننده filter فقط معیارها (metrics) را انتخاب می‌کند و کمک‌کننده map داده‌ها را به فرمت دلخواه تبدیل می‌کند قبل از اینکه داشبورد را به‌روزرسانی کند.

۲. خواندن و پردازش فایل‌های بزرگ

فرض کنید نیاز به پردازش یک فایل CSV بزرگ حاوی داده‌های مشتریان دارید. به جای بارگذاری کل فایل در حافظه، می‌توانید از کمک‌کننده‌های Async Iterator برای پردازش آن به صورت تکه‌تکه استفاده کنید.


async function* readLinesFromFile(filePath) {
  const file = await fsPromises.open(filePath, 'r');

  try {
    let buffer = Buffer.alloc(1024);
    let fileOffset = 0;
    let remainder = '';

    while (true) {
      const { bytesRead } = await file.read(buffer, 0, buffer.length, fileOffset);
      if (bytesRead === 0) {
        if (remainder) {
          yield remainder;
        }
        break;
      }

      fileOffset += bytesRead;
      const chunk = buffer.toString('utf8', 0, bytesRead);
      const lines = chunk.split('\n');

      lines[0] = remainder + lines[0];
      remainder = lines.pop() || '';

      for (const line of lines) {
        yield line;
      }
    }
  } finally {
    await file.close();
  }
}

const filePath = './customer_data.csv'; // با مسیر فایل خود جایگزین کنید
const lines = readLinesFromFile(filePath);

// پردازش خطوط
(async () => {
  for await (const customerData of lines.drop(1).map(line => line.split(',')).filter(data => data[2] === 'USA')) {
    console.log('Customer from USA:', customerData);
    // پردازش داده‌های مشتری از آمریکا
  }
})();

در این مثال، readLinesFromFile فایل را خط به خط می‌خواند و هر خط را به عنوان یک async iterable تولید می‌کند. کمک‌کننده drop(1) از ردیف هدر صرف‌نظر می‌کند، کمک‌کننده map خط را به ستون‌ها تقسیم می‌کند و کمک‌کننده filter فقط مشتریان از آمریکا را انتخاب می‌کند.

۳. مدیریت رویدادهای زنده

کمک‌کننده‌های Async Iterator همچنین می‌توانند برای مدیریت رویدادهای زنده از منابعی مانند WebSockets استفاده شوند. شما می‌توانید یک async iterable ایجاد کنید که رویدادها را به محض رسیدن منتشر (emit) کند و سپس از کمک‌کننده‌ها برای پردازش این رویدادها استفاده کنید.


async function* createWebSocketStream(url) {
  const ws = new WebSocket(url);

  yield new Promise((resolve, reject) => {
      ws.onopen = () => {
          resolve();
      };
      ws.onerror = (error) => {
          reject(error);
      };
  });

  try {
    while (ws.readyState === WebSocket.OPEN) {
      yield new Promise((resolve, reject) => {
        ws.onmessage = (event) => {
          resolve(JSON.parse(event.data));
        };
        ws.onerror = (error) => {
          reject(error);
        };
        ws.onclose = () => {
           resolve(null); // با null در هنگام بسته شدن اتصال resolve می‌شود
        }
      });

    }
  } finally {
    ws.close();
  }
}

const websocketURL = 'wss://example.com/events'; // با URL WebSocket خود جایگزین کنید
const eventStream = createWebSocketStream(websocketURL);

// پردازش استریم رویداد
(async () => {
  for await (const event of eventStream.filter(event => event.type === 'user_login').map(event => ({ userId: event.userId, timestamp: event.timestamp }))) {
    console.log('User Login Event:', event);
    // پردازش رویداد ورود کاربر
  }
})();

در این مثال، createWebSocketStream یک async iterable ایجاد می‌کند که رویدادهای دریافتی از یک WebSocket را منتشر می‌کند. کمک‌کننده filter فقط رویدادهای ورود کاربر را انتخاب می‌کند و کمک‌کننده map داده‌ها را به فرمت دلخواه تبدیل می‌کند.

مزایای استفاده از کمک‌کننده‌های Async Iterator

پشتیبانی مرورگر و محیط اجرا

کمک‌کننده‌های Async Iterator هنوز یک ویژگی نسبتاً جدید در جاوا اسکریپت هستند. تا اواخر سال ۲۰۲۴، آنها در مرحله ۳ فرآیند استانداردسازی TC39 قرار دارند، به این معنی که احتمالاً در آینده نزدیک استاندارد خواهند شد. با این حال، آنها هنوز به صورت بومی در همه مرورگرها و نسخه‌های Node.js پشتیبانی نمی‌شوند.

پشتیبانی مرورگر: مرورگرهای مدرن مانند Chrome، Firefox، Safari و Edge به تدریج در حال افزودن پشتیبانی از کمک‌کننده‌های Async Iterator هستند. می‌توانید آخرین اطلاعات سازگاری مرورگرها را در وب‌سایت‌هایی مانند Can I use... بررسی کنید تا ببینید کدام مرورگرها از این ویژگی پشتیبانی می‌کنند.

پشتیبانی Node.js: نسخه‌های اخیر Node.js (v18 و بالاتر) پشتیبانی آزمایشی از کمک‌کننده‌های Async Iterator را ارائه می‌دهند. برای استفاده از آنها، ممکن است نیاز به اجرای Node.js با فلگ --experimental-async-iterator داشته باشید.

پولی‌فیل‌ها (Polyfills): اگر نیاز به استفاده از کمک‌کننده‌های Async Iterator در محیط‌هایی دارید که به صورت بومی از آنها پشتیبانی نمی‌کنند، می‌توانید از یک پولی‌فیل استفاده کنید. پولی‌فیل قطعه کدی است که عملکرد گمشده را فراهم می‌کند. چندین کتابخانه پولی‌فیل برای کمک‌کننده‌های Async Iterator موجود است؛ یک گزینه محبوب کتابخانه core-js است.

پیاده‌سازی Async Iterators سفارشی

در حالی که کمک‌کننده‌های Async Iterator راهی مناسب برای پردازش async iterableهای موجود فراهم می‌کنند، گاهی اوقات ممکن است نیاز به ایجاد async iteratorهای سفارشی خود داشته باشید. این به شما امکان می‌دهد تا داده‌ها را از منابع مختلفی مانند پایگاه‌های داده، APIها یا سیستم‌های فایل به صورت استریمینگ مدیریت کنید.

برای ایجاد یک async iterator سفارشی، باید متد @@asyncIterator را بر روی یک شیء پیاده‌سازی کنید. این متد باید یک شیء با متد next() را برگرداند. متد next() باید یک پرامیس را برگرداند که به یک شیء با ویژگی‌های value و done resolve می‌شود.

در اینجا مثالی از یک async iterator سفارشی آورده شده است که داده‌ها را از یک API صفحه‌بندی‌شده (paginated) دریافت می‌کند:


async function* fetchPaginatedData(baseURL) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const url = `${baseURL}?page=${page}`;
    const response = await fetch(url);
    const data = await response.json();

    if (data.results.length === 0) {
      hasMore = false;
      break;
    }

    for (const item of data.results) {
      yield item;
    }

    page++;
  }
}

const apiBaseURL = 'https://api.example.com/data'; // با URL API خود جایگزین کنید
const paginatedData = fetchPaginatedData(apiBaseURL);

// پردازش داده‌های صفحه‌بندی‌شده
(async () => {
  for await (const item of paginatedData) {
    console.log('Item:', item);
    // پردازش آیتم
  }
})();

در این مثال، fetchPaginatedData داده‌ها را از یک API صفحه‌بندی‌شده دریافت می‌کند و هر آیتم را به محض بازیابی، تولید (yield) می‌کند. async iterator منطق صفحه‌بندی را مدیریت می‌کند و مصرف داده‌ها را به صورت استریمینگ آسان می‌سازد.

چالش‌ها و ملاحظات بالقوه

در حالی که کمک‌کننده‌های Async Iterator مزایای بی‌شماری دارند، آگاهی از برخی چالش‌ها و ملاحظات بالقوه مهم است:

بهترین شیوه‌ها برای استفاده از کمک‌کننده‌های Async Iterator

برای بهره‌برداری حداکثری از کمک‌کننده‌های Async Iterator، بهترین شیوه‌های زیر را در نظر بگیرید:

تکنیک‌های پیشرفته

ترکیب کمک‌کننده‌های سفارشی

شما می‌توانید کمک‌کننده‌های async iterator سفارشی خود را با ترکیب کمک‌کننده‌های موجود یا ساختن موارد جدید از ابتدا ایجاد کنید. این به شما امکان می‌دهد تا عملکرد را متناسب با نیازهای خاص خود تنظیم کرده و اجزای قابل استفاده مجدد ایجاد کنید.


async function* takeWhile(asyncIterable, predicate) {
  for await (const value of asyncIterable) {
    if (!predicate(value)) {
      break;
    }
    yield value;
  }
}

// مثال استفاده:
async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);
const firstFive = takeWhile(asyncIterable, x => x <= 5);

(async () => {
  for await (const value of firstFive) {
    console.log(value);
  }
})();

ترکیب چندین Async Iterable

شما می‌توانید چندین async iterable را با استفاده از تکنیک‌هایی مانند zip یا merge در یک async iterable واحد ترکیب کنید. این به شما امکان می‌دهد تا داده‌ها را از چندین منبع به طور همزمان پردازش کنید.


async function* zip(asyncIterable1, asyncIterable2) {
    const iterator1 = asyncIterable1[Symbol.asyncIterator]();
    const iterator2 = asyncIterable2[Symbol.asyncIterator]();

    while (true) {
        const result1 = await iterator1.next();
        const result2 = await iterator2.next();

        if (result1.done || result2.done) {
            break;
        }

        yield [result1.value, result2.value];
    }
}

// مثال استفاده:
async function* generateSequence1(end) {
    for (let i = 1; i <= end; i++) {
        yield i;
    }
}

async function* generateSequence2(end) {
    for (let i = 10; i <= end + 9; i++) {
        yield i;
    }
}

const iterable1 = generateSequence1(5);
const iterable2 = generateSequence2(5);

(async () => {
    for await (const [value1, value2] of zip(iterable1, iterable2)) {
        console.log(value1, value2);
    }
})();

نتیجه‌گیری

کمک‌کننده‌های Async Iterator جاوا اسکریپت راهی قدرتمند و زیبا برای پردازش استریم‌های داده ناهمزمان ارائه می‌دهند. آنها رویکردی تابعی و ترکیب‌پذیر برای دستکاری داده‌ها فراهم می‌کنند و ساخت خطوط لوله پردازش داده پیچیده را آسان‌تر می‌سازند. با درک مفاهیم اصلی Async Iterators و Async Iterables و تسلط بر متدهای مختلف کمکی، می‌توانید به طور قابل توجهی کارایی و قابلیت نگهداری کد جاوا اسکریپت ناهمزمان خود را بهبود بخشید. با ادامه رشد پشتیبانی مرورگرها و محیط‌های اجرا، کمک‌کننده‌های Async Iterator آماده‌اند تا به ابزاری ضروری برای توسعه‌دهندگان مدرن جاوا اسکریپت تبدیل شوند.