لو كتبت setTimeout(fn, 0) مع Promise.resolve().then(fn2) في نفس الدالة، fn2 بيشتغل الأول. مش bug — ده تصميم الـ Event Loop بالظبط. المقال ده هيخليك تقرا أي كود async وتعرف ترتيب التنفيذ من غير ما تشغّله.
Event Loop في JavaScript: الترتيب اللي بيتحكم في كل كود async
المشكلة باختصار
فيه فرق جوهري بين نوعين من المهام اللي الـ JS engine بيديرها: Macrotasks (زي setTimeout وsetInterval وأحداث DOM) وMicrotasks (زي Promise.then وqueueMicrotask وMutationObserver). لو معرفتش الفرق، هتلاقي نفسك بتعمل debug لكود بيتصرف عكس ما توقعت، خصوصًا لما تخلط بين await وحدث DOM في نفس الدالة.
إزاي بيشتغل الـ Event Loop فعلاً
JavaScript engine بينفذ تعليمات الكود الحالي (اللي اسمه synchronous call stack) لحد ما يفضى تمامًا. بعد كل فضاء في الـ stack، الـ engine بيعمل حاجتين بترتيب ثابت:
- يفضّي كل الـ Microtasks Queue لحد ما تبقى فاضية.
- يسحب Macrotask واحدة من الـ Macrotasks Queue وينفذها، وبعدها يرجع تاني للخطوة 1.
ده معناه إن الـ Promise callbacks بتاخد أولوية مطلقة على setTimeout، حتى لو الـ delay صفر. الافتراض هنا إننا بنتكلم عن بيئة المتصفح أو Node.js الحديثة (≥ v11) اللي عندها تمييز رسمي بين الـ queues.
مثال عملي: ترتيب التنفيذ المدهش
console.log('1');
setTimeout(() => console.log('2 - macrotask'), 0);
Promise.resolve().then(() => console.log('3 - microtask'));
queueMicrotask(() => console.log('4 - microtask'));
console.log('5');
// الناتج الفعلي:
// 1
// 5
// 3 - microtask
// 4 - microtask
// 2 - macrotask
ركز في اللي بيحصل: التنفيذ السطري (1 و5) بيخلص الأول. بعدها الـ engine بيشوف الـ Microtasks Queue فيها عنصرين (3 و4) فبيفضّيهم بالترتيب. أخيرًا بيسحب الـ Macrotask بتاعة setTimeout وينفذها. ده السلوك القياسي حسب HTML spec و V8 implementation.
سيناريو حقيقي بيكسر production
تخيل عندك React component بيعمل fetch، وبعدها في نفس الـ function بتستدعي setState وبتطلب من setTimeout تعمل redirect بعد 0ms: