بررسی عمیق راهنمای 'scan' در Async Iterator جاوا اسکریپت، کاوش در عملکرد، موارد استفاده و مزایای آن برای پردازش تجمعی ناهمگام.
راهنمای Async Iterator در جاوا اسکریپت: Scan - پردازش تجمعی ناهمگام
برنامهنویسی ناهمگام (Asynchronous) یکی از سنگ بنای توسعه مدرن جاوا اسکریپت است، به خصوص هنگام کار با عملیاتهای وابسته به ورودی/خروجی (I/O)، مانند درخواستهای شبکه یا تعاملات با فایل سیستم. Async iteratorها که در ES2018 معرفی شدند، مکانیزم قدرتمندی برای مدیریت جریانهای داده ناهمگام فراهم میکنند. راهنمای `scan` که اغلب در کتابخانههایی مانند RxJS یافت میشود و به طور فزایندهای به عنوان یک ابزار مستقل در دسترس است، پتانسیل بیشتری برای پردازش این جریانهای داده ناهمگام باز میکند.
درک Async Iteratorها
قبل از پرداختن به `scan`، بیایید مرور کنیم که async iteratorها چه هستند. یک async iterator شیئی است که از پروتکل async iterator پیروی میکند. این پروتکل یک متد `next()` را تعریف میکند که یک promise را برمیگرداند که به یک شی با دو خاصیت resolve میشود: `value` (مقدار بعدی در دنباله) و `done` (یک مقدار boolean که نشان میدهد آیا iterator به پایان رسیده است یا خیر). Async iteratorها به ویژه هنگام کار با دادههایی که در طول زمان میرسند، یا دادههایی که برای دریافت نیاز به عملیات ناهمگام دارند، مفید هستند.
در اینجا یک مثال ساده از یک async iterator آورده شده است:
async function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
async function main() {
const iterator = generateNumbers();
let result = await iterator.next();
console.log(result); // { value: 1, done: false }
result = await iterator.next();
console.log(result); // { value: 2, done: false }
result = await iterator.next();
console.log(result); // { value: 3, done: false }
result = await iterator.next();
console.log(result); // { value: undefined, done: true }
}
main();
معرفی راهنمای `scan`
راهنمای `scan` (که به آن `accumulate` یا `reduce` نیز گفته میشود) یک async iterator را با اعمال یک تابع انباشتگر (accumulator) به هر مقدار و انتشار نتیجه انباشته شده، تبدیل میکند. این کار مشابه متد `reduce` در آرایهها است، اما به صورت ناهمگام و بر روی iteratorها عمل میکند.
در اصل، `scan` یک async iterator، یک تابع انباشتگر و یک مقدار اولیه اختیاری را میگیرد. برای هر مقداری که توسط iterator منبع منتشر میشود، تابع انباشتگر با مقدار انباشته شده قبلی (یا مقدار اولیه اگر اولین تکرار باشد) و مقدار فعلی از iterator فراخوانی میشود. نتیجه تابع انباشتگر به مقدار انباشته شده بعدی تبدیل میشود که سپس توسط async iterator حاصل منتشر میشود.
سینتکس و پارامترها
سینتکس کلی برای استفاده از `scan` به شرح زیر است:
async function* scan(sourceIterator, accumulator, initialValue) {
let accumulatedValue = initialValue;
for await (const value of sourceIterator) {
accumulatedValue = accumulator(accumulatedValue, value);
yield accumulatedValue;
}
}
- `sourceIterator`: Async iterator ای که باید تبدیل شود.
- `accumulator`: تابعی که دو آرگومان میگیرد: مقدار انباشته شده قبلی و مقدار فعلی از iterator. این تابع باید مقدار انباشته شده جدید را برگرداند.
- `initialValue` (اختیاری): مقدار اولیه برای انباشتگر. اگر ارائه نشود، اولین مقدار از iterator منبع به عنوان مقدار اولیه استفاده خواهد شد و تابع انباشتگر از مقدار دوم شروع به کار خواهد کرد.
موارد استفاده و مثالها
راهنمای `scan` فوقالعاده متنوع است و میتواند در طیف گستردهای از سناریوهای مربوط به جریانهای داده ناهمگام استفاده شود. در اینجا چند مثال آورده شده است:
۱. محاسبه مجموع جاری
تصور کنید یک async iterator دارید که مقادیر تراکنشها را منتشر میکند. میتوانید از `scan` برای محاسبه مجموع جاری این تراکنشها استفاده کنید.
async function* generateTransactions() {
yield 10;
yield 20;
yield 30;
}
async function main() {
const transactions = generateTransactions();
const runningTotals = scan(transactions, (acc, value) => acc + value, 0);
for await (const total of runningTotals) {
console.log(total); // Output: 10, 30, 60
}
}
main();
در این مثال، تابع `accumulator` به سادگی مقدار تراکنش فعلی را به مجموع قبلی اضافه میکند. `initialValue` برابر با 0 تضمین میکند که مجموع جاری از صفر شروع شود.
۲. انباشت داده در یک آرایه
میتوانید از `scan` برای انباشت دادهها از یک async iterator در یک آرایه استفاده کنید. این کار میتواند برای جمعآوری دادهها در طول زمان و پردازش آنها به صورت دستهای مفید باشد.
async function* fetchData() {
yield { id: 1, name: 'Alice' };
yield { id: 2, name: 'Bob' };
yield { id: 3, name: 'Charlie' };
}
async function main() {
const dataStream = fetchData();
const accumulatedData = scan(dataStream, (acc, value) => [...acc, value], []);
for await (const data of accumulatedData) {
console.log(data); // Output: [{id: 1, name: 'Alice'}], [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}], [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}, {id: 3, name: 'Charlie'}]
}
}
main();
در اینجا، تابع `accumulator` از عملگر spread (`...`) برای ایجاد یک آرایه جدید حاوی تمام عناصر قبلی و مقدار فعلی استفاده میکند. `initialValue` یک آرایه خالی است.
۳. پیادهسازی یک محدودکننده نرخ (Rate Limiter)
یک مورد استفاده پیچیدهتر، پیادهسازی یک محدودکننده نرخ است. میتوانید از `scan` برای ردیابی تعداد درخواستهای ارسال شده در یک بازه زمانی مشخص و به تأخیر انداختن درخواستهای بعدی در صورت تجاوز از حد مجاز استفاده کنید.
async function* generateRequests() {
// Simulate incoming requests
yield Date.now();
await new Promise(resolve => setTimeout(resolve, 200));
yield Date.now();
await new Promise(resolve => setTimeout(resolve, 100));
yield Date.now();
}
async function main() {
const requests = generateRequests();
const rateLimitWindow = 1000; // 1 second
const maxRequestsPerWindow = 2;
async function* rateLimitedRequests(source, window, maxRequests) {
let queue = [];
for await (const requestTime of source) {
queue.push(requestTime);
queue = queue.filter(t => requestTime - t < window);
if (queue.length > maxRequests) {
const earliestRequest = queue[0];
const delay = window - (requestTime - earliestRequest);
console.log(`Rate limit exceeded. Delaying for ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
yield requestTime;
}
}
const limited = rateLimitedRequests(requests, rateLimitWindow, maxRequestsPerWindow);
for await (const requestTime of limited) {
console.log(`Request processed at ${requestTime}`);
}
}
main();
این مثال از `scan` به صورت داخلی (در تابع `rateLimitedRequests`) برای نگهداری یک صف از مهرهای زمانی درخواستها استفاده میکند. این تابع بررسی میکند که آیا تعداد درخواستها در بازه زمانی مجاز از حداکثر تعداد فراتر رفته است یا خیر. اگر چنین باشد، تأخیر لازم را محاسبه کرده و قبل از yield کردن درخواست، مکث میکند.
۴. ساخت یک تجمیعکننده داده آنی (مثال جهانی)
یک برنامه مالی جهانی را در نظر بگیرید که نیاز به تجمیع قیمتهای لحظهای سهام از بورسهای مختلف دارد. یک async iterator میتواند بهروزرسانیهای قیمت را از بورسهایی مانند بورس اوراق بهادار نیویورک (NYSE)، بورس اوراق بهادار لندن (LSE) و بورس اوراق بهادار توکیو (TSE) استریم کند. از `scan` میتوان برای نگهداری میانگین جاری یا بالاترین/پایینترین قیمت برای یک سهم خاص در تمام بورسها استفاده کرد.
// Simulate streaming stock prices from different exchanges
async function* generateStockPrices() {
yield { exchange: 'NYSE', symbol: 'AAPL', price: 170.50 };
yield { exchange: 'LSE', symbol: 'AAPL', price: 170.75 };
await new Promise(resolve => setTimeout(resolve, 50));
yield { exchange: 'TSE', symbol: 'AAPL', price: 170.60 };
}
async function main() {
const stockPrices = generateStockPrices();
// Use scan to calculate a running average price
const runningAverages = scan(
stockPrices,
(acc, priceUpdate) => {
const { total, count } = acc;
return { total: total + priceUpdate.price, count: count + 1 };
},
{ total: 0, count: 0 }
);
for await (const averageData of runningAverages) {
const averagePrice = averageData.total / averageData.count;
console.log(`Running average price: ${averagePrice.toFixed(2)}`);
}
}
main();
در این مثال، تابع `accumulator` مجموع جاری قیمتها و تعداد بهروزرسانیهای دریافتی را محاسبه میکند. سپس قیمت میانگین نهایی از این مقادیر انباشته شده محاسبه میشود. این کار یک نمای آنی از قیمت سهام در بازارهای مختلف جهانی فراهم میکند.
۵. تحلیل ترافیک وبسایت در سطح جهانی
یک پلتفرم جهانی تحلیل وب را تصور کنید که جریانهایی از دادههای بازدید وبسایت را از سرورهای واقع در سراسر جهان دریافت میکند. هر نقطه داده نشاندهنده بازدید یک کاربر از وبسایت است. با استفاده از `scan`، میتوانیم روند بازدید صفحات بر اساس کشور را به صورت آنی تحلیل کنیم. فرض کنید دادهها به این شکل هستند: `{ country: "US", page: "homepage", timestamp: 1678886400 }`.
async function* generateWebsiteVisits() {
yield { country: 'US', page: 'homepage', timestamp: Date.now() };
yield { country: 'CA', page: 'product', timestamp: Date.now() };
yield { country: 'UK', page: 'blog', timestamp: Date.now() };
yield { country: 'US', page: 'product', timestamp: Date.now() };
}
async function main() {
const visitStream = generateWebsiteVisits();
const pageViewCounts = scan(
visitStream,
(acc, visit) => {
const { country } = visit;
const newAcc = { ...acc };
newAcc[country] = (newAcc[country] || 0) + 1;
return newAcc;
},
{}
);
for await (const counts of pageViewCounts) {
console.log('Page view counts by country:', counts);
}
}
main();
در اینجا، تابع `accumulator` یک شمارنده برای هر کشور را بهروز میکند. خروجی، شمارش تجمعی بازدید صفحات برای هر کشور را با رسیدن دادههای بازدید جدید نشان میدهد.
مزایای استفاده از `scan`
راهنمای `scan` چندین مزیت هنگام کار با جریانهای داده ناهمگام ارائه میدهد:
- سبک اعلانی (Declarative): `scan` به شما امکان میدهد منطق پردازش تجمعی را به شیوهای اعلانی و مختصر بیان کنید که خوانایی و قابلیت نگهداری کد را بهبود میبخشد.
- مدیریت ناهمگام: این ابزار به طور یکپارچه عملیات ناهمگام را در تابع انباشتگر مدیریت میکند و آن را برای سناریوهای پیچیده شامل وظایف وابسته به ورودی/خروجی مناسب میسازد.
- پردازش آنی: `scan` پردازش آنی جریانهای داده را امکانپذیر میسازد و به شما اجازه میدهد تا به تغییرات در حین وقوع واکنش نشان دهید.
- قابلیت ترکیب (Composability): میتوان آن را به راحتی با سایر راهنماهای async iterator ترکیب کرد تا پایپلاینهای پردازش داده پیچیده ایجاد شود.
پیادهسازی `scan` (در صورت عدم وجود)
در حالی که برخی کتابخانهها یک راهنمای `scan` داخلی ارائه میدهند، در صورت نیاز میتوانید به راحتی نسخه خود را پیادهسازی کنید. در اینجا یک پیادهسازی ساده آورده شده است:
async function* scan(sourceIterator, accumulator, initialValue) {
let accumulatedValue = initialValue;
let first = true;
for await (const value of sourceIterator) {
if (first && initialValue === undefined) {
accumulatedValue = value;
first = false;
} else {
accumulatedValue = accumulator(accumulatedValue, value);
}
yield accumulatedValue;
}
}
این پیادهسازی بر روی iterator منبع پیمایش میکند و تابع انباشتگر را به هر مقدار اعمال میکند و نتیجه انباشته شده را yield میکند. این پیادهسازی حالتی را که `initialValue` ارائه نشده باشد، با استفاده از اولین مقدار از iterator منبع به عنوان مقدار اولیه، مدیریت میکند.
مقایسه با `reduce`
مهم است که `scan` را از `reduce` متمایز کنیم. در حالی که هر دو بر روی iteratorها عمل میکنند و از یک تابع انباشتگر استفاده میکنند، در رفتار و خروجی خود تفاوت دارند.
- `scan` مقدار انباشته شده را برای هر تکرار منتشر میکند و یک تاریخچه جاری از انباشت را فراهم میکند.
- `reduce` فقط مقدار انباشته شده نهایی را پس از پردازش تمام عناصر در iterator منتشر میکند.
بنابراین، `scan` برای سناریوهایی مناسب است که نیاز به ردیابی وضعیتهای میانی انباشت دارید، در حالی که `reduce` زمانی مناسب است که فقط به نتیجه نهایی نیاز دارید.
مدیریت خطا
هنگام کار با iteratorهای ناهمگام و `scan`، مدیریت صحیح خطاها بسیار مهم است. خطاها میتوانند در طول فرآیند پیمایش یا درون تابع انباشتگر رخ دهند. میتوانید از بلوکهای `try...catch` برای گرفتن و مدیریت این خطاها استفاده کنید.
async function* generatePotentiallyFailingData() {
yield 1;
yield 2;
throw new Error('Something went wrong!');
yield 3;
}
async function main() {
const dataStream = generatePotentiallyFailingData();
try {
const accumulatedData = scan(dataStream, (acc, value) => acc + value, 0);
for await (const data of accumulatedData) {
console.log(data);
}
} catch (error) {
console.error('An error occurred:', error);
}
}
main();
در این مثال، بلوک `try...catch` خطای پرتاب شده توسط iterator `generatePotentiallyFailingData` را میگیرد. سپس میتوانید خطا را به طور مناسب مدیریت کنید، مانند ثبت آن در لاگ یا تلاش مجدد برای انجام عملیات.
نتیجهگیری
راهنمای `scan` ابزاری قدرتمند برای انجام پردازش تجمعی ناهمگام بر روی async iteratorهای جاوا اسکریپت است. این ابزار به شما امکان میدهد تا تبدیلات داده پیچیده را به شیوهای اعلانی و مختصر بیان کنید، عملیات ناهمگام را به درستی مدیریت کنید و جریانهای داده را به صورت آنی پردازش کنید. با درک عملکرد و موارد استفاده آن، میتوانید از `scan` برای ساخت برنامههای ناهمگام قویتر و کارآمدتر استفاده کنید. چه در حال محاسبه مجموع جاری، انباشت داده در آرایهها، پیادهسازی محدودکنندههای نرخ یا ساخت تجمیعکنندههای داده آنی باشید، `scan` میتواند کد شما را ساده کرده و عملکرد کلی آن را بهبود بخشد. به یاد داشته باشید که مدیریت خطا را در نظر بگیرید و زمانی که به مقادیر انباشته شده میانی در طول پردازش جریانهای داده ناهمگام خود نیاز دارید، `scan` را به جای `reduce` انتخاب کنید. کاوش در کتابخانههایی مانند RxJS میتواند درک و کاربرد عملی شما از `scan` را در پارادایمهای برنامهنویسی واکنشی (reactive) بیشتر کند.