المستوى: للمحترف
كتبت setTimeout(fn, 0) وفي نفس اللحظة Promise.resolve().then(fn2)؟ الـ fn2 هتشتغل أول. ده مش bug في V8، ده اختلاف Microtask Queue عن Macrotask Queue. ولو ما فهمتش الفرق ده بالظبط، Race conditions في React state batching أو Vue reactivity هتاكلك ساعات في الـ debugging.
المشكلة باختصار
فيه مطورين بيحطوا setTimeout(fn, 0) عشان «يأجلوا» دالة سطر، وبيتفاجئوا إن Promise.resolve().then() بتسبقها بفرق ميلي ثانية على الأقل. السبب إن JavaScript مش بيشغّل الكود بـ FIFO بسيط — فيه قوايين منفصلة بأولويات مختلفة، ومحدش بيحرّك Macrotask لو فيه Microtask واحدة لسه مستنية في الطابور.
الـ Event Loop ببساطة: شيف واحد بيشتغل من طاولتين
للمبتدئ: تخيّل مطعم فيه شيف واحد بس. قدامه طاولتين طلبات. طاولة «VIP» (دي الـ Microtasks) وطاولة عادية (دي الـ Macrotasks). الشيف بيمشي بقاعدة بسيطة: لو على طاولة VIP أي طلب، يخلّصه الأول. وما يلمسش طاولة VIP وعليها ورق، حتى لو الطلب العادي مستنّي من ساعة. أول ما طاولة VIP تفضى، ياخد طلب واحد بس من الطاولة العادية، ثم يرجع يبصّ على VIP تاني قبل ما ياخد التاني.
دلوقتي علميًا: الـ Event Loop خوارزمية معرّفة في HTML Living Standard قسم 8.1.7 (Event loops). كل دورة (tick) بتمشي بالشكل ده بالظبط:
- اختر أقدم Macrotask من Task Queue ونفّذها لحد ما الـ Call Stack يفضى.
- شغّل كل الـ Microtasks اللي في Microtask Queue، واحدة ورا التانية، حتى تفضى تمامًا — حتى لو Microtask جديدة بتضاف أثناء التشغيل.
- في المتصفح: لو في rendering لازم يحصل (style، layout، paint)، نفّذه دلوقتي.
- ارجع لخطوة 1.
الفكرة الجوهرية: Microtask Queue بتتفرّغ بالكامل بين كل Macrotask. مش Microtask واحدة، كلها.
مين بيروح Microtask ومين بيروح Macrotask؟
القاعدة دي حفظتها مرة واحدة، بتفرق معاك في كل مشروع:
- Microtasks:
Promise.then/catch/finally،queueMicrotask()،MutationObserver،process.nextTickفي Node.js (دي أولوية أعلى من Microtasks العادية). - Macrotasks:
setTimeout،setInterval،setImmediate(Node)، I/O callbacks،requestAnimationFrame(دي بتشتغل قبل الـ paint مباشرة، queue منفصلة فعليًا).
المثال اللي بيكسر الفهم الغلط
نفّذ الكود ده في Chrome DevTools أو Node 20+: