Event Loop در جاوااسکریپت چگونه کار می‌کند؟ (توضیح به زبان ساده)
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 9 دقیقه

Event Loop در جاوااسکریپت چگونه کار می‌کند؟ (توضیح به زبان ساده)

یک قطعه کد ساده جاوااسکریپت را اجرا می‌کنید و انتظار دارید خط‌ها دقیقا از بالا به پایین اجرا شوند. اما ناگهان setTimeout بعد از Promise اجرا می‌شود، یا خروجی‌ها با ترتیبی ظاهر می‌شوند که با ذهن خطی تو هم‌خوانی ندارد. این رفتار تصادفی نیست. پشت این نظم ظاهرا نامنظم، یک مکانیزم مرکزی قرار دارد که همه چیز را هماهنگ می‌کند: Event Loop.

جاوااسکریپت روی یک Thread اجرا می‌شود، اما همزمان می‌تواند کارهای غیرهمزمان مثل درخواست شبکه، تایمرها و رویدادهای کاربر را مدیریت کند. این قابلیت بدون Event Loop ممکن نیست. همین سازوکار است که تعیین می‌کند چه کدی، چه زمانی و با چه اولویتی اجرا شود.

در این مطلب، Event Loop را بدون پیچیدگی‌های غیرضروری بررسی می‌کنیم و می‌بینیم پشت اجرای به‌ظاهر ساده‌ کدهای جاوااسکریپت چه سیستمی در حال کار است.

مدل اجرای جاوااسکریپت

برای فهم Event Loop، اول باید دید جاوااسکریپت چطور کد را اجرا می‌کند. هسته این مدل، یک ساختار ساده اما حیاتی به نام Call Stack است.

Call Stack جایی است که توابع در آن قرار می‌گیرند و به ترتیب اجرا می‌شوند. هر تابعی که فراخوانی می‌شود، وارد Stack می‌شود و وقتی کارش تمام شد، از آن خارج می‌شود. اجرای جاوااسکریپت در حالت عادی کاملا خطی و تک‌به‌تک است، یعنی تا زمانی که یک تابع در حال اجراست، بقیه صبر می‌کنند.

function a() {
  console.log("A");
}

function b() {
  console.log("B");
}

a();
b();

در این مثال، ابتدا a وارد Call Stack می‌شود، اجرا می‌شود و خارج می‌شود، سپس b وارد Stack شده و اجرا می‌شود. ترتیب کاملا قابل پیش‌بینی است.

اما مسئله زمانی شروع می‌شود که کدهای غیرهمزمان وارد بازی می‌شوند، مثل setTimeout یا درخواست‌های شبکه. اینجا دیگر Call Stack به‌تنهایی کافی نیست و پای بخش‌های دیگری از محیط اجرا به میان می‌آید.

Event Loop چیست؟

Event Loop مکانیزمی است که بین Call Stack و صف‌های اجرای کارها (Task Queue) هماهنگی ایجاد می‌کند. وظیفه آن ساده است: بررسی کند آیا Call Stack خالی شده یا نه، و در صورت خالی بودن، کار بعدی را از صف اجرا وارد Stack کند.

به زبان ساده، Event Loop مثل یک ناظر دائمی عمل می‌کند. تا زمانی که Call Stack مشغول است، هیچ کاری وارد آن نمی‌شود. اما وقتی Stack خالی شد، Event Loop تصمیم می‌گیرد کدام کار بعدی باید اجرا شود.

این طراحی باعث می‌شود جاوااسکریپت بتواند بدون داشتن چند Thread واقعی، رفتار غیرهمزمان (Asynchronous) داشته باشد. کارهایی مثل تایمرها، درخواست‌های شبکه یا رویدادهای کاربر ابتدا در بخش‌های دیگر محیط اجرا مدیریت می‌شوند و سپس توسط Event Loop به Call Stack منتقل می‌شوند.

در نتیجه، ترتیب اجرای کدها فقط به ترتیب نوشتن آن‌ها وابسته نیست، بلکه به وضعیت Call Stack و صف‌هایی که Event Loop مدیریت می‌کند هم بستگی دارد.

Web APIها در مرورگر

بخش مهمی از اجرای کدهای غیرهمزمان در جاوااسکریپت، در خود موتور جاوااسکریپت انجام نمی‌شود. این وظیفه بر عهده Web APIها در محیط مرورگر است.

وقتی از توابعی مثل setTimeout ،fetch یا event listenerها استفاده می‌کنید، اجرای مستقیم آن‌ها وارد Call Stack نمی‌شود. مرورگر این کارها را به Web APIها می‌سپارد و خودش مدیریت می‌کند.

برای مثال، در setTimeout، مرورگر شمارش زمان را خارج از Call Stack انجام می‌دهد. بعد از پایان زمان، callback مربوطه به صف اجرا (Task Queue) ارسال می‌شود تا Event Loop در زمان مناسب آن را وارد Call Stack کند.

در مورد fetch نیز درخواست شبکه توسط مرورگر انجام می‌شود. پاسخ پس از آماده شدن، به شکل یک Promise resolve شده به Microtask Queue اضافه می‌شود.

این تفکیک باعث می‌شود Call Stack همیشه آزاد بماند و اجرای کدهای اصلی متوقف نشود، در حالی که عملیات‌های زمان‌بر در پس‌زمینه مدیریت می‌شوند.

Task Queue و Microtask Queue

همه کارهایی که خارج از Call Stack انجام می‌شوند، یک مسیر واحد برای بازگشت ندارند. جاوااسکریپت برای مدیریت ترتیب اجرا، از دو صف اصلی استفاده می‌کند: Task Queue و Microtask Queue.

Task Queue محل قرار گرفتن کارهایی مثل setTimeout ،setInterval و رویدادهای مرورگر است. این کارها بعد از آماده شدن، وارد این صف می‌شوند و منتظر می‌مانند تا Event Loop آن‌ها را وارد Call Stack کند.

در مقابل، Microtask Queue برای کارهای با اولویت بالاتر است. Promiseها و برخی عملیات داخلی جاوااسکریپت در این صف قرار می‌گیرند. تفاوت مهم اینجاست که Microtask Queue قبل از Task Queue بررسی و تخلیه می‌شود.

یعنی اگر هم‌زمان هم یک Promise آماده باشد و هم یک setTimeout، ابتدا Microtask اجرا می‌شود و بعد نوبت به Task می‌رسد. همین تفاوت کوچک، علت بسیاری از رفتارهای غیرمنتظره در ترتیب اجرای کدهاست.

Event Loop بعد از خالی شدن Call Stack ابتدا تمام Microtaskها را اجرا می‌کند و سپس سراغ Task Queue می‌رود. این چرخه دائمی است و اساس اجرای غیرهمزمان در جاوااسکریپت را تشکیل می‌دهد.

Event Loop در عمل (یک سناریوی واقعی)

برای درک دقیق‌تر Event Loop، بهتر است یک مثال را قدم‌به‌قدم بررسی کنیم. کد زیر ترکیبی از اجرای همزمان، Promise و setTimeout است:

console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

Promise.resolve().then(() => {
  console.log("C");
});

console.log("D");

حالا ترتیب اجرا را مرحله‌به‌مرحله بررسی کنیم:

  1. console.log("A") اجرا می‌شود و مستقیما وارد Call Stack است. خروجی: A

  2. setTimeout ثبت می‌شود و callback آن به Web API سپرده می‌شود

  3. Promise.resolve().then(...) وارد Microtask Queue می‌شود

  4. console.log("D") اجرا می‌شود. خروجی: D

تا اینجا خروجی فعلی: A D

حالا Call Stack خالی شده است، Event Loop وارد عمل می‌شود:

  1. ابتدا Microtask Queue اجرا می‌شود. خروجی: C

  2. سپس Task Queue اجرا می‌شود. خروجی: B

نتیجه نهایی:

A
D
C
B

نکته مهم اینجاست که setTimeout(..., 0) به معنای اجرای فوری نیست. این تابع همیشه در Task Queue قرار می‌گیرد و بعد از Microtaskها اجرا می‌شود. همین ترتیب ساده، دلیل بسیاری از رفتارهای غیرمنتظره در جاوااسکریپت است.

کاربردهای واقعی Event Loop (با مثال‌های عملی)

Event Loop فقط یک مفهوم تئوریک برای توضیح ترتیب اجرا نیست، تقریبا تمام رفتارهای غیرهمزمان در جاوااسکریپت «از UI در مرورگر تا API call در بک‌اند Node.js» مستقیما به آن وابسته است. در ادامه چند سناریوی واقعی را بررسی می‌کنیم که نشان می‌دهد Event Loop در عمل چه تصمیم‌هایی می‌گیرد.

۱. جلوگیری از فریز شدن UI در مرورگر

در مرورگر، اگر یک عملیات سنگین مستقیما روی Call Stack اجرا شود، کل صفحه قفل می‌شود. Event Loop این امکان را می‌دهد که کارهای سنگین به بخش‌های قابل مدیریت شکسته شوند.

function heavyTask() {
  let sum = 0;

  for (let i = 0; i < 1e9; i++) {
    sum += i;
  }

  console.log(sum);
}

