Event Loop در JavaScript چیست؟ آموزش کامل همراه با مثال

Event Loop در JavaScript چیست؟ آموزش کامل همراه با مثال

  • سطح : متوسط
  • مطالعه : 25 دقیقه
  • 90

Event Loop در JavaScript چیست؟

اگر تا به حال برایتان سؤال شده است که چرا Promise قبل از setTimeout اجرا می‌شود یا JavaScript چگونه بدون داشتن چند Thread هم‌زمان چندین عملیات را مدیریت می‌کند، پاسخ تمام این سؤال‌ها در مفهومی به نام Event Loop قرار دارد.

در این مقاله به زبان ساده یاد می‌گیرید Event Loop در JavaScript چیست، چگونه کار می‌کند و چه ارتباطی با Call Stack، Execution Context، Web APIs، Callback Queue، Microtask Queue، Promise و async/await دارد.

پیش نیازه ها

PREREQUISITES

اگر هنوز با Promise و async/await آشنا نیستید، پیشنهاد می‌کنیم ابتدا مقاله

آموزش Promise در JavaScript 

آموزش Async Await در JavaScript

را مطالعه کنید؛ زیرا درک آن‌ها برای یادگیری Event Loop ضروری است.

آنچه در این مقاله خواهید آموخت

  • درک مفهوم Event Loop و دلیل اهمیت آن در JavaScript
  • آشنایی با معماری JavaScript Runtime و اجزای اصلی آن
  • شناخت تفاوت Single Thread و Asynchronous JavaScript
  • یادگیری مفهوم Execution Context
  • آشنایی با Call Stack
  • درک تفاوت Heap Memory و Call Stack
  • شناخت Web APIs
  • تفاوت Microtask Queue و Callback Queue
  • ترتیب اجرای Promise و setTimeout()
  • تفاوت Event Loop در Browser و Node.js

مقدمه

فرض کنید داخل یک رستوران فقط یک آشپز وجود دارد. سفارش‌های مشتریان یکی پس از دیگری ثبت می‌شوند و آشپز فقط می‌تواند در هر لحظه روی یک سفارش کار کند. اگر آماده شدن یک غذا زمان زیادی طول بکشد، منطقی نیست آشپز تا پایان پخت آن منتظر بماند و هیچ سفارش دیگری را انجام ندهد.

JavaScript نیز دقیقاً شرایط مشابهی دارد. این زبان به صورت Single Thread اجرا می‌شود؛ یعنی در هر لحظه فقط یک دستور را پردازش می‌کند. اما با این وجود، هنگام کار با درخواست‌های شبکه، تایمرها، رویدادهای کاربر و عملیات Asynchronous، برنامه متوقف نمی‌شود.

این قابلیت به لطف JavaScript Runtime و مکانیزمی به نام Event Loop فراهم شده است.

در این مقاله قدم‌به‌قدم با تمام اجزای این سازوکار آشنا می‌شویم و با مثال‌های واقعی بررسی می‌کنیم که چرا بعضی از دستورات زودتر از بقیه اجرا می‌شوند و چگونه Runtime جاوااسکریپت ترتیب اجرای آن‌ها را مدیریت می‌کند.

JavaScript واقعاً Single Thread است؟

یکی از اولین مفاهیمی که هنگام یادگیری JavaScript با آن روبه‌رو می‌شویم، Single Thread JavaScript است.

Single Thread بودن به این معناست که موتور JavaScript تنها یک Call Stack در اختیار دارد و در هر لحظه فقط می‌تواند یک دستور را اجرا کند.

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

console.log("شروع");

for (let i = 0; i < 1000000000; i++) {}

console.log("پایان");

در مثال بالا تا زمانی که حلقه for به پایان نرسد، دستور دوم اجرا نخواهد شد.

اما سؤال مهم اینجاست؛ اگر JavaScript فقط یک Thread دارد، چگونه درخواست‌های شبکه، تایمرها و رویدادهای کاربر را بدون توقف برنامه مدیریت می‌کند؟

پاسخ این سؤال را در ادامه و با بررسی Execution Context، Call Stack و در نهایت Event Loop خواهیم یافت.

Execution Context چیست؟

قبل از اینکه موتور JavaScript حتی یک خط از کدهای شما را اجرا کند، ابتدا محیطی برای اجرای آن آماده می‌کند. به این محیط Execution Context گفته می‌شود.

به زبان ساده، Execution Context مجموعه‌ای از اطلاعات موردنیاز موتور JavaScript برای اجرای کد است. این اطلاعات شامل متغیرها، توابع، مقدار this و محدوده دسترسی یا Scope هستند.

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

به همین دلیل، هر قطعه کدی که در JavaScript اجرا می‌شود، ابتدا دارای یک Execution Context اختصاصی خواهد بود.

Execution Context در جاوااسکریپت

انواع Execution Context

در JavaScript دو نوع Execution Context بیشترین کاربرد را دارند.

Global Execution Context

هنگامی که فایل JavaScript برای اولین بار اجرا می‌شود، موتور JavaScript یک Global Execution Context ایجاد می‌کند.

تمام متغیرها، توابع و کدهایی که در سطح اصلی فایل قرار دارند، داخل این محیط اجرایی ذخیره می‌شوند و تا پایان اجرای برنامه در دسترس خواهند بود.

const website = "CafeAmoozesh";

function welcome() {
    console.log("Welcome");
}

در مثال بالا، متغیر website و تابع welcome هر دو داخل Global Execution Context قرار می‌گیرند.

Function Execution Context

هر بار که یک تابع فراخوانی می‌شود، موتور JavaScript یک Function Execution Context جدید ایجاد می‌کند.

این محیط شامل پارامترها، متغیرهای محلی، مقدار this و Scope مربوط به همان تابع است.

function greet(name) {
    const message = `سلام ${name}`;

    console.log(message);
}

greet("علی");

در زمان اجرای تابع greet() یک Function Execution Context جدید ساخته می‌شود. بعد از پایان اجرای تابع، این محیط از حافظه حذف خواهد شد.

Execution Context چه ارتباطی با Call Stack دارد؟

یکی از مهم‌ترین نکاتی که بسیاری از برنامه‌نویسان تازه‌کار به آن توجه نمی‌کنند، ارتباط بین Execution Context و Call Stack است.

Call Stack در واقع محل نگهداری Execution Contextها است، نه خود توابع.

هر بار که یک تابع اجرا می‌شود، Execution Context مربوط به آن روی Call Stack قرار می‌گیرد. اگر همان تابع، تابع دیگری را فراخوانی کند، Execution Context جدید روی قبلی قرار می‌گیرد.

به همین دلیل Call Stack از قانون LIFO (Last In, First Out) پیروی می‌کند؛ یعنی آخرین موردی که وارد Stack شده، اولین موردی است که خارج می‌شود.

function one() {
    two();
}

function two() {
    three();
}

function three() {
    console.log("Hello");
}

one();

ترتیب Execution Contextها در Call Stack به صورت زیر خواهد بود.

Execution Context → three()

Execution Context → two()

Execution Context → one()

Global Execution Context

بعد از پایان اجرای تابع three()، ابتدا Execution Context آن از Stack خارج می‌شود. سپس همین اتفاق برای two() و one() رخ می‌دهد تا در نهایت فقط Global Execution Context باقی بماند.

Heap Memory چیست؟

در کنار Execution Context و Call Stack، مفهوم دیگری به نام Heap Memory نیز نقش مهمی در نحوه اجرای برنامه‌های JavaScript دارد. اگرچه Heap مستقیماً بخشی از Event Loop نیست، اما درک آن کمک می‌کند تصویر کامل‌تری از JavaScript Runtime داشته باشید.

زمانی که یک متغیر از نوع مرجع (Reference Type) مانند Object، Array یا Function ایجاد می‌شود، داده‌های آن داخل Heap Memory ذخیره می‌شوند. در مقابل، اطلاعات مربوط به اجرای توابع و Execution Contextها داخل Call Stack قرار می‌گیرند.

به بیان ساده، Stack مسئول مدیریت روند اجرای برنامه است، در حالی که Heap محل نگهداری داده‌هایی است که ممکن است در طول اجرای برنامه بارها مورد استفاده قرار گیرند.

Call Stack Heap Memory
Execution Context Object
Function Call Array
ترتیب اجرای توابع Reference Type
ساختار LIFO حافظه پویا

پس از پایان استفاده از داده‌های موجود در Heap، موتور JavaScript با استفاده از Garbage Collector حافظه اشغال‌شده را آزاد می‌کند. این فرآیند به صورت خودکار انجام می‌شود و توسعه‌دهنده معمولاً نیازی به مدیریت دستی حافظه ندارد.

مقایسه Call Stack و Heap Memory در جاوااسکریپت

Call Stack چیست؟

اکنون که با مفهوم Execution Context آشنا شدید، درک Call Stack بسیار ساده‌تر خواهد بود.

Call Stack ساختاری از نوع Stack است که موتور JavaScript از آن برای مدیریت اجرای توابع استفاده می‌کند.

هر زمان یک تابع فراخوانی شود، Execution Context مربوط به آن وارد Call Stack می‌شود. بعد از پایان اجرای تابع نیز همان Execution Context از Stack خارج خواهد شد.

به همین دلیل JavaScript همیشه می‌داند در هر لحظه کدام تابع در حال اجرا است.

Call Stack چگونه کار می‌کند؟

function first() {
    second();
}

function second() {
    third();
}

function third() {
    console.log("CafeAmoozesh");
}

first();

ترتیب ورود توابع به Call Stack به شکل زیر است.

Global()

↓

first()

↓

second()

↓

third()

پس از اجرای تابع third()، آن تابع از Stack خارج می‌شود. سپس نوبت خروج second() و بعد first() خواهد بود.

Call-Stack-در-جاوااسکریپت

اگر Call Stack بیش از حد بزرگ شود چه اتفاقی می‌افتد؟

اگر توابع بدون توقف یکدیگر را فراخوانی کنند و هیچ شرط خروجی وجود نداشته باشد، Call Stack به مرور پر می‌شود.

در چنین شرایطی موتور JavaScript دیگر فضای کافی برای ایجاد Execution Context جدید نخواهد داشت و خطایی با عنوان Maximum Call Stack Size Exceeded نمایش داده می‌شود.

function test() {
    test();
}

test();

این خطا معمولاً به دلیل استفاده نادرست از Recursion یا ایجاد حلقه‌های بازگشتی بی‌پایان رخ می‌دهد.

Web APIs چیست؟

تا اینجا یاد گرفتیم که JavaScript فقط یک Call Stack دارد و تمام کدها روی همان Stack اجرا می‌شوند. اما سؤال مهم اینجاست که اگر Call Stack مشغول باشد، عملیات‌هایی مانند setTimeout()، fetch() یا رویدادهای کاربر چگونه اجرا می‌شوند؟

پاسخ این سؤال در JavaScript Runtime قرار دارد.

در مرورگر، مجموعه‌ای از قابلیت‌ها در اختیار JavaScript قرار می‌گیرد که به آن‌ها Web APIs گفته می‌شود. این قابلیت‌ها جزئی از زبان JavaScript نیستند؛ بلکه توسط مرورگر پیاده‌سازی شده‌اند.

به همین دلیل توابعی مانند setTimeout()، fetch()، addEventListener() و بسیاری از APIهای دیگر بدون اینکه Thread اصلی JavaScript را مسدود کنند، در پس‌زمینه مدیریت می‌شوند.

نمونه‌ای از Web APIs

  • setTimeout() برای اجرای یک تابع بعد از مدت مشخص
  • setInterval() برای اجرای دوره‌ای یک تابع
  • fetch() برای ارسال درخواست HTTP
  • addEventListener() برای مدیریت رویدادهای کاربر
  • DOM APIs برای کار با عناصر صفحه
  • Geolocation API برای دریافت موقعیت مکانی

تمام این قابلیت‌ها خارج از موتور JavaScript اجرا می‌شوند و پس از پایان عملیات، نتیجه را به Event Loop اطلاع می‌دهند.

Web APIs در JavaScript Runtime

اجرای setTimeout در پشت صحنه

مثال زیر را در نظر بگیرید.

console.log("شروع");

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

console.log("پایان");

خروجی برنامه به شکل زیر خواهد بود.

شروع

پایان

Timeout

ممکن است در نگاه اول این ترتیب عجیب به نظر برسد، اما اگر مراحل اجرا را بررسی کنیم، دلیل آن کاملاً مشخص خواهد شد.

  1. دستور اول وارد Call Stack شده و اجرا می‌شود.
  2. تابع setTimeout() وارد Call Stack می‌شود.
  3. مرورگر Timer را داخل Web APIs ایجاد می‌کند.
  4. Call Stack آزاد می‌شود.
  5. پس از پایان زمان انتظار، Callback آماده اجرا خواهد شد.

نکته مهم این است که Callback مستقیماً وارد Call Stack نمی‌شود، بلکه ابتدا وارد صفی به نام Callback Queue خواهد شد.

Callback Queue چیست؟

Callback Queue صفی است که Callbackهای آماده اجرا در آن منتظر می‌مانند تا Call Stack خالی شود.

وقتی یک عملیات Asynchronous مانند setTimeout() یا یک رویداد کلیک به پایان می‌رسد، Callback آن مستقیماً اجرا نمی‌شود؛ بلکه ابتدا داخل Callback Queue قرار می‌گیرد.

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

یک مثال ساده

console.log("A");

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

console.log("C");

خروجی برنامه به صورت زیر خواهد بود.

A

C

B

با اینکه مقدار Timer برابر صفر است، Callback بلافاصله اجرا نمی‌شود.

علت این رفتار آن است که Callback ابتدا وارد Callback Queue می‌شود و تا زمانی که Call Stack کاملاً خالی نشود، امکان اجرا نخواهد داشت.

Callback Queue در جاوااسکریپت

آیا Callback Queue همان Macrotask Queue است؟

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

عملیات‌هایی مانند setTimeout()، setInterval() و رویدادهای ایجادشده توسط addEventListener() پس از پایان اجرای خود، Callback مربوطه را داخل Macrotask Queue قرار می‌دهند. سپس Event Loop زمانی که Call Stack خالی باشد، این Callbackها را برای اجرا به Call Stack منتقل می‌کند.

به همین دلیل است که Callbackهای معمولی همیشه بعد از Microtaskها اجرا می‌شوند؛ زیرا Event Loop ابتدا Microtask Queue را بررسی می‌کند و تنها پس از خالی شدن آن، به سراغ Macrotask Queue می‌رود.

نوع عملیات صف اجرا
Promise.then() Microtask Queue
queueMicrotask() Microtask Queue
setTimeout() Macrotask Queue
setInterval() Macrotask Queue
addEventListener() Macrotask Queue

Callback Queue چه وظایفی را نگهداری می‌کند؟

مهم‌ترین عملیات‌هایی که وارد Callback Queue می‌شوند عبارت‌اند از:

  • Callback مربوط به setTimeout()
  • Callback مربوط به setInterval()
  • رویدادهای کاربر مانند کلیک و تایپ
  • برخی Callbackهای مربوط به APIهای مرورگر

اما همه عملیات‌های Asynchronous وارد Callback Queue نمی‌شوند.

Promiseها و async/await از صف دیگری به نام Microtask Queue استفاده می‌کنند که اولویت بالاتری نسبت به Callback Queue دارد. در بخش بعدی با این صف آشنا خواهیم شد.

Event Loop چیست؟

اکنون تمام قطعات پازل را در اختیار داریم.

در واقع Event Loop مکانیزمی در JavaScript Runtime است که وظیفه هماهنگ کردن اجرای عملیات Asynchronous JavaScript را بر عهده دارد.

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

به لطف همین سازوکار، JavaScript می‌تواند بدون داشتن چند Thread برای اجرای کد، عملیات‌های زمان‌بر را مدیریت کند و رابط کاربری برنامه پاسخ‌گو باقی بماند.

Event Loop چگونه کار می‌کند؟

روند کلی اجرای برنامه به شکل زیر است.

  1. کدهای همگام وارد Call Stack می‌شوند.
  2. عملیات‌های Asynchronous به Web APIs ارسال می‌شوند.
  3. پس از پایان عملیات، Callbackها وارد صف مناسب می‌شوند.
  4. Event Loop منتظر خالی شدن Call Stack می‌ماند.
  5. وظیفه بعدی را از صف برداشته و وارد Call Stack می‌کند.

Microtask Queue چیست؟

تا اینجا یاد گرفتیم که Callbackهای مربوط به setTimeout() و بسیاری از رویدادهای مرورگر وارد Callback Queue می‌شوند. اما تمام عملیات‌های Asynchronous از این صف استفاده نمی‌کنند.

برخی از عملیات‌ها مانند Promise، queueMicrotask() و ادامه اجرای async/await داخل صف دیگری به نام Microtask Queue قرار می‌گیرند.

تفاوت اصلی این دو صف در اولویت اجرای آن‌ها است. هر زمان که Call Stack خالی شود، Event Loop ابتدا تمام وظایف موجود در Microtask Queue را اجرا می‌کند و سپس به سراغ Callback Queue می‌رود.

همین تفاوت باعث می‌شود Promiseها معمولاً قبل از Callbackهای setTimeout() اجرا شوند.

تفاوت Microtask Queue و Callback Queue در JavaScript

چه عملیات‌هایی وارد Microtask Queue می‌شوند؟

  • Promise.then()
  • Promise.catch()
  • Promise.finally()
  • queueMicrotask()
  • ادامه اجرای async/await

این عملیات‌ها نسبت به Callbackهای معمولی اولویت بیشتری دارند و زودتر اجرا خواهند شد.

تفاوت Callback Queue و Microtask Queue

در نگاه اول ممکن است هر دو صف عملکرد مشابهی داشته باشند، اما ترتیب اجرای آن‌ها کاملاً متفاوت است.

ویژگی Microtask Queue Callback Queue
اولویت اجرا بالاتر پایین‌تر
نمونه‌ها Promise، async/await setTimeout()، setInterval()
زمان اجرا بلافاصله پس از خالی شدن Call Stack پس از اتمام تمام Microtaskها

همین تفاوت کوچک، دلیل بسیاری از خروجی‌هایی است که در نگاه اول عجیب به نظر می‌رسند.

چرا Promise قبل از setTimeout اجرا می‌شود؟

این سؤال یکی از رایج‌ترین سؤالات مصاحبه‌های JavaScript است.

بسیاری تصور می‌کنند دلیل این رفتار، سرعت بیشتر Promise است؛ اما واقعیت چیز دیگری است.

Promise سریع‌تر نیست؛ بلکه اولویت اجرای بالاتری دارد.

بیایید این موضوع را با یک مثال بررسی کنیم.

console.log("شروع");

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

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

console.log("پایان");

قبل از دیدن پاسخ، سعی کنید خروجی را خودتان پیش‌بینی کنید.

خروجی برنامه

شروع

پایان

Promise

Timeout

چرا این اتفاق می‌افتد؟

  1. دستور اول اجرا می‌شود.
  2. setTimeout() وارد Web APIs می‌شود.
  3. Callback مربوط به Promise وارد Microtask Queue می‌شود.
  4. دستور آخر اجرا می‌شود.
  5. Call Stack خالی می‌شود.
  6. Event Loop ابتدا تمام Microtaskها را اجرا می‌کند.
  7. در نهایت Callback مربوط به setTimeout() اجرا می‌شود.

به همین دلیل همیشه Promise قبل از Callback Queue اجرا خواهد شد.

