1428 words
7 minutes
Event Loop & Async: ليه JavaScript دايمًا بتطلع النتايج “غلط”؟

بعد ما فهمنا مين شايف مين (Scope)، ومين ماسك إيه في الذاكرة (Closures)، ومين نده مين (this).. جه الوقت نجاوب على أهم سؤال في الـ JS:

إمتى الكود ده هيتنفذ أصلًا؟

موضوع الـ Asynchronous والـ Event Loop هو اللي بيخلينا نشوف “عجائب” في الكونسول، زي إن السطر رقم 10 يتنفذ قبل السطر رقم 5، أو إن console.log تطلعلنا نتايج غير اللي مستنينها.

المقالة دي مش مجرد رسمة فيها دايرة وسهمين. المقالة دي تحليل شامل للي بيحصل جوه الـ Engine، وهنغطي فيها الـ Edge Cases اللي بتوقع Seniors.


1. الـ Call Stack (قائمة المهام المقدسة)#

الـ JavaScript بطبيعتها Single-threaded. يعني عندها “مخ” واحد، وإيد واحدة. مابتخيش تعمل حاجتين في نفس الوقت.

الـ Call Stack هو المكان اللي الـ JS بتسجل فيه هي واقفة فين دلوقتي.

  • ندهت دالة؟ نحطها في الـ Stack (Push).
  • الدالة خلصت؟ نشيلها من الـ Stack (Pop).

تخيل الـ Stack ده كأنه مدير مكتب قدامه ورقة واحدة بس، وممنوع ياخد الورقة اللي تحتها غير لما يخلص اللي في إيده.

Stack Overflow#

لو دالة ندهت نفسها (Recursion) من غير شرط خروج، الـ Stack ده هيتملي لحد ما ينفجر.

function panic() {
  panic();
}
panic(); // Error: Maximum call stack size exceeded 💥

الـ Engine بيقولك: “أنا ورقي خلص والذاكرة بتاعتي اتملت، ارحمني!”


رسم توضيحي للـ Call Stack:#

تخيل إننا بننادي الدوال دي:

function a() { b(); }
function b() { c(); }
function c() { console.log('Hi'); }
a();

شكل الـ Stack وهو بيتملي:

   │        │        │        │        │        │
   │        │        │   c()  │        │        │
   │        │   b()  │   b()  │   b()  │        │
   │   a()  │   a()  │   a()  │   a()  │        │
   └────────┘        └────────┘        └────────┘
     Push a    Push b    Push c     Pop c    Pop...

2. المشكلة: إحنا في 2025 واليوزر خلقُه ضيق#

لو الـ JS بتعمل حاجة واحدة بس في الوقت، تخيل لو عملنا request يجيب داتا من السيرفر، والعملية دي خدت 5 ثواني. لو الـ JS واقفت تستنى الـ request ده.. المتصفح كله هيهنج (Freeze). لا زراير هتشتغل، ولا سكرول هيتحرك. الشاشة هتبقى صنم لمدة 5 ثواني.

عشان كده الـ JS “ذكية”. هي مش بتعمل الحاجات التقيلة دي بنفسها. هي “بترميها” على حد تاني.

Web APIs (المساعدين الخفيين)#

الـ setTimeout، الـ fetch، الـ DOM Events.. دول مش جزء من الـ Javascript Engine أصلاً! دول خدمات بيقدمها المتصفح (Browser) للـ JS.

لما تقول:

setTimeout(() => {
  console.log("Hello");
}, 2000);

اللي بيحصل وراء الكواليس:

  1. الـ JS بتقول للمتصفح: “خد الـ Timer ده عندك، ولما تخلص عد الـ 2000ms، ابقى نبهني.”
  2. الـ JS بتشيل إيدها من الموضوع فوراً، وتكمل تنفيذ السطر اللي بعده.
  3. المتصفح هو اللي بيعد الوقت في الخلفية.

معلومة للـ Seniors: المتصفحات الحديثة عندها حد أدنى للـ DELAY (حوالي 4ms) حتى لو كتبت setTimeout(fn, 0). مفيش حاجة اسمها 0 بالظبط.


3. مين يقف في الطابور؟ (Queues)#

بمجرد ما المتصفح يخلص المهمة (التايمر خلص، أو الداتا وصلت من السيرفر)، مبيقدرش يدخل الكود ينفذه فوراً. لازم يستأذن ويقف في الطابور.

وهنا التريكة.. هما طابورين مش واحد، والتمييز الطبقي بينهم واضح جدًا:

أ) Task Queue (المعروفة شعبيًا باسم Macrotask Queue)#

ده الطابور العادي. بيقف فيه:

  • setTimeout
  • setInterval
  • setImmediate (Node.js)
  • DOM Events (clicks, etc.)

ب) Microtask Queue (الـ VIP) 🎩#

