۱۳. اجرای غیرهمزمان؛ Event Loop، میکروتسک و ترکیب Promise
در سطح متوسط باید بتوانید ترتیب اجرای کد را پیشبینی کنید و برای عملیات مستقل یا وابسته ساختار مناسب انتخاب کنید. async به معنی اجرای موازی همهٔ کدها نیست. یک حلقهٔ محاسباتی طولانی در همان رشته هنوز میتواند کلیک و رندر را معطل کند. Promise ابزار توصیف نتیجهٔ آینده است؛ بهتنهایی کار سنگین را به رشتهٔ دیگری منتقل نمیکند.
مثال اول؛ ترتیب را قبل از اجرا بنویسید
مثال را در Console یک صفحهٔ عادی مرورگر اجرا کنید. پیش از اجرا روی کاغذ بنویسید کدام پیام اول و کدام آخر است. این مثال به شبکه وابسته نیست؛ بنابراین برای فهمیدن صفها نتیجهٔ قابل تکرارتری دارد.
console.log('A: شروع');
setTimeout(() => console.log('D: تایمر'), 0);
Promise.resolve().then(() => console.log('C: میکروتسک'));
console.log('B: پایان کد همزمان');
// A سپس B سپس C سپس Dکد فعلی تا پایان اجرا میشود؛ callbackِ then در میکروتسک و callback تایمر در یک task بعدی اجرا میشود. صفر در setTimeout به معنی اجرای فوری نیست. زمان واقعی میتواند بیشتر شود، چون صف و شرایط محیط اثر دارند. نتیجهٔ این مثال را به ترتیب همهٔ APIهای Node تعمیم ندهید؛ این درس مدل مرورگر را بررسی میکند.
از این ترتیب یک پیام عملی بگیرید: اگر بعد از تغییر متن دکمه یک محاسبهٔ بسیار طولانی انجام دهید، لزوماً کاربر قبل از محاسبه متن تازه را نمیبیند. همچنین زنجیرهٔ بیپایان میکروتسک میتواند فرصت taskهای بعدی را بگیرد. راهحل کار سنگین، انتخاب ساختار مناسب، تقسیم سنجیدهٔ کار یا در موارد لازم Worker است؛ گذاشتن کلمهٔ async جلوی تابع درمان توقف رابط نیست.
مثال دوم؛ کار مستقل را پشت هم معطل نکنید
فرض کنید داشبورد، فهرست هزینه و نام دستهها را از دو منبع مستقل میخواند. در مثال زیر منبعها ساختگیاند تا رفتار را بدون اینترنت بررسی کنیم. کد را در یک فایل .mjs با Node یا در Console مرورگری با پشتیبانی top-level await اجرا کنید.
const later = (value, ms) => new Promise(resolve => {
setTimeout(() => resolve(value), ms);
});
const expensesPromise = later([{ amount: 180000 }], 40);
const categoriesPromise = later(['آموزش', 'خوراک'], 10);
const [expenses, categories] = await Promise.all([
expensesPromise, categoriesPromise
]);
console.log(expenses[0].amount, categories[0]); // 180000 آموزشترتیب آرایهٔ خروجی، ترتیب ورودی Promise.all است و به ترتیب تمام شدن وابسته نیست. کارها هنگام فراخوانی later آغاز شدهاند؛ Promise.all آنها را آغاز نمیکند، نتیجههایشان را ترکیب میکند. برای عملیات وابسته چنین ترکیبی درست نیست: اگر ابتدا باید شناسهٔ تیکت ساخته شود، درخواست پیام همان تیکت نمیتواند پیش از دریافت شناسه آغاز شود.
همهٔ شکستها یک تصمیم ندارند
در صفحهٔ داشبورد شاید شکست یک بخش نباید بخش دیگر را حذف کند. Promise.all با اولین رد شدن، رد میشود، ولی بقیهٔ عملیات را خودکار لغو نمیکند. allSettled برای گزارش مستقل نتیجهها مناسبتر است. نمونهٔ زیر ادامهٔ مستقلی برای تمرین همین تصمیم است؛ خطای ساختگی را عمداً میسازیم.
const outcomes = await Promise.allSettled([
Promise.resolve({ count: 3 }),
Promise.reject(new Error('گزارش موقتاً در دسترس نیست'))
]);
for (const item of outcomes) {
if (item.status === 'fulfilled') console.log('داده:', item.value);
else console.log('خطا:', item.reason.message);
}در پروژهٔ تیکت، try/catch باید مرز عملیات قابل توضیح را پوشش دهد. catch خالی فقط خطا را ناپدید میکند و ممکن است رابط در حالت انتظار بماند. در finally میتوان کنترلها را آزاد کرد، اما برای درخواستهای همپوشان باید مطمئن شد finally درخواست قدیمی وضعیت درخواست جدید را خراب نمیکند. درس بعد با شناسهٔ درخواست همین مشکل را حل میکند.
تمرین مستقل و پاسخ تشریحی
سه گزارش ساختگی بسازید که یکی از آنها شکست میخورد. ابتدا با Promise.all و سپس با allSettled اجرا کنید و تفاوت اطلاعات قابل نمایش را بنویسید. سپس نسخهای بسازید که عملیات دوم واقعاً به خروجی اول وابسته است. پاسخ راهنما: برای گزارش مستقل، هر outcome به وضعیت همان بخش تبدیل میشود؛ برای وابستگی، await اول قبل از فراخوانی عملیات دوم لازم است. زمان اجرای دقیق را معیار آزمون قرار ندهید؛ ترتیب و نتیجه معیار بهتری است.
معیار تحویل: ترتیب A تا D را توضیح دهید؛ ثابت کنید all نتایج را به ترتیب ورودی برمیگرداند؛ نشان دهید رد شدن all به معنی لغو بقیه نیست؛ برای خطای هر عملیات پیام معنادار داشته باشید. اگر تعداد زیادی درخواست دارید، همه را بیمحدودیت آغاز نکنید؛ کنترل تعداد عملیات همزمان مرحلهٔ بعدی طراحی است.
منابع انگلیسی برای مطالعهٔ تکمیلی
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
تألیف و مثالهای مستقل: تحریریه درسآوند.