Event Loop هنگام اجرای Promise چگونه تصمیم می‌گیرد؟

در هر چرخه، Event Loop ابتدا بررسی می‌کند که آیا Call Stack خالی است یا خیر.

اگر Call Stack خالی باشد، ابتدا تمام وظایف موجود در Microtask Queue را اجرا می‌کند.

نکته مهم این است که Event Loop فقط یک Microtask را اجرا نمی‌کند، بلکه تا زمانی که Microtask Queue کاملاً خالی نشود، به Callback Queue مراجعه نخواهد کرد.

این رفتار باعث می‌شود زنجیره‌های Promise نیز پشت سر هم اجرا شوند.

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

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

خروجی

A

B

C

در این مثال، هر دو then() قبل از اجرای setTimeout() اجرا می‌شوند، زیرا هر دو داخل Microtask Queue قرار دارند.

چه زمانی استفاده از Microtask Queue خطرناک می‌شود؟

با وجود مزایای Microtask Queue، استفاده نادرست از آن می‌تواند باعث کاهش پاسخ‌گویی برنامه شود.

اگر تعداد زیادی Microtask پشت سر هم ایجاد شوند، Event Loop مجبور است ابتدا تمام آن‌ها را اجرا کند و تا پایان این کار، Callbackهای معمولی و حتی برخی رویدادهای کاربر منتظر خواهند ماند.

به همین دلیل نباید زنجیره‌های بسیار طولانی از Promiseها یا Microtaskها ایجاد کنید، مگر اینکه واقعاً به آن نیاز داشته باشید.

ارتباط Promise در JavaScript با Event Loop

یکی از مهم‌ترین قابلیت‌های Asynchronous JavaScript، استفاده از Promise است. Promise راهی استاندارد برای مدیریت عملیات‌های زمان‌بر مانند درخواست‌های شبکه، خواندن فایل‌ها و بسیاری از عملیات Asynchronous فراهم می‌کند.

نکته مهم این است که Callbackهای مربوط به then()، catch() و finally() مستقیماً وارد Call Stack نمی‌شوند، بلکه داخل Microtask Queue قرار می‌گیرند.

به همین دلیل، زمانی که Call Stack خالی شود، Event Loop ابتدا Callbackهای Promise را اجرا کرده و سپس به سراغ Callback Queue می‌رود.

console.log("شروع");

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

console.log("پایان");

خروجی برنامه

شروع

پایان

CafeAmoozesh

دلیل این ترتیب اجرا آن است که Callback مربوط به then() بعد از خالی شدن Call Stack وارد مرحله اجرا می‌شود.

async/await چگونه با Event Loop کار می‌کند؟

بسیاری از برنامه‌نویسان تصور می‌کنند async/await باعث می‌شود کد به صورت همگام (Synchronous) اجرا شود، اما این تصور درست نیست.

در واقع async/await فقط یک روش خواناتر برای کار با Promiseها است و همچنان از همان سازوکار Event Loop استفاده می‌کند.

زمانی که موتور JavaScript به کلمه await می‌رسد، اجرای تابع متوقف می‌شود، اما Thread اصلی JavaScript آزاد خواهد بود تا سایر کدها را اجرا کند.

بعد از آماده شدن Promise، ادامه اجرای تابع داخل Microtask Queue قرار می‌گیرد.

async function loadData() {

    console.log("شروع");

    await Promise.resolve();

    console.log("پایان");

}

loadData();

console.log("خارج از تابع");

خروجی برنامه

شروع

خارج از تابع

پایان

همان‌طور که مشاهده می‌کنید، اجرای تابع بعد از await متوقف شده و ادامه آن به Microtask Queue منتقل می‌شود.

Event Loop را قدم‌به‌قدم شبیه‌سازی کنیم

اکنون که با تمام مفاهیم اصلی آشنا شده‌اید، بیایید اجرای یک برنامه را مرحله‌به‌مرحله بررسی کنیم.

ابتدا کد زیر را مشاهده کنید و سعی کنید خروجی آن را حدس بزنید.

console.log("1");

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

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

console.log("4");

مرحله اول

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

خروجی:

1

مرحله دوم

تابع setTimeout() وارد Call Stack می‌شود و سپس Timer داخل Web APIs ثبت می‌شود.

بعد از پایان زمان انتظار، Callback آماده ورود به Callback Queue خواهد شد.

مرحله سوم

Promise اجرا شده و Callback مربوط به then() داخل Microtask Queue قرار می‌گیرد.

مرحله چهارم

دستور آخر اجرا می‌شود.

خروجی:

1

4

مرحله پنجم

اکنون Call Stack کاملاً خالی است.

Event Loop ابتدا Microtask Queue را بررسی می‌کند.

بنابراین مقدار زیر چاپ می‌شود.

3

مرحله ششم

بعد از خالی شدن Microtask Queue، Event Loop به Callback Queue مراجعه می‌کند.

Callback مربوط به setTimeout() وارد Call Stack شده و اجرا می‌شود.

