فهم مشغّل كود الجافاسكربت بعمق: كيف تنظم Call Stack و الـ Queues
- مقدمة
- مفهوم JavaScript Runtime
- مفهوم Call Stack
- مفهوم Microtasks Queue
- مفهوم Macrotasks Queue
- مفهوم Event Loop
مقدمة
بعد أن تعرّفت على مفهوم التزامن في جافاسكربت. و تعلمت التعامل مع الـ Promises و الدالة fetch() و المؤقتات مثل setTimeOut و setInterval قد تبدأ بملاحظة بعض الالتباس في ترتيب التنفيذ و أولوية المهام داخل الكود.
هذا المقال سيساعدك على فهم أعمق لما يجري "تحت الغطاء" أثناء تنفيذ الكود في جافاسكربت، و لماذا تتنفذ بعض الأسطر قبل غيرها، و كيف تم تصميم و تنظيم نظام التزامن في اللغة ليبدو بسيطاً على السطح، لكنه في الواقع دقيق جداً من الداخل.
مفهوم JavaScript Runtime
بيئة التشغيل (Runtime Environment) هي المكان الذي ينفّذ الكود الخاص بك. هذه البيئة تحتوي على المحرك (Engine) الذي يفهم الكود و ينفذّه بالإضافة إلى الـAPIs التي توفّر أدوات للتعامل مع العالم الحقيقي مثل console.log() و fetch و setTimeOut و setInterval و غيرها.
مراحل تنفيذ الكود في المحرّك
المحرك يمرر الكود على 3 أشياء أساسية حتى ينفّذه و هي:
- المحلل (Parser).
- المفسر (Interpreter).
- المترجم (Compiler).
دور المحلل (Parser)
المحلل هو المسوؤل عن تحويل الكود الى نوع داخلي اسمه Abstract Syntax Tree و التي يمكن اختصارها بالكلمة AST.
عند كتابة كود مثل let name = "harmash"; فإنّ محلل الكود سيقوم بقرائته حرفاً حرفاً و من ثم بناء شجرة تشرح بنيته المنطقة كما يلي.
مثال
- VariableDeclaration { // هنا تم الإشارة إلى أنه تم تعريف متغير start: 0 // هنا تم الإشارة إلى رقم فهرس أول حرف في الكود end: 21 // هنا تم الإشارة إلى رقم فهرس آخر حرف في الكود - declarations { // هنا تم ذكر كل المعلومات التي تخص إسم و قيمة المتغير - VariableDeclarator { start: 4 end: 20 // هنا تم ذكر كل المعلومات التي تخص إسم للمتغير - id: Identifier { start: 4 end: 8 name: "name" } // هنا تم ذكر كل المعلومات التي تخص قيمة المتغير - init: Literal { start: 11 end: 20 value: "harmash" raw: "\"harmash\"" } } } kind: let // let هنا تم الإشارة إلى أنه تم تعريف المتغير بواسطة الكلمة المفتاحية }
لترى أمثلة حيّة أكثر لكل كود تكتبه كيف يتعامل معه المحلل زر موقع astexplorer.net
دور المفسّر (Interpreter)
المفسّر هو المسوؤل عن تحويل الشجرة الـ AST إلى Byte Code و التي تكون بمثابة الكود الذي يمكن تنفيذه بشكل مبدئي.
دور المترجم (Compiler)
المترجم هو الذي يراقب الكود أثناء التنفيذ فلو لاحظ و جود كود ثقيل أو متكرر فإنه يحوّله إلى لغة يفهمها المعالج مباشرةً ليصبح التنفيذ أسرع. أي يمكن اعتباره محسّن لتنفيذ الكود. فهو يجري عملية ترجمة حقيقية للغة الآلة.
الكود الذي تكتبه في جافاسكربت سيمر من خلال المراحل التالية تباعاً:
JS Code Parsing AST Bytecode Optimized Machine Code
مفهوم Call Stack
هو جزء من ذاكرة المحرك لديه الأولوية القصوى بالتنفيذ حيث يتم وضع الأكواد المتزامنة فيه ليتم تنفيذها أول بأول.
الاكواد المتزامنة مثل:
- الحلقات
for/while/do while. - الشروط
if/else if/else. - المتغيرات (Variables).
- الدوال (Functions).
- العمليات الحسابية أو أي عملية طباعة مثل
console.log().
مثال توضيحي
في المثال التالي، سيتم تنفيذ الكود خطوة خطوة كما يلي:
- سيتم وضع الدالة a اولا في ال stack
- ثم سيلاحظ المحرك ان هناك دالة اخرى تتنفذ بداخلها وهي الدالة b
- فعند الدخول الى الدالة b سيجد console.log سيتم تنفيذها واخراجها من ال stack
- ثم سيتم اخراج الدالة b من ال stack
- ثم سيتم اخراج الدالة a ويصبح ال stack فارغا
مثال
function a() { b(); } function b() { console.log('hi'); } a();
كل الأوامر التي تم ذكرها في المثال السابق تعتبر Pure Javascript Codes بمعنى أنّ المسوؤل عن تنفيذها هو المحرك و ليس المتصفح نفسه باستثناء الأمر console.log() فهو يندرج تحت Web API أي أنّ بيئة المتصفح (Browser Environment) مسؤولة عن توفيرها.
مفهوم Microtasks Queue
هو أيضاً مكان في الذاكرة و لكنه مختلف عن الـ Stack فهو يعتبر قائمة انتظار للمهام الصغيرة التي يجب أن تتنفذ بعد أن يتم إفراغ الـ Stack مثل الـ Promise و queueMicrotask() و fetch() و async function().
الـ Promise و queueMicrotask() تعتبران Pure Javascript Codes بمعنى أنّ المسوؤل عن تنفيذهما هو المحرك و ليس المتصفح نفسه باستثناء الدالة fetch() فهي يندرج تحت Web API فهي ترسل طلب عبر الشبكة، أي أنّ بيئة المتصفح (Browser Environment) مسؤولة عن توفيرها.
مفهوم Macrotasks Queue
هو قائمة انتظار للمهام الكبيرة أو المؤجلة مثل setTimeOut() و setInterval().
الأولوية بتنفيذ ما بداخل الـ Macrotasks Queue تأتي بعد تنفيذ ما بداخل الـ Microtasks Queue.
الدوال المؤجلة مثل setTimeOut() و setInterval() هي من ضمن الـ Web API أي أنّ المتصفح (Browser Environment) مسؤولة هو من يتولى تنفيذها و ليس المحرك.
في المثال التالي نلاحظ أن الأولولية في التنفيذ تأتي لمن هو بداخل الـ Call Stack ثم الـ Microtasks Queue ثم الـ Macrotasks Queue.
مثال
setTimeout(() => {console.log("1")}, 0); // Macrotasks Queue console.log("2"); // Call Stack function foo () {console.log("3")}; foo(); // Call Stack Promise.resolve().then(() => {console.log("4")}); // Microtasks Queue console.log("5"); // Call Stack
النتيجة
2 3 5 4 1
الأولوية دائماً للـ Call Stack لذلك تم تنفيذ كل ما بداخل الـ Call Stack حيث طبع 2 ثم 3 ثم 5. ثم تأتي الأولوية للـ Microtasks Queue حيث طبع 4. ثم تم تنفيذالـ Macrotasks Queue في النهاية طبع 1.
مفهوم Event Loop
الـ Event Loop هو الجزء المسوؤل عن إدارة الـ Call Stack و قوائم الانتظار سواء Microtasks Queue و Macrotasks Queue. أي هو من ينظم تنفيذ الأكواد حسب الأولولية التي تكلمنا عنها في السابق.
جافاسكربت هي تعتبر Single-threaded بمعنى أن عندها خيط واحد فقط لتنفيذ الكود. لذلك لا بد من وجود مدير ينظم الأدوار حسب قوائم الانتظار في حال وجود أكواد متزامنة و غير متزامنة ليعمل الكود بسلاسة. و هذا هو دور ال Event Loop.
في المثال التالي قمنا بإنشاء زر يعد بشكل تنازلي من 10 الى 1 ثم يطبع كلمة Resend باستخدام setTimeOut().
مثال
<button id="btn" style="pointer-events:none; width:150px; padding:10px; user-select:none; color:gray;">Send</button> <script> let btnElement = document.getElementById("btn"); // Call Stack for (let i=10; i>=0; i--) { // Call Stack setTimeout(() => { // Macrotasks Queue btnElement.textContent = i; }, (10 - i) * 1000); } setTimeout(() => { // Macrotasks Queue btnElement.textContent = "Re-Send"; btnElement.style.pointerEvents = "all"; btnElement.style.color = "black"; }, 10000) </script>
طريقة عمل الكود
- تم إنشاء الزر مع إعطائه تنسيق أوّلي ليبدو غير مفعل (جعلنا لون الخط رمادي و منعنا التفاعل معه من خلال
pointer-events: none; - إستدعينا الزر من خلال المعرّف
idالخاص به. - الحلقة ستعمل 10 مرات و لأنها متزامنة سيتم دائماً تنفيذها أولاً. (أي عند الدخول إلى الحلقة لن يتم تنفيذ
setTimeOut()لأنها Macrotasks Queue و في كل دورة من دورات الحلقة سيتم وضع نسخة من الـsetTimeOut()بعدد ثواني أكبر بثانية في كل مرة. - بعد الانتهاء سيبدأ بالعد و إضافة القيمة
iابتدائاً من 10 إلى الرقم 0 بعد كل ثانية. - الـ
setTimeOut()الأخرى ايضا هي في الـ Macrotasks Queue لذلك ستعمل بعد 10 ثواني. أي ستنتهي تقريباً بالتزامن مع دوال الـsetTimeOut()التي تتنفذ بعد كل ثانية. و ستعرض في النهاية النصResendعلى الزر مع إعادة ضبط التنسيق ليبدو الزر مفعلاً.
