Generator در جاوااسکریپت این امکان را میدهد که به راحتی iteratorها را تعریف کنیم و کدی بنویسیم که بتوان آن را متوقف و در ادامه اجرا کرد. این قابلیت کنترل دقیقتری بر جریان اجرای کد فراهم میکند.
بسیاری از توسعهدهندگان برای مدیریت تسکهای asynchronous به ابزارهایی مانند RxJS یا سایر observableها روی میآورند، اما generatorها اغلب نادیده گرفته میشوند، در حالی که میتوانند بسیار قدرتمند باشند.
در این مقاله بررسی میکنیم که چگونه generatorها به ما اجازه میدهند از یک تابع خارج شویم، state را مدیریت کنیم، پیشرفت عملیاتهای طولانی را گزارش دهیم و در نهایت کدی خواناتر داشته باشیم. همچنین، آنها را با راهحلهای محبوبی مانند RxJS مقایسه میکنیم تا ببینیم generatorها در چه مواردی بهتر عمل میکنند و چه زمانی استفاده از روشهای دیگر منطقیتر است.
به طور ساده، generator یک نوع خاص از تابع در جاوااسکریپت است که میتواند در حین اجرا متوقف و بعداً ادامه داده شود. این ویژگی به ما اجازه میدهد جریان اجرای کد را دقیقتر از توابع عادی کنترل کنیم.
یک تابع generator در جاوااسکریپت یک آبجکت generator برمیگرداند که با پروتکلهای iterable و iterator سازگار است.
Generatorها برای اولین بار در ES6 معرفی شدند و از آن زمان به یکی از قابلیتهای مهم جاوااسکریپت تبدیل شدهاند. برای تعریف آنها از کلمهی کلیدی
function
همراه با *
استفاده میشود، به این شکل: function*
.
در ادامه، یک مثال از نحوه استفاده از generatorها را داریم:
function* generatorFunction() { return "Hello World"; //generator body }
گاهی اوقات ممکن است
*
را قبل از نام تابع ببینیم، مانند function*
. در حالی که این سینتکس کمتر رایج است، اما همچنان معتبر میباشد.
در نگاه اول، یک generator در جاوااسکریپت ممکن است شبیه یک تابع عادی به نظر برسد (به جز
*
)، اما تفاوتهای مهمی وجود دارد که آنها را منحصربهفرد و قدرتمند میکند.
در یک تابع عادی، زمانی که آن را فراخوانی میکنیم، از ابتدا تا انتها اجرا میشود و هیچ راهی برای توقف در میانه و ادامه آن وجود ندارد. اما generatorها این امکان را میدهند که اجرای کد را در هر نقطهای که
yield
قرار دارد متوقف کنیم و بعداً آن را ادامه دهیم.
این ویژگی توقفپذیری، باعث میشود که state بین توقفهای مختلف حفظ شود، که این قابلیت را برای پردازش مجموعه دادههای بزرگ در بخشهای کوچک ایدهآل میکند. علاوه بر این، در حالی که یک تابع عادی هنگام اجرا مقدار نهایی خود را return میکند، generatorها هنگام فراخوانی یک آبجکت generator برمیگردانند. این آبجکت یک iterator است که میتوان از آن برای پیمایش در دنبالهای از مقادیر استفاده کرد.
هنگامی که با یک generator کار میکنیم، آن را فقط یکبار فراخوانی نمیکنیم و تمام! بلکه با استفاده از متدهایی مانند
next()
، throw()
و return()
میتوانیم state آن را از بیرون کنترل نماییم:
next(value)
: اجرای generator را ادامه میدهد. همچنین، میتواند یک مقدار را به داخل generator ارسال کند که این مقدار توسط آخرین yield
دریافت میشود. این متد یک آبجکت شامل دو ویژگی value
و done
(که مشخص میکند iterator به پایان رسیده است یا نه) را برمیگرداند.throw(error)
: یک خطا را در داخل throw generator میکند، که به ما امکان مدیریت استثناها را میدهد.return(value)
: اجرای generator را زودتر از موعد خاتمه داده و مقدار مشخص شده را برمیگرداند.این ارتباط دوطرفه یک برتری بزرگ نسبت به توابع عادی محسوب میشود و میتواند برای مدیریت پردازشهای پیچیده، جریانهای کاری و کنترل خطاها مفید باشد.
برای شروع، تابع generator
Hello World
را که قبلاً نشان دادیم، مقداردهی اولیه کرده و value آن را دریافت میکنیم:
const generator = generatorFunction();
وقتی تابع
generatorFunction()
را فراخوانی کرده و در یک متغیر ذخیره میکنیم، بلافاصله رشته "Hello World"
را دریافت نمیکنیم. در عوض، یک آبجکت generator دریافت میکنیم که در ابتدا در حالت suspended قرار دارد. این یعنی اجرا متوقف شده و هنوز هیچ کدی اجرا نشده است.
اگر
generator
را در کنسول لاگ بگیریم، میبینیم که یک مقدار ساده نیست، بلکه یک آبجکت است که نشان میدهد generator هنوز فعال میباشد. برای دریافت مقدار تابع generator، باید متد next()
را روی این آبجکت فراخوانی کنیم:
const result = generator.next();
خروجی حاصل به صورت زیر خواهد بود:
{ value: 'Hello World', done: true }
این فراخوانی، رشته “Hello World” را به عنوان مقدار کلید value در آبجکت بازگشتی به ما میدهد. همچنین، ویژگی
done
مقدار true
خواهد داشت، زیرا دیگر هیچ کدی برای اجرا باقی نمانده است. در نتیجه، وضعیت تابع آبجکت generator از suspended به closed تغییر میکند.
تا اینجا، فقط دیدیم که چگونه میتوانیم یک مقدار واحد را از یک تابع آبجکت generator برگردانیم. اما اگر بخواهیم چند مقدار مختلف را return کنیم، چه کاری باید انجام دهیم؟
اینجاست که عملگر
yield
به کمک ما میآید.
yield
Generator در جاوااسکریپت به ما اجازه میدهد اجرای تابع را متوقف کرده و دوباره آن را ادامه دهیم، و این کار با استفاده از کلمهی کلیدی
yield
انجام میشود.
به عنوان مثال، فرض کنید یک تابع generator به این شکل داریم:
function* generatorFunction() { yield "first value"; yield "second value"; yield "third value"; yield "last value"; }
هر بار که متد
next()
را روی generator فراخوانی میکنیم، تابع تا زمانی که به یک عبارت yield
برسد، اجرا میشود و سپس متوقف میگردد. در این لحظه، تابع generator یک آبجکت شامل دو ویژگی را return میکند:
value
: مقداری که yield
return کرده است.done
: یک مقدار Boolean که نشان میدهد generator به پایان رسیده است یا نه.تا زمانی که یک
yield
دیگر در تابع وجود داشته باشد (یا return
اجرا نشده باشد)، مقدار done
برابر با false
خواهد بود. اما به محض این که generator هیچ yield
ای برای اجرا نداشته باشد، مقدار done
به true
تغییر میکند.
اگر متد
next()
را چهار بار روی یک generator که سه yield
دارد، اجرا کنیم، خروجی به این شکل خواهد بود:
const generator = generatorFunction(); generator.next(); // { value: 'first value', done: false } generator.next(); // { value: 'second value', done: false } generator.next(); // { value: 'third value', done: false } generator.next(); // { value: 'last value', done: true }
نکته جالب این است که
yield
فقط برای return کردن value
استفاده نمیشود؛ بلکه مثل یک مسیر دوطرفه عمل میکند و میتواند مقادیر را از بیرون دریافت نماید. این یعنی بین generator و کد فراخوانیکننده آن، ارتباط دوطرفه برقرار میشود.
برای ارسال مقدار به یک تابع generator در جاوااسکریپت، میتوانیم متد
next()
را همراه با یک آرگومان فراخوانی کنیم. به عنوان مثال:
function* generatorFunction() { console.log(yield); console.log(yield); } const generator = generatorFunction(); generator.next(); // First call — no yield has been paused yet, so nothing to pass in generator.next("first input"); generator.next("second input");
این کد موارد زیر را به ترتیب ثبت میکند:
first input second input
اگر دقت کنیم میبینیم که در فراخوانی اول
generator.next()
، هیچ مقداری چاپ نمیشود، دلیل این است که هنوز هیچ yield
معلقی برای دریافت مقدار وجود ندارد.generator.next("first input")
است، مقدار "first input"
به yield
معلق قبلی ارسال شده و در خروجی چاپ میشود. همین الگو برای فراخوانیهای بعدی نیز دنبال میشود.
این دقیقاً روشی است که generatorها امکان ارسال و دریافت داده را بین خود و فراخوانیکننده فراهم میکنند.
async generatorها با معرفی ECMAScript 2017، به جاوااسکریپت اضافه شدند. این نوع خاص از توابع generator، با Promiseها کار میکنند.
با کمک async generatorها، دیگر محدود به اجرای کدهای synchronous نیستیم.
اکنون میتوانیم تسکهایی مانند دریافت داده از یک API، خواندن فایلها، یا هر کاری که نیاز به انتظار برای یک Promise دارد را با این روش مدیریت نماییم.
در ادامه مثالی از یک تابع async generator را داریم:
async function* asyncGenerator() { yield await Promise.resolve("1"); yield await Promise.resolve("2"); yield await Promise.resolve("3"); } const generator = asyncGenerator(); await generator.next(); // { value: '1', done: false } await generator.next(); // { value: '2', done: false } await generator.next(); // { value: '3', done: true }
تفاوت اصلی این نوع generator این است که باید روی هر فراخوانی
generator.next()
از await
استفاده کنیم تا مقدار را دریافت نماییم، زیرا همه چیز به شکل asynchronous اجرا میشود.
در این بخش بررسی میکنیم که چگونه میتوانیم از async generatorها برای دریافت دادههای صفحهبندیشده از یک API ریموت استفاده کنیم.
این یک سناریوی عالی برای استفاده از async generatorها است، زیرا میتوانیم منطق تکرار متوالی دادهها را در یک تابع واحد قرار دهیم.
در این مثال، از API رایگان DummyJSON استفاده میکنیم تا لیستی از محصولات صفحهبندیشده را دریافت نماییم.
برای دریافت داده از این API، باید یک درخواست GET به این Endpoint ارسال کنیم. پارامترهای
limit
و skip
را تنظیم میکنیم تا بتوانیم تعداد نتایج و میزان جابهجایی در لیست را مشخص نماییم:
https://dummyjson.com/products?limit=10&skip=0
یک نمونه از پاسخ این Endpoint میتواند به شکل زیر باشد:
{ "products": [ { "id": 1, "title": "Annibale Colombo Bed", "price": 1899.99 }, {...}, // ۱۰ items ], "total": 194, "skip": 0, "limit": 10 }
برای بارگذاری مجموعه بعدی از محصولات، تنها کاری که باید انجام دهیم این است که مقدار
skip
را به اندازه limit
افزایش دهیم تا تمام دادهها را دریافت کنیم.
به این ترتیب، میتوانیم یک تابع generator سفارشی برای دریافت دادهها را به صورت زیر پیادهسازی نماییم:
async function* fetchProducts(skip = 0, limit = 10) { let total = 0; do { const response = await fetch( `https://dummyjson.com/products?limit=${limit}&skip=${skip}`, ); const { products, total: totalProducts } = await response.json(); total = totalProducts; skip += limit; yield products; } while (skip < total); }
اکنون میتوانیم با استفاده از حلقه
for await...of
روی این تابع تکرار کنیم تا همه محصولات را دریافت نماییم:
for await (const products of fetchProducts()) { for (const product of products) { console.log(product.title); } }
با این روش، تمامی محصولات را یکی پس از دیگری دریافت میکنیم تا زمانی که دیگر دادهای برای دریافت باقی نماند:
Essence Mascara Lash Princess Eyeshadow Palette with Mirror Powder Canister Red Lipstick Red Nail Polish ... // ۱۵ more items
در نتیجه، با استفاده از async generatorها، فرآیند دریافت دادههای صفحهبندیشده کارآمدتر، خواناتر و سادهتر از همیشه خواهد بود.
Generatorها میتوانند به عنوان State Machineهای ساده عمل کنند، چون به خاطر میسپارند که در کجا متوقف شدهاند.
اما همیشه بهترین گزینه برای مدیریت state نیستند، مخصوصاً وقتی فریمورکهای مدرن جاوااسکریپت ابزارهای پیشرفتهای برای مدیریت state دارند.
چرا استفاده از generatorها به عنوان State Machine همیشه ایدهآل نیست؟
با این که generatorها حافظه داخلی دارند و میتوانند فرآیندها را ادامه دهند، اما در بسیاری از موارد، پیادهسازی یک State Machine با آنها در جاوااسکریپت پیچیدگی زیادی دارد و ارزش استفاده از روشهای سادهتر را کم میکند.
راه حل جایگزین، مدل Actor است. اگر همچنان بخواهیم چنین رویکردی را بررسی کنیم، مدل Actor گزینه بهتری خواهد بود. این مدل که از زبان Erlang نشأت گرفته، بر پایه واحدهای مستقلی کار میکند که state و رفتار خود را مدیریت میکنند و فقط از طریق ارسال پیام با یکدیگر ارتباط میگیرند. این روش باعث ماژولار شدن سیستم و مدیریت بهتر تغییرات state میشود.
وقتی صحبت از پردازش Web Streamها میشود، هم generatorها و هم RxJS ابزارهای قدرتمندی هستند، اما هرکدام مزایا و معایب خاص خود را دارند.
خبر خوب این است که میتوانیم از هر دو آنها در کنار هم استفاده کنیم.
فرض کنید یک API داریم که چندین رشته ۸ کاراکتری تصادفی را به صورت Stream بازمیگرداند.
در قدم اول، میتوانیم یک تابع generator تعریف کنیم که به صورت Lazy، تکههای داده را یکییکی return میکند:
// Fetch data from HTTP stream async function* fetchStream() { const response = await fetch("https://example/api/stream"); const reader = response.body?.getReader(); if (!reader) throw new Error(); try { while (true) { const { done, value } = await reader.read(); if (done) break; yield value; } } catch (error) { throw error; } finally { reader.releaseLock(); } }
فراخوانی
fetchStream()
یک async generator بازمیگرداند، و سپس میتوانیم دادهها را با یک حلقه پردازش کنیم.
اما RxJS میتواند قابلیتهای بیشتری به این فرآیند اضافه نماید.
RxJS مجموعهای از عملگرهای قدرتمند مثل:
map
: برای تبدیل دادههاfilter
: برای فیلتر کردن دادههای خاصtake
: برای محدود کردن تعداد دادهها را در اختیار ما قرار میدهد.برای استفاده از این قابلیتها، ابتدا generator را به یک observable تبدیل میکنیم.
اکنون از عملگر
take
برای فیلتر کردن پنج تکه اول داده استفاده خواهیم کرد:
import { from, take } from "rxjs"; // Consume HTTP stream using RxJS async () => { from(fetchStream()) .pipe(take(5)) .subscribe({ next: (chunk) => { const decoder = new TextDecoder(); console.log("Chunk:", decoder.decode(chunk)); }, complete: () => { console.log("Stream complete"); }, }); };
در این مثال، عملگر
from
در RxJS، تبدیل Generator به یک Observable را انجام میدهد که اجازه میدهد دادهها را به شکل synchronous مدیریت کنیم.
خروجی کد به صورت زیر خواهد بود:
Chunk: ky^p1egh Chunk: 1q)zIz43 Chunk: xm5aJGSX Chunk: GSx6a2UQ Chunk: GFlwWPu^ Stream complete
for await...of
استفاده کنیم؟بله، میتوانیم Stream را به صورت ساده و بدون RxJS مورد استفاده قرار دهیم:
// Consume the HTTP stream using for-await-of for await (const chunk of fetchStream()) { const decoder = new TextDecoder(); console.log("Chunk:", decoder.decode(chunk)); }
اما در این روش، مزایای RxJS را از دست میدهیم، یعنی نمیتوانیم از عملگرهایی مثل
take
برای محدود کردن تعداد دادهها استفاده کرد. همچین، مدیریت دادههای پیچیده دشوارتر خواهد شد.
اما خبر خوبی که وجود دارد این است که ویژگی Iteration Helpers در نسخه بعدی ECMAScript (در حال حاضر در مرحله ۴) اضافه خواهد شد. این قابلیت به ما اجازه میدهد که به طور Native خروجی generatorها را فیلتر یا محدود نماییم؛ درست مثل کاری که RxJS برای observableها انجام میدهد.
Generator در جاوااسکریپت یک روش قدرتمند، اما کمتر شناخته شده برای مدیریت عملیاتهای asynchronous، کنترل state و پردازش جریانهای داده است. با امکان توقف و ازسرگیری اجرا، این قابلیت کنترل دقیقتری نسبت به توابع معمولی فراهم میکند، به ویژه زمانی که با تسکهای طولانی یا مجموعه دادههای بزرگ سروکار داریم.
با اینحال، در حالی که generatorها در بسیاری از سناریوها عملکرد خوبی دارند، اما ابزارهایی مانند RxJS یک اکوسیستم قدرتمند از اپراتورها ارائه میدهند که مدیریت جریانهای پیچیده و رویدادمحور را سادهتر میکنند.
البته نیازی به انتخاب قطعی بین این دو روش نیست؛ بلکه میتوانیم بسته به نیاز خود، سادگی generatorها را با قدرت تبدیلهای RxJS ترکیب کرده یا حتی فقط از یک حلقه ساده
for await...of
در پروژه خود استفاده کنیم.
در آینده، Iteration Helperهای جدید ممکن است امکانات generatorها را به RxJS نزدیکتر کنند، اما در آیندهای قابل پیشبینی، RxJS همچنان گزینهای کلیدی برای مدیریت الگوهای واکنشی پیچیده خواهد بود.
۵۰ درصد تخفیف ویژه نوروز فرانت کست تا پایان هفته
کد تخفیف: spr
دیدگاهها: