JavaScript المتزامن وغير المتزامن: فهم Call Stack وPromises وEvent Loop ببساطة

دقائق القراءة: 7
شرح الفرق بين JavaScript المتزامن وغير المتزامن مع Call Stack وPromises

تُعد JavaScript من أكثر لغات البرمجة استخداماً في تطوير الويب، لكنها في الوقت نفسه من أكثر اللغات التي تثير الالتباس لدى المبتدئين، خاصة عند الحديث عن كونها single-threaded وفي الوقت نفسه قادرة على تنفيذ مهام asynchronous. قد يبدو ذلك متناقضاً للوهلة الأولى، لكن الحقيقة أن فهم هذا السلوك هو مفتاح استيعاب كيفية عمل اللغة داخل المتصفح أو بيئة التشغيل.

في هذا المقال سنشرح بصورة عملية وواضحة الفرق بين التنفيذ المتزامن والتنفيذ غير المتزامن في JavaScript، وكيف تعمل Call Stack، وما دور Browser APIs وEvent Loop وPromises في تنظيم التنفيذ دون تعطيل البرنامج.

ماذا ستتعلم من هذا المقال؟

  • كيف تنفذ JavaScript الأوامر المتزامنة بشكل تسلسلي.
  • كيف تنشأ العمليات غير المتزامنة رغم أن JavaScript تعمل بخيط تنفيذ واحد.
  • كيف يساعدك فهم هذا الموضوع على استيعاب Promises بشكل أعمق.
  • أمثلة عملية توضح ترتيب التنفيذ الحقيقي داخل المحرك.

الدوال في JavaScript كائنات من الدرجة الأولى

من أهم خصائص JavaScript أن الدوال فيها تُعامل بوصفها First-Class Citizens، أي يمكنك:

  • تعريف دالة وتخزينها في متغير.
  • تمرير دالة كوسيط إلى دالة أخرى.
  • إرجاع دالة من داخل دالة أخرى.
  • تعديل التعامل معها مثل أي قيمة أخرى تقريباً.

هذا يمنحك مرونة كبيرة في تنظيم الشيفرة وتقسيم المنطق البرمجي إلى وحدات صغيرة قابلة لإعادة الاستخدام.

تنظيم أسطر الشيفرة البرمجية داخل دوال في JavaScript

لكن تعريف الدالة وحده لا يكفي، إذ يجب استدعاؤها حتى يبدأ المحرك بتنفيذها.

// Define a function
function f1() {
  // Do something
  // Do something again
  // Again
  // So on...
}

// Invoke the function
f1();

بشكل افتراضي، ينفذ المحرك الأسطر داخل الدالة سطرًا بعد سطر. وينطبق الأمر نفسه على استدعاء عدة دوال في الملف نفسه. هذا هو أساس التنفيذ المتزامن في JavaScript.

JavaScript المتزامن: كيف تعمل Call Stack؟

عند تعريف دالة ثم استدعائها، يستخدم محرك JavaScript بنية بيانات تُعرف باسم stack لتتبع الدالة التي يجري تنفيذها حالياً. ويُطلق على هذه البنية اسم Function Execution Stack أو الاسم الأشهر Call Stack.

ما الذي تفعله Call Stack؟

  • عند استدعاء دالة، يضيفها المحرك إلى أعلى المكدس.
  • إذا استدعت هذه الدالة دالة أخرى، تُضاف الدالة الجديدة فوقها مباشرة.
  • عندما تنتهي الدالة الداخلية من التنفيذ، تُزال من المكدس.
  • يعود التنفيذ بعد ذلك إلى الدالة السابقة من النقطة التي توقف عندها.
  • يستمر هذا التسلسل حتى يفرغ المكدس بالكامل.

مخطط يوضح آلية عمل Function Execution Stack أو Call Stack في JavaScript

مثال بسيط على تنفيذ الدوال بالتسلسل

