الگوهای پیشرفته جنریتورهای جاوااسکریپت شامل تکرار ناهمزمان، پیادهسازی ماشین حالت و موارد استفاده عملی برای توسعه وب مدرن را کاوش کنید.
جنریتورهای جاوااسکریپت: الگوهای پیشرفته برای تکرار ناهمزمان و ماشینهای حالت
جنریتورهای جاوااسکریپت که در ES6 معرفی شدند، مکانیزم قدرتمندی برای ایجاد اشیاء قابل تکرار و مدیریت جریان کنترل پیچیده فراهم میکنند. در حالی که استفاده اولیه از آنها نسبتاً ساده است، پتانسیل واقعی جنریتورها در توانایی آنها برای مدیریت عملیات ناهمزمان و پیادهسازی ماشینهای حالت نهفته است. این مقاله به الگوهای پیشرفته با استفاده از جنریتورهای جاوااسکریپت میپردازد و بر روی تکرار ناهمزمان و پیادهسازی ماشین حالت، همراه با مثالهای عملی مرتبط با توسعه وب مدرن تمرکز دارد.
درک جنریتورهای جاوااسکریپت
قبل از پرداختن به الگوهای پیشرفته، بیایید به طور خلاصه اصول اولیه جنریتورهای جاوااسکریپت را مرور کنیم.
جنریتورها چه هستند؟
جنریتور یک نوع خاص از تابع است که میتوان آن را متوقف و دوباره از سر گرفت، که به شما اجازه میدهد جریان اجرای یک تابع را کنترل کنید. جنریتورها با استفاده از سینتکس function*
تعریف میشوند و از کلمه کلیدی yield
برای توقف اجرا و بازگرداندن یک مقدار استفاده میکنند.
مفاهیم کلیدی:
function*
: یک تابع جنریتور را مشخص میکند.yield
: اجرای تابع را متوقف کرده و یک مقدار را باز میگرداند.next()
: اجرای تابع را از سر میگیرد و به صورت اختیاری مقداری را به داخل جنریتور پاس میدهد.return()
: جنریتور را خاتمه داده و یک مقدار مشخص را باز میگرداند.throw()
: یک خطا در داخل تابع جنریتور پرتاب میکند.
مثال:
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const generator = numberGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
تکرار ناهمزمان با جنریتورها
یکی از قدرتمندترین کاربردهای جنریتورها در مدیریت عملیات ناهمزمان است، به ویژه هنگام کار با جریانهای داده. تکرار ناهمزمان به شما امکان میدهد دادهها را به محض در دسترس قرار گرفتن پردازش کنید، بدون اینکه نخ اصلی را مسدود کنید.
مشکل: جهنم کالبکها و پرامیسها
برنامهنویسی ناهمزمان سنتی در جاوااسکریپت اغلب شامل کالبکها یا پرامیسها میشود. در حالی که پرامیسها ساختار را در مقایسه با کالبکها بهبود میبخشند، مدیریت جریانهای ناهمزمان پیچیده هنوز هم میتواند دشوار باشد.
جنریتورها، در ترکیب با پرامیسها یا async/await
، روشی تمیزتر و خواناتر برای مدیریت تکرار ناهمزمان ارائه میدهند.
ایتراتورهای ناهمزمان
ایتراتورهای ناهمزمان یک رابط استاندارد برای پیمایش منابع داده ناهمزمان فراهم میکنند. آنها شبیه به ایتراتورهای معمولی هستند اما از پرامیسها برای مدیریت عملیات ناهمزمان استفاده میکنند.
ایتراتورهای ناهمزمان یک متد next()
دارند که یک پرامیس را باز میگرداند که به یک شی با ویژگیهای value
و done
حل میشود.
مثال:
async function* asyncNumberGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
async function consumeGenerator() {
const generator = asyncNumberGenerator();
console.log(await generator.next()); // { value: 1, done: false }
console.log(await generator.next()); // { value: 2, done: false }
console.log(await generator.next()); // { value: 3, done: false }
console.log(await generator.next()); // { value: undefined, done: true }
}
consumeGenerator();
موارد استفاده واقعی برای تکرار ناهمزمان
- جریانسازی داده از یک API: دریافت داده به صورت تکهای از یک سرور با استفاده از صفحهبندی. یک پلتفرم رسانه اجتماعی را تصور کنید که در آن میخواهید پستها را به صورت دستهای دریافت کنید تا مرورگر کاربر را تحت فشار قرار ندهید.
- پردازش فایلهای بزرگ: خواندن و پردازش فایلهای بزرگ به صورت خط به خط بدون بارگذاری کل فایل در حافظه. این موضوع در سناریوهای تحلیل داده بسیار حیاتی است.
- جریانهای داده آنی: مدیریت دادههای آنی از یک WebSocket یا جریان Server-Sent Events (SSE). به یک برنامه نتایج زنده ورزشی فکر کنید.
مثال: جریانسازی داده از یک API
بیایید مثالی از دریافت داده از یک API که از صفحهبندی استفاده میکند را در نظر بگیریم. ما یک جنریتور ایجاد خواهیم کرد که دادهها را به صورت تکهای دریافت میکند تا زمانی که تمام دادهها بازیابی شوند.
async function* paginatedDataFetcher(url, pageSize = 10) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${url}?page=${page}&pageSize=${pageSize}`);
const data = await response.json();
if (data.length === 0) {
hasMore = false;
return;
}
for (const item of data) {
yield item;
}
page++;
}
}
async function consumeData() {
const dataStream = paginatedDataFetcher('https://api.example.com/data');
for await (const item of dataStream) {
console.log(item);
// Process each item as it arrives
}
console.log('Data stream complete.');
}
consumeData();
در این مثال:
paginatedDataFetcher
یک جنریتور ناهمزمان است که دادهها را از یک API با استفاده از صفحهبندی دریافت میکند.- دستور
yield item
اجرا را متوقف کرده و هر آیتم داده را باز میگرداند. - تابع
consumeData
از یک حلقهfor await...of
برای پیمایش ناهمزمان جریان داده استفاده میکند.
این رویکرد به شما امکان میدهد دادهها را به محض در دسترس قرار گرفتن پردازش کنید، که آن را برای مدیریت مجموعه دادههای بزرگ کارآمد میسازد.
ماشینهای حالت با جنریتورها
یکی دیگر از کاربردهای قدرتمند جنریتورها، پیادهسازی ماشینهای حالت است. ماشین حالت یک مدل محاسباتی است که بر اساس رویدادهای ورودی بین حالتهای مختلف جابجا میشود.
ماشینهای حالت چه هستند؟
ماشینهای حالت برای مدلسازی سیستمهایی استفاده میشوند که تعداد محدودی حالت و انتقال بین آن حالتها دارند. آنها به طور گسترده در مهندسی نرمافزار برای طراحی سیستمهای پیچیده استفاده میشوند.
اجزای کلیدی یک ماشین حالت:
- حالتها (States): شرایط یا مدهای مختلف سیستم را نشان میدهند.
- رویدادها (Events): انتقال بین حالتها را فعال میکنند.
- انتقالها (Transitions): قوانین جابجایی از یک حالت به حالت دیگر بر اساس رویدادها را تعریف میکنند.
پیادهسازی ماشینهای حالت با جنریتورها
جنریتورها روشی طبیعی برای پیادهسازی ماشینهای حالت فراهم میکنند زیرا میتوانند حالت داخلی را حفظ کرده و جریان اجرا را بر اساس رویدادهای ورودی کنترل کنند.
هر دستور yield
در یک جنریتور میتواند یک حالت را نشان دهد و از متد next()
میتوان برای فعال کردن انتقال بین حالتها استفاده کرد.
مثال: یک ماشین حالت ساده چراغ راهنمایی
بیایید یک ماشین حالت ساده چراغ راهنمایی با سه حالت را در نظر بگیریم: قرمز (RED)
، زرد (YELLOW)
و سبز (GREEN)
.
function* trafficLightStateMachine() {
let state = 'RED';
while (true) {
switch (state) {
case 'RED':
console.log('Traffic Light: RED');
state = yield;
break;
case 'YELLOW':
console.log('Traffic Light: YELLOW');
state = yield;
break;
case 'GREEN':
console.log('Traffic Light: GREEN');
state = yield;
break;
default:
console.log('Invalid State');
state = yield;
}
}
}
const trafficLight = trafficLightStateMachine();
trafficLight.next(); // Initial state: RED
trafficLight.next('GREEN'); // Transition to GREEN
trafficLight.next('YELLOW'); // Transition to YELLOW
trafficLight.next('RED'); // Transition to RED
در این مثال:
trafficLightStateMachine
یک جنریتور است که ماشین حالت چراغ راهنمایی را نشان میدهد.- متغیر
state
حالت فعلی چراغ راهنمایی را نگه میدارد. - دستور
yield
اجرا را متوقف کرده و منتظر انتقال حالت بعدی میماند. - از متد
next()
برای فعال کردن انتقال بین حالتها استفاده میشود.
الگوهای پیشرفته ماشین حالت
۱. استفاده از اشیاء برای تعاریف حالت
برای اینکه ماشین حالت قابل نگهداریتر باشد، میتوانید حالتها را به عنوان اشیائی با اقدامات مرتبط تعریف کنید.
const states = {
RED: {
name: 'RED',
action: () => console.log('Traffic Light: RED'),
},
YELLOW: {
name: 'YELLOW',
action: () => console.log('Traffic Light: YELLOW'),
},
GREEN: {
name: 'GREEN',
action: () => console.log('Traffic Light: GREEN'),
},
};
function* trafficLightStateMachine() {
let currentState = states.RED;
while (true) {
currentState.action();
const nextStateName = yield;
currentState = states[nextStateName] || currentState; // Fallback to current state if invalid
}
}
const trafficLight = trafficLightStateMachine();
trafficLight.next(); // Initial state: RED
trafficLight.next('GREEN'); // Transition to GREEN
trafficLight.next('YELLOW'); // Transition to YELLOW
trafficLight.next('RED'); // Transition to RED
۲. مدیریت رویدادها با انتقالها
میتوانید انتقالهای صریح بین حالتها را بر اساس رویدادها تعریف کنید.
const states = {
RED: {
name: 'RED',
action: () => console.log('Traffic Light: RED'),
transitions: {
TIMER: 'GREEN',
},
},
YELLOW: {
name: 'YELLOW',
action: () => console.log('Traffic Light: YELLOW'),
transitions: {
TIMER: 'RED',
},
},
GREEN: {
name: 'GREEN',
action: () => console.log('Traffic Light: GREEN'),
transitions: {
TIMER: 'YELLOW',
},
},
};
function* trafficLightStateMachine() {
let currentState = states.RED;
while (true) {
currentState.action();
const event = yield;
const nextStateName = currentState.transitions[event];
currentState = states[nextStateName] || currentState; // Fallback to current state if invalid
}
}
const trafficLight = trafficLightStateMachine();
trafficLight.next(); // Initial state: RED
// Simulate a timer event after some time
setTimeout(() => {
trafficLight.next('TIMER'); // Transition to GREEN
setTimeout(() => {
trafficLight.next('TIMER'); // Transition to YELLOW
setTimeout(() => {
trafficLight.next('TIMER'); // Transition to RED
}, 2000);
}, 5000);
}, 5000);
موارد استفاده واقعی برای ماشینهای حالت
- مدیریت حالت کامپوننت UI: مدیریت حالت یک کامپوننت UI، مانند یک دکمه (مثلاً
IDLE
،HOVER
،PRESSED
،DISABLED
). - مدیریت گردش کار: پیادهسازی گردش کارهای پیچیده، مانند پردازش سفارش یا تأیید اسناد.
- توسعه بازی: کنترل رفتار موجودیتهای بازی (مثلاً
IDLE
،WALKING
،ATTACKING
،DEAD
).
مدیریت خطا در جنریتورها
مدیریت خطا هنگام کار با جنریتورها، به ویژه هنگام کار با عملیات ناهمزمان یا ماشینهای حالت، بسیار مهم است. جنریتورها مکانیزمهایی برای مدیریت خطاها با استفاده از بلوک try...catch
و متد throw()
فراهم میکنند.
استفاده از try...catch
میتوانید از یک بلوک try...catch
در داخل یک تابع جنریتور برای گرفتن خطاهایی که در حین اجرا رخ میدهند، استفاده کنید.
function* errorGenerator() {
try {
yield 1;
throw new Error('Something went wrong');
yield 2; // This line will not be executed
} catch (error) {
console.error('Error caught:', error.message);
yield 'Error handled';
}
yield 3;
}
const generator = errorGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // Error caught: Something went wrong
// { value: 'Error handled', done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
استفاده از throw()
متد throw()
به شما اجازه میدهد تا از بیرون یک خطا را به داخل جنریتور پرتاب کنید.
function* throwGenerator() {
try {
yield 1;
yield 2;
} catch (error) {
console.error('Error caught:', error.message);
yield 'Error handled';
}
yield 3;
}
const generator = throwGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.throw(new Error('External error'))); // Error caught: External error
// { value: 'Error handled', done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
مدیریت خطا در ایتراتورهای ناهمزمان
هنگام کار با ایتراتورهای ناهمزمان، باید خطاهایی را که ممکن است در حین عملیات ناهمزمان رخ دهند، مدیریت کنید.
async function* asyncErrorGenerator() {
try {
yield await Promise.reject(new Error('Async error'));
} catch (error) {
console.error('Async error caught:', error.message);
yield 'Async error handled';
}
}
async function consumeGenerator() {
const generator = asyncErrorGenerator();
console.log(await generator.next()); // Async error caught: Async error
// { value: 'Async error handled', done: false }
}
consumeGenerator();
بهترین شیوهها برای استفاده از جنریتورها
- از جنریتورها برای جریان کنترل پیچیده استفاده کنید: جنریتورها برای سناریوهایی که نیاز به کنترل دقیق بر جریان اجرای یک تابع دارید، بهترین گزینه هستند.
- جنریتورها را با پرامیسها یا
async/await
برای عملیات ناهمزمان ترکیب کنید: این کار به شما امکان میدهد کد ناهمزمان را به سبکی همزمانتر و خواناتر بنویسید. - از ماشینهای حالت برای مدیریت حالتها و انتقالهای پیچیده استفاده کنید: ماشینهای حالت میتوانند به شما در مدلسازی و پیادهسازی سیستمهای پیچیده به روشی ساختاریافته و قابل نگهداری کمک کنند.
- خطاها را به درستی مدیریت کنید: همیشه خطاها را در داخل جنریتورهای خود مدیریت کنید تا از رفتار غیرمنتظره جلوگیری شود.
- جنریتورها را کوچک و متمرکز نگه دارید: هر جنریتور باید یک هدف واضح و مشخص داشته باشد.
- جنریتورهای خود را مستند کنید: مستندات واضحی برای جنریتورهای خود، شامل هدف، ورودیها و خروجیهای آنها، ارائه دهید. این کار درک و نگهداری کد را آسانتر میکند.
نتیجهگیری
جنریتورهای جاوااسکریپت ابزاری قدرتمند برای مدیریت عملیات ناهمزمان و پیادهسازی ماشینهای حالت هستند. با درک الگوهای پیشرفتهای مانند تکرار ناهمزمان و پیادهسازی ماشین حالت، میتوانید کدی کارآمدتر، قابل نگهداریتر و خواناتر بنویسید. چه در حال جریانسازی داده از یک API باشید، چه مدیریت حالتهای کامپوننت UI یا پیادهسازی گردش کارهای پیچیده، جنریتورها یک راهحل انعطافپذیر و زیبا برای طیف گستردهای از چالشهای برنامهنویسی ارائه میدهند. قدرت جنریتورها را برای ارتقاء مهارتهای توسعه جاوااسکریپت خود و ساخت برنامههای قویتر و مقیاسپذیرتر بپذیرید.