console.log("Start");
heavyTask();
console.log("End");

در این حالت، تا زمانی که heavyTask تمام نشود:

  • هیچ کلیک یا اسکرولی پردازش نمی‌شود

  • UI کاملا فریز می‌شود

راه‌حل واقعی، شکستن کار و انتقال آن به Event Loop است:

function heavyTaskChunked() {
  let i = 0;
  let sum = 0;

  function step() {
    let limit = i + 1e6;

    for (; i < limit && i < 1e9; i++) {
      sum += i;
    }

    if (i < 1e9) {
      setTimeout(step, 0);
    } else {
      console.log(sum);
    }
  }

  step();
}

console.log("Start");
heavyTaskChunked();
console.log("End");

اینجا setTimeout باعث می‌شود کنترل به Event Loop برگردد و مرورگر بتواند بین تکه‌های اجرا، UI را پردازش کند.

۲. مدیریت درخواست‌های شبکه در اپلیکیشن‌های واقعی

فرض کنید یک اپلیکیشن داریی که باید اطلاعات کاربر را از API بگیرد. اگر این عملیات به صورت همزمان اتفاق می‌افتاد، کل برنامه متوقف می‌شد.

console.log("Request start");

fetch("https://api.example.com/user")
  .then(res => res.json())
  .then(data => {
    console.log("User:", data);
  });

console.log("Request sent");

ترتیب اجرا:

  1. "Request start" اجرا می‌شود

  2. fetch به Web API سپرده می‌شود

  3. "Request sent" اجرا می‌شود

  4. پاسخ شبکه بعدا وارد Microtask Queue می‌شود

  5. Promise handler اجرا می‌شود

نکته مهم: UI یا اجرای برنامه در این مدت متوقف نمی‌شود. Event Loop اجازه می‌دهد برنامه همزمان پاسخ‌گو باقی بماند.

۳. تفاوت رفتار setTimeout و Promise در سیستم‌های واقعی

این تفاوت در سیستم‌های واقعی مثل UI frameworks یا سرور Node.js بسیار مهم است.