function f1() {
  // some code
}

function f2() {
  // some code
}

function f3() {
  // some code
}

// Invoke the functions one by one
f1();
f2();
f3();

في هذا المثال، تدخل f1() إلى Call Stack، ثم تُنفذ وتخرج. بعد ذلك يحدث الشيء نفسه مع f2() ثم f3(). التنفيذ هنا خطي وواضح بالكامل.

تدفق تنفيذ الدوال بالتسلسل داخل Call Stack في JavaScript

مثال متداخل على استدعاء الدوال

function f1() {
  // Some code
}

function f2() {
  f1();
}

function f3() {
  f2();
}

f3();

في هذا السيناريو، يبدأ التنفيذ بالدالة f3()، ثم تستدعي f2()، والتي بدورها تستدعي f1(). لذلك يصبح ترتيب المكدس كما يلي:

  1. إضافة f3().
  2. إضافة f2() فوقها.
  3. إضافة f1() في الأعلى.
  4. انتهاء f1() ثم خروجها.
  5. انتهاء f2() ثم خروجها.
  6. انتهاء f3() ثم خروجها.

مثال على استدعاء دوال متداخلة داخل Call Stack في JavaScript

الخلاصة هنا أن كل ما يحدث داخل Call Stack يتم بشكل متزامن Sequential. أي أن JavaScript تنهي ما في المكدس أولاً قبل أن تنظر إلى أي شيء آخر.

JavaScript غير المتزامن: كيف يحدث ذلك رغم أنها single-threaded؟

مصطلح asynchronous يعني أن بعض العمليات لا تُنفذ في اللحظة نفسها أو لا تتطلب من المحرك الانتظار حتى تنتهي قبل إكمال بقية الشيفرة. هذا مفيد جداً في الحالات التي تشمل:

  • جلب بيانات من خادم باستخدام fetch().
  • تنفيذ أمر بعد مدة زمنية عبر setTimeout.
  • الاستجابة لأحداث المستخدم مثل click وscroll وmouseover.

لو كانت JavaScript تتوقف بالكامل عند كل عملية تنتظر زمناً أو استجابة خارجية، لأصبحت صفحات الويب بطيئة وغير تفاعلية. لذلك تعتمد اللغة على آلية ذكية تتعاون فيها مع Browser APIs وEvent Loop والطوابير المختلفة.

كيف تتعامل JavaScript مع Browser APIs وWeb APIs؟

العمليات المرتبطة بالمتصفح مثل setTimeout أو معالجات الأحداث تعتمد غالباً على callback functions. وهذه الدوال لا تُنفذ فوراً داخل Call Stack، بل تُؤجل حتى تكتمل العملية المرتبطة بها.

مثال على setTimeout

function printMe() {
  console.log('print me');
}

setTimeout(printMe, 2000);

هنا ستظهر العبارة print me بعد مرور ثانيتين.

لكن ماذا لو أضفنا دالة أخرى بعد setTimeout مباشرة؟

function printMe() {
  console.log('print me');
}

function test() {
  console.log('test');
}

setTimeout(printMe, 2000);
test();

النتيجة المتوقعة هي:

test
print me

السبب أن JavaScript لا تتوقف بانتظار انتهاء المهلة الزمنية، بل تواصل تنفيذ الشيفرة التالية مباشرة.

ما هي Callback Queue أو Task Queue؟

تحتفظ JavaScript بطابور خاص يُعرف باسم Callback Queue أو Task Queue. وهو يعمل وفق مبدأ FIFO أي: أول عنصر يدخل هو أول عنصر يخرج.

عندما تنتهي عملية غير متزامنة مرتبطة بـ Browser API، لا تنتقل دالتها مباشرة إلى Call Stack، بل تُوضع أولاً في هذا الطابور بانتظار الوقت المناسب للتنفيذ.

مخطط يوضح العلاقة بين Call Stack وCallback Queue وBrowser APIs في JavaScript