2

خروجی نهایی برنامه

1

4

3

2

این مثال یکی از مهم‌ترین نمونه‌هایی است که در دوره‌های آموزشی و مصاحبه‌های JavaScript مورد استفاده قرار می‌گیرد.

شبیه_سازی مرحله_ای Event Loop در JavaScript

جمع‌بندی عملکرد Event Loop

اگر بخواهیم تمام آنچه تاکنون یاد گرفتیم را در چند مرحله خلاصه کنیم، روند اجرای یک برنامه Asynchronous در JavaScript به شکل زیر خواهد بود.

  1. کدهای همگام وارد Call Stack می‌شوند.
  2. عملیات‌های زمان‌بر به Web APIs ارسال می‌شوند.
  3. بعد از پایان عملیات، Callback یا Promise وارد صف مناسب می‌شود.
  4. Event Loop منتظر خالی شدن Call Stack می‌ماند.
  5. ابتدا تمام Microtaskها اجرا می‌شوند.
  6. در نهایت Callbackهای معمولی اجرا خواهند شد.

اگر این شش مرحله را به خاطر داشته باشید، می‌توانید خروجی اکثر برنامه‌های Asynchronous را بدون اجرای کد پیش‌بینی کنید.

خروجی کدهای Event Loop را پیش‌بینی کنید

یکی از بهترین روش‌های یادگیری Event Loop در JavaScript، تحلیل خروجی کدها قبل از اجرای آن‌ها است. اگر بتوانید تنها با مشاهده کد، ترتیب اجرای دستورات را پیش‌بینی کنید، یعنی مفاهیم Call Stack، Microtask Queue و Callback Queue را به خوبی درک کرده‌اید.

قبل از مشاهده پاسخ هر مثال، چند دقیقه زمان بگذارید و خروجی را روی کاغذ یا در ذهن خود پیش‌بینی کنید.

تمرین اول

console.log("A");

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

console.log("C");
نمایش پاسخ
A

C

B

تابع setTimeout() ابتدا وارد Web APIs شده و Callback آن داخل Callback Queue قرار می‌گیرد. پس از خالی شدن Call Stack، Event Loop آن را اجرا می‌کند.

تمرین دوم

console.log(1);

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

console.log(3);
نمایش پاسخ
1

3

2

Callback مربوط به Promise وارد Microtask Queue می‌شود و بعد از خالی شدن Call Stack اجرا خواهد شد.

تمرین سوم

console.log("Start");

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

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

console.log("End");
نمایش پاسخ
Start

End

Promise

Timeout

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

اشتباهات رایج درباره Event Loop (باورهای غلط)

بسیاری از توسعه‌دهندگان تازه‌کار هنگام یادگیری JavaScript برداشت‌های اشتباهی درباره Event Loop دارند. در این بخش، چند مورد از رایج‌ترین این باورهای غلط را بررسی می‌کنیم.

اشتباه اول: JavaScript چند Thread دارد.

خیر. موتور JavaScript فقط یک Thread برای اجرای کدهای JavaScript در اختیار دارد. عملیات‌هایی مانند Timerها یا درخواست‌های شبکه توسط Runtime و Web APIs مدیریت می‌شوند، نه توسط Threadهای JavaScript.


اشتباه دوم: setTimeout با مقدار صفر فوراً اجرا می‌شود.

مقدار صفر به معنی اجرای فوری نیست. این مقدار فقط حداقل زمان انتظار را مشخص می‌کند. Callback همچنان باید وارد Callback Queue شود و منتظر خالی شدن Call Stack بماند.


اشتباه سوم: Promise سریع‌تر از setTimeout است.

Promise سریع‌تر نیست؛ بلکه Callback آن داخل Microtask Queue قرار می‌گیرد که نسبت به Callback Queue اولویت بیشتری دارد.


اشتباه چهارم: Event Loop خودش کدها را اجرا می‌کند.

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


اشتباه پنجم: async/await باعث همگام شدن برنامه می‌شود.

ظاهر کدهای async/await شبیه برنامه‌های همگام است، اما پشت صحنه همچنان از Promise و Event Loop استفاده می‌شود.


اشتباه ششم: Callback Queue همیشه اولین صفی است که بررسی می‌شود.

در واقع Event Loop ابتدا Microtask Queue را بررسی می‌کند و فقط زمانی به Callback Queue مراجعه می‌کند که تمام Microtaskها اجرا شده باشند.

Best Practices هنگام کار با Event Loop

رعایت چند نکته ساده می‌تواند باعث شود برنامه‌های JavaScript عملکرد بهتری داشته باشند و تجربه کاربری روان‌تری ارائه دهند.

  • از ایجاد حلقه‌های طولانی روی Thread اصلی خودداری کنید.
  • برای خوانایی بیشتر، از async/await به جای Callbackهای تو در تو استفاده کنید.
  • از ایجاد زنجیره‌های بسیار طولانی Promise بدون نیاز خودداری کنید.
  • برای عملیات‌های زمان‌بر از APIهای Asynchronous استفاده کنید.
  • همیشه قبل از تحلیل خروجی برنامه، جایگاه هر دستور را در Call Stack، Microtask Queue و Callback Queue مشخص کنید.
  • برای درک بهتر رفتار Event Loop، از ابزارهای Debug مرورگر استفاده کنید.