setTimeout(() => {
  console.log("timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("promise");
});

console.log("sync");

خروجی:

sync
promise
timeout

در پروژه‌های واقعی این ترتیب باعث می‌شود:

  • تغییرات state در UI (Promise-based) سریع‌تر اعمال شوند

  • عملیات‌های زمان‌بندی‌شده (timeout) در انتهای چرخه اجرا شوند

۴. Node.js و پردازش همزمان درخواست‌ها

در Node.js، Event Loop نقش حیاتی‌تری دارد، چون کل سرور روی همین مدل اجرا می‌شود.

const http = require("http");

const server = http.createServer((req, res) => {
  if (req.url === "/slow") {
    setTimeout(() => {
      res.end("Slow response");
    }, 2000);
  } else {
    res.end("Fast response");
  }
});

server.listen(3000);

در اینجا:

  • هر درخواست وارد Call Stack نمی‌شود و منتظر نمی‌ماند

  • setTimeout اجازه می‌دهد سایر درخواست‌ها همزمان پردازش شوند

  • Event Loop بین requestها سوئیچ می‌کند

اگر Event Loop وجود نداشت:

  • هر درخواست باید تا پایان اجرا منتظر می‌ماند

  • سرور عملاً تک‌درخواست و بلاک می‌شد

۵. UI frameworks (React / Vue / Svelte) و batching

فریمورک‌ها از Event Loop برای بهینه‌سازی رندر استفاده می‌کنند.

مثال ذهنی:

setState(1);
setState(2);
setState(3);

به‌جای سه رندر جدا:

  • Event Loop تغییرات را در یک Microtask جمع می‌کند

  • سپس یک بار UI را به‌روزرسانی می‌کند (Batching)

این رفتار باعث کاهش:

  • رندرهای اضافی

  • مصرف CPU

  • و reflow/repaint می‌شود

۶. جلوگیری از blocking در event handlers

اگر یک event handler سنگین باشد، کل صفحه غیرقابل استفاده می‌شود.

document.addEventListener("click", () => {
  console.log("clicked");

  setTimeout(() => {
    console.log("deferred work");
  }, 0);
});

ایده اینجاست:

  • کار فوری سریع انجام می‌شود

  • کار سنگین به Event Loop واگذار می‌شود

  • UI responsive باقی می‌ماند

خطاهای ذهنی رایج درباره Event Loop

بخش زیادی از سردرگمی در جاوااسکریپت از اینجا شروع می‌شود که Event Loop با مدل‌های ذهنی اشتباه تفسیر می‌شود. نتیجه‌اش کدی است که «کار می‌کند»، اما دلیل رفتار آن روشن نیست.

۱. تصور اینکه جاوااسکریپت چندنخی است

یکی از رایج‌ترین اشتباه‌ها این است که فکر کنیم جاوااسکریپت خودش چند Thread دارد. در حالی که هسته اجرای JS تک‌نخی است.

کارهایی مثل شبکه، تایمر یا I/O واقعا در Threadهای جداگانه اجرا می‌شوند، اما این Threadها متعلق به محیط اجرا (Browser/Node.js) هستند، نه خود جاوااسکریپت.

نتیجه مهم:

  • JS فقط یک Call Stack دارد

  • همزمانی واقعی توسط Event Loop مدیریت می‌شود، نه Threadهای JS

۲. تصور اشتباه درباره setTimeout(0)

خیلی‌ها فکر می‌کنند این کد فورا اجرا می‌شود:

setTimeout(() => {
  console.log("run");
}, 0);

اما واقعیت این است:

  • این callback وارد Task Queue می‌شود

  • فقط وقتی Call Stack و Microtask Queue خالی شوند اجرا می‌شود

پس 0ms به معنای «فوری» نیست، بلکه یعنی «در اولین فرصت ممکن بعد از چرخه فعلی».

۳. نادیده گرفتن Microtask Queue

رفتار Promiseها معمولا باعث خطا در ذهن می‌شود. تصور رایج این است که همه asyncها یکسان هستند.

setTimeout(() => console.log("timeout"), 0);

Promise.resolve().then(() => console.log("promise"));

نتیجه همیشه:

promise
timeout

چون:

  • Microtask Queue همیشه قبل از Task Queue اجرا می‌شود

  • Promiseها اولویت بالاتری دارند

۴. اشتباه گرفتن async بودن با parallel execution

async بودن به معنای «همزمان اجرا شدن» نیست.

console.log("A");

fetch("/api").then(() => {
  console.log("B");
});

console.log("C");

بسیاری فکر می‌کنند B و C همزمان اجرا می‌شوند، اما در واقع:

  • C بلافاصله اجرا می‌شود

  • B بعد از تکمیل درخواست شبکه و ورود به Microtask Queue اجرا می‌شود

async یعنی:

اجرای غیرمسدودکننده، نه اجرای همزمان

۵. تصور Event Loop به‌عنوان یک thread مستقل

برخی Event Loop را یک Thread جدا در نظر می‌گیرند. این برداشت اشتباه است.

Event Loop:

  • یک ساختار کنترلی (control mechanism) است

  • نه Thread

  • نه Process

وظیفه آن فقط:

  • بررسی Call Stack

  • مدیریت Queueها

  • زمان‌بندی اجرای callbackها

۶. فکر کردن به ترتیب اجرا به‌عنوان «غیرقابل پیش‌بینی»

در ظاهر ممکن است ترتیب اجرا پیچیده به نظر برسد، اما کاملا قانون‌مند است:

اولویت کلی:

  1. Call Stack (کد synchronous)

  2. Microtask Queue (Promises)

  3. Task Queue (setTimeout, events)

هر رفتاری خارج از این الگو، معمولاً نتیجه درک ناقص از همین ترتیب است.

جمع‌بندی

Event Loop ستون اصلی اجرای غیرهمزمان در جاوااسکریپت است، سازوکاری که بین Call Stack ،Web APIs و Queueها هماهنگی ایجاد می‌کند و تعیین می‌کند هر قطعه کد چه زمانی اجرا شود.

در این مطلب دیدیم که جاوااسکریپت با وجود تک‌نخی بودن، می‌تواند کارهای غیرهمزمان را مدیریت کند. این قابلیت از ترکیب Call Stack ،Microtask Queue و Task Queue به همراه Event Loop به‌وجود می‌آید. نتیجه این طراحی، اجرای روان برنامه بدون مسدود شدن UI یا پردازش‌های سرور است.

درک درست Event Loop باعث می‌شود رفتارهایی مثل تفاوت Promise و setTimeout یا ترتیب اجرای کدهای async قابل پیش‌بینی شوند. این مفهوم یکی از پایه‌ای‌ترین بخش‌های توسعه جاوااسکریپت است و درک آن، مستقیما روی کیفیت طراحی کد و دیباگ تاثیر می‌گذارد.

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
در انتظار ثبت رای

/@arastoo
ارسطو عباسی
کارشناس تست نرم‌افزار و مستندات

...

دیدگاه و پرسش
برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

ارسطو عباسی

کارشناس تست نرم‌افزار و مستندات