أين يأتي دور Event Loop؟

Event Loop هي الآلية التي تراقب حالة Call Stack والطوابير. ووظيفتها الأساسية بسيطة:

  • تتحقق مما إذا كان Call Stack فارغاً.
  • إذا كان فارغاً وتوجد دوال داخل Callback Queue، تنقل أول دالة إلى Call Stack.
  • يبدأ تنفيذ تلك الدالة كأي دالة عادية.
  • تستمر العملية على شكل حلقة متكررة.

بكلمات مختصرة: العمليات غير المتزامنة لا تقاطع التنفيذ الحالي، بل تنتظر دورها حتى يفرغ المكدس.

مثال عملي على Event Loop

function f1() {
  console.log('f1');
}

function f2() {
  console.log('f2');
}

function main() {
  console.log('main');
  setTimeout(f1, 0);
  f2();
}

main();

قد يظن البعض أن f1() ستعمل قبل f2() لأن التأخير يساوي 0، لكن النتيجة الفعلية هي:

main
f2
f1

والسبب أن setTimeout حتى مع 0 لا يضع الدالة مباشرة داخل Call Stack، بل يرسلها إلى Callback Queue. ثم يأتي دور Event Loop لاحقاً لنقلها بعد انتهاء كل ما هو موجود في المكدس.

توضيح خطوة بخطوة لكيفية عمل Event Loop مع setTimeout في JavaScript

تسلسل التنفيذ في المثال السابق

  1. تدخل main() إلى Call Stack.
  2. يُنفذ console.log('main').
  3. تتعامل Browser API مع setTimeout.
  4. بعد انتهاء المهلة، تُرسل الدالة f1() إلى Callback Queue.
  5. تُنفذ f2() مباشرة لأنها ضمن المسار المتزامن.
  6. تخرج main() من المكدس.
  7. يرى Event Loop أن المكدس أصبح فارغاً.
  8. ينقل f1() من الطابور إلى Call Stack.
  9. تُنفذ f1() أخيراً.

كيف يتعامل محرك JavaScript مع Promises؟

Promises هي كائنات خاصة في JavaScript تُستخدم لتنظيم العمليات غير المتزامنة بطريقة أوضح وأكثر قابلية للإدارة من الاعتماد الكامل على callbacks.

يمكنك إنشاء Promise باستخدام الباني Promise وتمرير executor function إليه. داخل هذه الدالة، يمكنك استدعاء:

  • resolve عند نجاح العملية.
  • reject عند فشلها.

مثال على Promise

const promise = new Promise((resolve, reject) =>
  resolve('I am a resolved promise')
);

بعد ذلك يمكن التعامل مع النتيجة باستخدام .then() ومع الأخطاء باستخدام .catch().

promise.then(result => console.log(result));

تظهر أهمية Promises بشكل كبير عند استخدام fetch() أو أي عملية تستغرق وقتاً وتُرجع نتيجة لاحقاً.

ما هي Job Queue في JavaScript؟

رغم أن Promises غير متزامنة، فإنها لا تستخدم Callback Queue نفسها الخاصة بـ Browser APIs. بدلاً من ذلك، يعتمد المحرك على طابور آخر يسمى Job Queue.

العناصر الموجودة في:

  • Callback Queue تُسمى macro tasks.
  • Job Queue تُسمى micro tasks.

وهنا تكمن النقطة المهمة: عند فراغ Call Stack، يمنح Event Loop أولوية لعناصر Job Queue قبل عناصر Callback Queue.

مخطط يوضح أولوية Job Queue على Callback Queue في JavaScript

قاعدة مهمة لفهم الأولوية

  • في كل دورة من Event Loop، يتم فحص المهام الجاهزة للتنفيذ.
  • بعد انتهاء المهمة الحالية، تُنفذ جميع عناصر micro tasks الموجودة في Job Queue.
  • بعد ذلك فقط يُنتقل إلى macro tasks في Callback Queue.

