در این بخش آخر، میخواهیم به سراغ مباحث عمیقتر و فنیتر برویم و ببینیم کالبکهایی که ثبت میکنیم، دقیقاً چه زمانی صدا زده میشوند.
در APIهای قدیمی که بر پایه کالبک (Callback-based) طراحی میشدند، زمان و چگونگیِ اجرا شدنِ کالبک کاملاً به تصمیم توسعهدهنده آن API بستگی داشت. مثلاً ممکن بود یک کالبک در یک شرایط به صورت همگام (ایستاده و سریع) و در شرایطی دیگر به صورت ناهمگام (با تأخیر) اجرا شود.
function doSomething(callback) {
if (Math.random() > 0.5) {
callback(); // اجرای همگام (Synchronous)
} else {
setTimeout(() => callback(), 1000); // اجرای ناهمگام (Asynchronous)
}
}
طراحی بالا در دنیای برنامهنویسی به شدت منع شده است؛ چون باعث شکلگیری وضعیتی به نام «وضعیت زالگو» (State of Zalgo) میشود. در فضای طراحی APIهای ناهمگام، وضعیت زالگو یعنی یک کالبک در برخی شرایط همگام و در برخی دیگر ناهمگام اجرا شود، که این موضوع ابهام و سردرگمی زیادی برای استفادهکننده ایجاد میکند. (برای مطالعه بیشتر، میتوانید به مقاله «طراحی APIها برای ناهمگامی» مراجعه کنید، جایی که این اصطلاح برای اولین بار به صورت رسمی در آن مطرح شد).
این مدل طراحیِ API، تحلیلِ اثرات جانبی (Side Effects) کد را غیرممکن یا بسیار سخت میکند. به این نمونه نگاه کنید:
let value = 1;
doSomething(() => {
value = 2;
});
console.log(value); // خروجی چنده؟ ۱ یا ۲؟ معلوم نیست!
اما در طرف مقابل، پرامیسها یک نوع «وارونگی کنترل» (Inversion of Control) ایجاد میکنند؛ یعنی توسعهدهنده و سازنده آن API دیگر کنترلی روی زمان اجرا شدن کالبک ندارد. در عوض، وظیفه مدیریت صف کالبکها و تصمیمگیری برای زمان اجرای آنها به خودِ پیادهسازیِ ساختار پرامیس واگذار شده است. به این ترتیب، هم کاربر و هم توسعهدهنده به طور خودکار از تضمینهای ساختاری و معنایی محکمی بهرهمند میشوند؛ از جمله:
کالبکهایی که با متد .then() اضافه میشوند، هیچوقت قبل از به پایان رسیدن دورِ فعلیِ حلقهی رویداد جاوااسکریپت (JavaScript Event Loop) اجرا نخواهند شد.
این کالبکها حتی اگر زمانی اضافه شوند که عملیات ناهمگامِ پرامیس از قبل با موفقیت یا خطا تمام شده باشد، باز هم حتماً اجرا میشوند.
شما میتوانید با چند بار صدا زدن متد .then()، چندین کالبک مختلف به یک پرامیس اضافه کنید. این کالبکها دقیقاً به همان ترتیبی که نوشته و وارد شدهاند، یکی پس از دیگری اجرا میشوند.
برای جلوگیری از غافلگیر شدن برنامهنویس، توابعی که به .then() پاس داده میشوند هیچوقت به صورت همگام صدا زده نمیشوند؛ حتی اگر پرامیس از قبل تایید شده و آماده (Already-resolved) باشد:
Promise.resolve().then(() => console.log(2));
console.log(1);
// خروجی به این ترتیب خواهد بود: 1 و بعد 2
دلیلش این است که این تابع به جای اجرای فوری، داخل یک صف مخصوص به نام صف مایکروتسکها (Microtask Queue) قرار میگیرد. این یعنی تابع بعداً اجرا میشود (دقیقاً زمانی که تابع سازندهاش خارج شده و پشته اجرای جاوااسکریپت کاملاً خالی است)، درست قبل از اینکه کنترل دوباره به ایونت لوپ برگردد؛ یعنی خیلی زود:
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
wait(0).then(() => console.log(4));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
console.log(1);
// خروجی به این ترتیب چاپ میشود: 1, 2, 3, 4
یک تفاوت ظریف اما بسیار مهم در موتور جاوااسکریپت وجود دارد: کالبکهای مربوط به پرامیسها به عنوان مایکروتسک (Microtask) مدیریت میشوند، در حالی که کالبکهای تابع setTimeout() به عنوان تسک (Task Queue / Macrotask) به صف فرستاده میشوند.
بیا با یک کد آزمایشی رفتار این دو را ببینیم:
const promise = new Promise((resolve, reject) => {
console.log("Promise callback");
resolve();
}).then((result) => {
console.log("Promise callback (.then)");
});
setTimeout(() => {
console.log("event-loop cycle: Promise (fulfilled)", promise);
}, 0);
console.log("Promise (pending)", promise);
اگر این کد را اجرا کنیم، خروجی به این صورت در کنسول ظاهر میشود:
Promise callback
Promise (pending) Promise {<pending>}
Promise callback (.then)
event-loop cycle: Promise (fulfilled) Promise {<fulfilled>}
(برای درک عمیقتر جزئیات این تفاوت، میتوانید بخش مستندات Tasks vs. microtasks را مطالعه کنید).
اگر در شرایطی قرار گرفتید که پرامیسها و تسکهای شما (مثل رویدادها یا کالبکها) با ترتیبی غیرقابل پیشبینی اجرا میشوند و کار را خراب میکنند، استفاده از یک مایکروتسک میتواند به شما کمک کند. مخصوصاً در مواقعی که پرامیسها به صورت مشروط (Conditional) ساخته میشوند، با استفاده از مایکروتسک میتوانید وضعیت را بررسی کرده یا تعادل را در اجرای پرامیسها برقرار کنید.
اگر فکر میکنید مایکروتسکها میتوانند به حل این دسته از چالشها در پروژه شما کمک کنند، به راهنمای تخصصی مایکروتسک مراجعه کنید تا یاد بگیرید چطور با استفاده از تابع queueMicrotask()، یک تابع را به صورت دستی به صف مایکروتسکها بفرستید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://developer.mozilla.org/en-US/docs/Web/JavaScript