سؤالات رایج مصاحبه درباره Event Loop

یکی از موضوعاتی که تقریباً در تمام مصاحبه‌های شغلی مربوط به JavaScript مطرح می‌شود، Event Loop است. اگر بتوانید به سؤالات زیر پاسخ دهید، احتمال موفقیت شما در مصاحبه‌های Front-End و Back-End (Node.js) بیشتر خواهد شد.

۱. Event Loop چیست و چه وظیفه‌ای دارد؟

Event Loop مکانیزمی در JavaScript Runtime است که پس از خالی شدن Call Stack، وظایف آماده اجرا را از Microtask Queue یا Callback Queue به Call Stack منتقل می‌کند.

چه APIهای دیگری از Event Loop استفاده می‌کنند؟

بسیاری از توسعه‌دهندگان تصور می‌کنند تنها setTimeout() با Event Loop ارتباط دارد، در حالی که Runtime مرورگر APIهای متعددی را از طریق همین سازوکار مدیریت می‌کند.

برای مثال، setInterval() در بازه‌های زمانی مشخص Callback تولید می‌کند و پس از هر بار اجرا، Callback جدیدی را وارد Macrotask Queue می‌کند. همچنین requestAnimationFrame() برای هماهنگ‌سازی اجرای کد با فرآیند رندر مرورگر استفاده می‌شود و گزینه مناسبی برای ساخت انیمیشن‌های روان است.

علاوه بر این، رویدادهای کاربر مانند کلیک روی دکمه‌ها (addEventListener()) و درخواست‌های HTTP که با fetch() ارسال می‌شوند نیز پس از آماده شدن، از طریق Event Loop مدیریت خواهند شد.

Web API نحوه مدیریت
setTimeout() Macrotask Queue
setInterval() Macrotask Queue
requestAnimationFrame() Render Cycle
addEventListener() Macrotask Queue
fetch() Promise → Microtask Queue

مقایسه Web APIs در JavaScript Runtime

۲. تفاوت Call Stack و Callback Queue چیست؟

Call Stack محل اجرای کدهای JavaScript است، در حالی که Callback Queue محلی برای نگهداری Callbackهای آماده اجرا است. Callbackها تا زمانی که Call Stack خالی نشود اجرا نخواهند شد.

۳. تفاوت Microtask Queue و Callback Queue چیست؟

هر دو صف برای نگهداری وظایف Asynchronous استفاده می‌شوند، اما Microtask Queue اولویت بیشتری دارد و همیشه قبل از Callback Queue پردازش می‌شود.

۴. چرا Promise قبل از setTimeout اجرا می‌شود؟

زیرا Callback مربوط به Promise داخل Microtask Queue قرار می‌گیرد، اما Callback مربوط به setTimeout() وارد Callback Queue می‌شود و اولویت پایین‌تری دارد.

۵. آیا setTimeout با مقدار صفر بلافاصله اجرا می‌شود؟

خیر. مقدار صفر فقط حداقل زمان انتظار را مشخص می‌کند. Callback همچنان باید منتظر خالی شدن Call Stack بماند.

۶. آیا async/await باعث همگام شدن JavaScript می‌شود؟

خیر. async/await فقط سینتکس ساده‌تری برای Promiseها فراهم می‌کند و همچنان از Event Loop استفاده می‌کند.

۷. اگر Call Stack مشغول باشد چه اتفاقی برای Event Loop می‌افتد؟

تا زمانی که Call Stack خالی نشود، Event Loop نمی‌تواند هیچ Callback جدیدی را وارد Stack کند. به همین دلیل اجرای عملیات Asynchronous نیز به تأخیر خواهد افتاد.

۸. Execution Context چه ارتباطی با Call Stack دارد؟

هر تابع هنگام اجرا یک Execution Context ایجاد می‌کند و این Execution Context داخل Call Stack قرار می‌گیرد.

۹. آیا Event Loop جزئی از زبان JavaScript است؟

خیر. Event Loop بخشی از Runtime محیط اجرا (مانند مرورگر یا Node.js) است و جزئی از مشخصات اصلی زبان JavaScript محسوب نمی‌شود.

۱۰. مهم‌ترین دلیل استفاده از Event Loop چیست؟

مدیریت عملیات‌های Asynchronous بدون مسدود کردن Thread اصلی JavaScript و حفظ پاسخ‌گویی برنامه.

چالش کافه آموزش

قبل از مشاهده پاسخ‌ها، سعی کنید به سؤالات زیر پاسخ دهید.

سؤال ۱

کدام گزینه وظیفه مدیریت اجرای Callbackهای آماده را بر عهده دارد؟

  1. Execution Context
  2. Call Stack
  3. Event Loop
  4. Scope
پاسخ

گزینه ۳ صحیح است.


سؤال ۲

Callback مربوط به Promise داخل کدام صف قرار می‌گیرد؟

  1. Callback Queue
  2. Microtask Queue
  3. Call Stack
  4. Web APIs
پاسخ

گزینه ۲ صحیح است.


سؤال ۳

کدام گزینه اولویت بیشتری برای اجرا دارد؟

  1. setTimeout()
  2. addEventListener()
  3. Promise.then()
  4. setInterval()
پاسخ

گزینه ۳ صحیح است.


سؤال ۴

کدام عبارت صحیح است؟

  1. JavaScript چند Thread برای اجرای کد دارد.
  2. Event Loop بخشی از Call Stack است.
  3. JavaScript فقط یک Thread برای اجرای کد دارد.
  4. Promise داخل Callback Queue قرار می‌گیرد.
پاسخ

گزینه ۳ صحیح است.


سؤال ۵

اگر Call Stack خالی نباشد چه اتفاقی می‌افتد؟

  1. Microtaskها فوراً اجرا می‌شوند.
  2. Event Loop منتظر خالی شدن Call Stack می‌ماند.
  3. Callback Queue حذف می‌شود.
  4. Promise اجرا نخواهد شد.
پاسخ

گزینه ۲ صحیح است.

سؤالات متداول

آیا Event Loop فقط در مرورگر وجود دارد؟

خیر. مرورگر و Node.js هر دو از Event Loop استفاده می‌کنند، اما Runtime و نحوه پیاده‌سازی آن‌ها تفاوت‌هایی دارد.

آیا Promise همیشه قبل از setTimeout اجرا می‌شود؟

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

آیا Event Loop باعث چند Thread شدن JavaScript می‌شود؟

خیر. JavaScript همچنان Single Thread است و Event Loop فقط اجرای عملیات Asynchronous را مدیریت می‌کند.

آیا می‌توان بدون دانستن Event Loop برنامه‌نویس JavaScript شد؟

برای پروژه‌های کوچک شاید مشکلی نداشته باشید، اما برای درک Promise، async/await، Fetch API، Node.js و رفع باگ‌های پیچیده، یادگیری Event Loop ضروری است.

مهم‌ترین مفاهیمی که باید قبل از یادگیری Event Loop بدانیم چیست؟

  • Function
  • Scope
  • Execution Context
  • Call Stack
  • Promise

جمع‌بندی

در این مقاله یاد گرفتید که Event Loop چگونه با کمک Call Stack، Web APIs، Microtask Queue و Callback Queue اجرای عملیات‌های Asynchronous را مدیریت می‌کند.

اکنون می‌توانید دلیل اجرای Promise قبل از setTimeout() را توضیح دهید، رفتار async/await را درک کنید و خروجی بسیاری از برنامه‌های JavaScript را بدون اجرا پیش‌بینی کنید.

اگر این مفاهیم را به خوبی یاد بگیرید، درک موضوعاتی مانند Fetch API، Service Worker، WebSocket، Node.js و بسیاری از مباحث پیشرفته JavaScript برای شما بسیار ساده‌تر خواهد شد.

تفاوت Event Loop در مرورگر و Node.js

اگرچه مفهوم Event Loop در مرورگر و Node.js یکسان است، اما نحوه پیاده‌سازی آن‌ها تفاوت‌هایی دارد. در هر دو محیط، هدف اصلی Event Loop مدیریت اجرای عملیات‌های Asynchronous و جلوگیری از مسدود شدن Thread اصلی است، اما Runtime هر کدام از سازوکار متفاوتی استفاده می‌کند.

در مرورگر، Event Loop با استفاده از Web APIs عملیات‌هایی مانند setTimeout()، درخواست‌های HTTP، رویدادهای کاربر و تغییرات DOM را مدیریت می‌کند.

در Node.js خبری از Web APIs مرورگر نیست. در عوض، کتابخانه libuv وظیفه مدیریت عملیات‌های Asynchronous مانند فایل‌ها، شبکه، Timerها و بسیاری از عملیات سطح سیستم را بر عهده دارد.

مقایسه Event Loop در Browser و Node.js

ویژگی مرورگر (Browser) Node.js
Runtime مرورگر Node.js Runtime
مدیریت عملیات Asynchronous Web APIs libuv
مدیریت DOM دارد ندارد
رویدادهای کاربر دارد ندارد
سیستم فایل ندارد دارد

با وجود این تفاوت‌ها، ترتیب اجرای Call Stack، Microtask Queue و Callback Queue در هر دو محیط تقریباً مشابه است. به همین دلیل اگر Event Loop را در مرورگر به خوبی یاد بگیرید، درک آن در Node.js نیز بسیار ساده‌تر خواهد بود.

پرسش و پاسخ

اولین دیدگاه رو شما برای ما ارسال و کنید

کاربر گرامی : برای ارسال دیدگاه خود ابتدا باید در سایت لاگین نمایید.