ده طابور البشوات. ليه أولوية قصوى. بيقف فيه:

  • Promise callbacks (.then/catch/finally)
  • queueMicrotask
  • MutationObserver
  • process.nextTick (في Node.js - وده ليه أولوية أعلى كمان من الـ Microtasks!)

4. الـ Event Loop (البودي جارد)#

الـ Event Loop هو عبارة عن Loop لا نهائي، وظيفته بسيطة جدًا:

دورة حياة الـ Event Loop (The Big Picture) 🎡#

┌───────────────────────────┐
│     Call Stack (JS)       │ ◄─── يتنفذ هنا
│   [ console.log('Hi') ]   │
└─────────────┬─────────────┘

          فاضي؟ (Empty)

      ┌───────▼───────┐        ┌──────────────────────┐
      │  Event Loop   │ ◄────  │  Microtask Queue 🎩  │
      └───────┬───────┘        │ (Promises, nextTick) │
              │                └──────────────────────┘
              │                           ▲
              │                           │
              ▼                           │
      ┌──────────────────────┐            │
      │   Task Queue 🐢      │ ───────────┘
      │ (Timer, DOM, I/O)    │
      └──────────────────────┘
  1. هل الـ Call Stack فاضي؟ (المدير خلص اللي في إيده؟)

    • لو لأ: سيبه يشتغل.
    • لو آه: بص على الطابور.
  2. قواعد المرور الصارمة:

    • خلص كل اللي في الـ Microtask Queue (الـ VIP) الأول، لحد ما يفضى تماماً.
    • بعدين خد واحد بس من الـ Task Queue.
    • كرر العملية (ارجع شيك على الـ Microtasks تاني!).

مثال عملي يكسر الدماغ:#

console.log('Script Start');

setTimeout(() => console.log('setTimeout'), 0);

Promise.resolve()
  .then(() => console.log('Promise 1'))
  .then(() => console.log('Promise 2'));

console.log('Script End');

الترتيب:

  1. Script Start (Sync)
  2. Script End (Sync)
  3. Promise 1 (Microtask 1)
  4. Promise 2 (Microtask 2 - إحنا لسه بنفضي الـ queue!)
  5. setTimeout (Macrotask)

لاحظ: Promise 2 اتنفذت قبل setTimeout مع إنها جت بعدها! ده لأن الـ Event Loop مش هيسيب الـ Microtask Queue غير لما ينضفه تماماً.


5. Async / Await: كشف الحقيقة#

الناس فاكرة async/await سحر بيخلي الكود Synchronous. الحقيقة هي Syntax Sugar فوق الـ Promises.

الفخ اللي بيوقع ناس كتير:#

async function getData() {
  console.log('A'); // 1. ده بيتنفذ Sync عادي جداً
  await fetch('/api'); // 2. هنا بيحصل الـ Pause
  console.log('B'); // 3. ده بيروح الـ Microtask Queue
}

console.log('C');
getData();
console.log('D');

الترتيب الصح: C -> A -> D -> B

ليه A قبل D؟ لأن الـ async function بتشتغل synchronous لحد أول await. أول ما تشوف await، الدالة بتخرج من الـ Stack، وباقي الكود بيتحول لـ .then() callback مستني النتيجة.


6. مشاكل حقيقية (Bugs) وكوارث الـ Production 🐛#

1- Race Conditions#

لو عندك 2 buttons بيعملوا fetch، واليوزر داس على الأول وبعدين التاني بسرعة. مش مضمون إن التاني يرجع بعد الأول. الحل: استخدم Flags عشان تتجاهل الـ Requests القديمة، أو مكتبات زي TanStack Query بتعمل cancel out-of-the-box.

2- React & Stale Data#

في React، الـ setState مش بتتنفذ في لحظتها (Batched).

const [count, setCount] = useState(0);

function handleClick() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  // النتيجة: 1 مش 3 �
}

السبب: الـ count هنا هو قيمة “قديمة” (Closure) من الـ render اللي فات. الحل: استخدم Functional Update: setCount(prev => prev + 1).

3- Infinite Microtask Loop (تجميد المتصفح)#

لو عملت دالة بترمي نفسها في الـ Microtask Queue:

function loop() {
  Promise.resolve().then(loop);
}
loop();

النتيجة: المتصفح هيموت. الـ Event Loop مش هيعرف يروح للـ Render ولا للـ User Clicks لأن الـ VIP Queue مبيفضاش!


7. Node.js Event Loop (وحش مختلف) 🟢#

في المتصفح، الـ Loop بسيط نسبياً. في Node.js، الموضوع أعقد بكتير (6 مراحل!). أهم حاجة تعرفها في Node.js:

  • Timers Phase: setTimeout, setInterval
  • Poll Phase: I/O callbacks (files, network)
  • Check Phase: setImmediate
setImmediate(() => console.log('Immediate'));
setTimeout(() => console.log('Timeout'), 0);

في Node.js، الترتيب هنا غير مضمون! ممكن دي الأول وممكن دي الأول، حسب الـ Performance بتاع الـ Process وقت البدء.


8. مواضيع متقدمة للـ Seniors 🚀#

Render Blocking#

المتصفح بيحاول يرسم الشاشة (Repaint) كل 16ms (عشان يجيب 60fps). الـ Event Loop بيدي فرصة للـ Render بين الـ Macrotasks. لو عندك كود تقيل في الـ Microtask Queue، الـ Render هيتأخر والـ UI هتقطع (Jank).

Long Tasks API#

إزاي تعرف الكود اللي بيهنج صفحتك؟

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) { // أكتر من 50ms يعتبر Long Task
      console.warn('مصيبة:', entry);
    }
  }
});
observer.observe({entryTypes: ['longtask']});

9. ما وراء الستار في V8 (Under the Hood) 🔧#

عشان نبقى Seniors بجد، لازم نفهم الـ Engine مبني إزاي من جوه. الـ JavaScript Engine (زي V8) مش شغال لوحده. هو جزء من منظومة أكبر جوه المتصفح.

المعمارية الداخلية (Architecture):#

  1. Heap Memory:

    • مكان تخزين الـ Objects والـ Closures (زي ما اتكلمنا في المقالات اللي فاتت).
    • الـ V8 بيعمل Garbage Collection هنا.
  2. Call Stack:

    • مكان تنفيذ الـ JS Code (Single Threaded).
  3. C++ API Bindings (Web APIs):

    • لما بتنادي setTimeout، الـ V8 بيبعت الأمر ده للـ Browser (مكتوب بـ C++).
    • المتصفح هو اللي عنده الـ Threads المتعددة (Timer Thread, Network Thread).
  4. Callback Queues:

    • دي Data Structures (طوابير) مكتوبة بـ C++ بتجمع الـ Callbacks اللي جاهزة للتنفيذ.

السر: الـ JavaScript بطيئة؟ لأ. الـ JavaScript مش بطيئة بطبيعتها، لكنها مش معمولة للشغل التقيل (CPU-bound). الـ JavaScript ممتازة في الـ Orchestration، مش في الـ Computation الثقيلة. الشغل التقيل بيحصل في C++ (Web APIs) و Concurrent على مستوى الـ Host.


10. أنماط متقدمة للتحكم في الـ concurrency 🔀#

أ) Sequential vs Parallel#

الفرق بين إنك تشغل الحاجات ورا بعض أو مع بعض:

// 🐢 Sequential (بيخلصوا في 3 ثواني)
async function slow() {
  await wait(1000);
  await wait(1000);
  await wait(1000);
}

// 🐇 Parallel (بيخلصوا في ثانية واحدة)
async function fast() {
  const p1 = wait(1000);
  const p2 = wait(1000);
  const p3 = wait(1000);
  await Promise.all([p1, p2, p3]);
}

ب) Timeout لـ Promise (عشان الـ Network متهنجش)#

إزاي تجبر Promise إنه يفشل لو طول عن وقت معين؟

function timeout(promise, ms) {
  const timeoutPromise = new Promise((_, reject) => 
    setTimeout(() => reject(new Error('Too slow! 🐢')), ms)
  );
  return Promise.race([promise, timeoutPromise]);
}

// الاستخدام
try {
  await timeout(fetch('/api'), 5000);
} catch (e) {
  console.log('سيرفر بطيء جداً');
}
// ملحوظة: الـ fetch مش هتقف فعلياً في المتصفح، الـ Promise بس هو اللي هيرفض. للإلغاء الحقيقي استخدم AbortController.

الخلاصة: إزاي تذاكر الموضوع ده صح؟#

  1. افصل بين الـ Sync والـ Async: أي كود عادي هو Sync. إنشاء الـ Promise هو Sync، لكن الـ handlers بتاعته هي اللي Async (Micro). أي setTimeout هو Async (Macro).
  2. ارسمها: استخدم أدوات زي Loupe عشان تشوف الـ Stack والـ Queue بعينك.
  3. احذر من الـ Await: هي بتوقف الدالة، مش بتوقف البرنامج كله.

تحدي أخير (سؤال انترفيو):#

Promise.resolve()
  .then(() => console.log('1'))
  .then(() => console.log('2'));

Promise.resolve()
  .then(() => console.log('3'))
  .then(() => console.log('4'));

الناتج: 1 -> 3 -> 2 -> 4 التفسير: كل .then جديدة بترمي callback جديد في آخر الـ Queue. فـ 1 و 3 هيدخلوا الأول، وبعدين نتايجهم (2 و 4) ييجوا بعدهم.

الـ Event Loop هو قلب الـ JavaScript. لو فهمت دقاته، هتعرف ترقص معاه. 💃