لذلك، إذا وُجدت Promise وsetTimeout جاهزتان معاً، فإن Promise تُنفذ أولاً.

مثال يوضح الفرق بين Promise وsetTimeout

function f1() {
  console.log('f1');
}

function f2() {
  console.log('f2');
}

function main() {
  console.log('main');
  setTimeout(f1, 0);

  new Promise((resolve, reject) => resolve('I am a promise'))
    .then(resolve => console.log(resolve));

  f2();
}

main();

الناتج سيكون:

main
f2
I am a promise
f1

رغم أن setTimeout يملك تأخيراً صفرياً، فإن الرسالة القادمة من Promise تظهر أولاً، لأنها موجودة في Job Queue ذات الأولوية الأعلى.

مقارنة عملية بين Job Queue وCallback Queue في JavaScript

اختبار سريع لفهم الترتيب في JavaScript

جرّب توقّع ناتج الشيفرة التالية قبل النظر إلى الإجابة:

function f1() {
  console.log('f1');
}

function f2() {
  console.log('f2');
}

function f3() {
  console.log('f3');
}

function main() {
  console.log('main');
  setTimeout(f1, 50);
  setTimeout(f3, 30);

  new Promise((resolve, reject) =>
    resolve('I am a Promise, right after f1 and f3! Really?')
  ).then(resolve => console.log(resolve));

  new Promise((resolve, reject) =>
    resolve('I am a Promise after Promise!')
  ).then(resolve => console.log(resolve));

  f2();
}

main();

الناتج الصحيح هو:

main
f2
I am a Promise, right after f1 and f3! Really?
I am a Promise after Promise!
f3
f1

لماذا ظهر الناتج بهذا الترتيب؟

  • main وf2 من التنفيذ المتزامن، لذا يظهران أولاً.
  • الـ Promises تُضاف إلى Job Queue وتُنفذ قبل setTimeout.
  • الدالة f3 تسبق f1 لأن مهلة 30ms أقل من 50ms.

ملخص المفاهيم الأساسية

المفهوم الوظيفة
Call Stack تنفيذ الدوال المتزامنة بالترتيب
Browser APIs التعامل مع المهام المؤجلة والأحداث الخارجية
Callback Queue احتواء دوال callbacks الجاهزة للتنفيذ
Job Queue احتواء مهام Promises ذات الأولوية الأعلى
Event Loop مراقبة المكدس والطوابير ونقل المهام للتنفيذ

نقاط مهمة يجب تذكرها

  • JavaScript لغة single-threaded لكنها ليست محدودة بالتنفيذ المتزامن فقط.
  • كل ما يدخل Call Stack يُنفذ بالتسلسل.
  • Browser APIs تساعد على تنفيذ العمليات غير المتزامنة خارج المكدس.
  • نتائج setTimeout والأحداث تنتظر داخل Callback Queue.
  • مهام Promises تنتظر داخل Job Queue.
  • Job Queue لها أولوية أعلى من Callback Queue.

الخلاصة التقنية

الفهم الحقيقي لـ Call Stack وEvent Loop وPromises لا يساعدك فقط على توقع ناتج الشيفرة، بل يجعلك تكتب تطبيقات JavaScript أكثر كفاءة وقابلية للصيانة. من الناحية التقنية، أكثر خطأ شائع يقع فيه المطورون هو افتراض أن setTimeout(fn, 0) يعني التنفيذ الفوري، بينما الواقع أن أولوية التنفيذ تحكمها حالة Call Stack ثم Job Queue ثم Callback Queue. لذلك، إذا أردت إتقان JavaScript الحديثة، فإتقان هذه الآليات الداخلية ليس أمراً ثانوياً، بل أساساً ضرورياً لفهم سلوك اللغة في التطبيقات الفعلية.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *