6115 words
31 minutes
إزاي Node.js بيشتغل من جوه؟ (اليوم التاني)

ليه Node.js مش مجرد JavaScript؟#

لو حد سألك “إيه الفرق بين JavaScript في المتصفح وفي Node؟” وقولت “مفيش فرق، هي هي” — يبقى لسه واقف على السطح.

أيوه الـ Syntax واحد. تقدر تكتب const x = 5 و Array.map و async/await في الاتنين. بس اللغة مش هي الحكاية — الحكاية في البيئة (Runtime) اللي بتشغّلها فيها.

// في المتصفح — عندك عالم الـ DOM والواجهات:
document.getElementById("btn").addEventListener("click", () => {
    window.alert("مرحب بيك!");
});

// في Node.js — عندك عالم السيرفر والأنظمة:
const fs = require('fs');
const http = require('http');
fs.readFileSync('/etc/hostname');  // بتقرأ ملفات من نظام التشغيل!

تعال نشوف الفرق الجذري بين البيئتين:

الميزةالمتصفح (Browser)الـ Node.js
الـ DOM (document, window)موجودمش موجود
الوصول لملفات الجهاز (fs)ممنوع أمنياًمتاح بالكامل
فتح سيرفر شبكي (http.createServer)مستحيلده شغله الأساسي
التحكم في العمليات (child_process)ممنوعمتاح
الـ Sandbox (صندوق الأمان)محبوس جواهحر تماماً
الـ APIs المتاحةfetch, localStorage, Canvasfs, net, crypto, stream, cluster

تعال أقربهالك بتشبيه: الجافاسكريبت في المتصفح زي موظف بنك ورا زجاج — بيشوف العميل وبيكلمه، بس إيده ماتوصلهوش. ده الـ Sandbox؛ المتصفح بيحميك من إن أي موقع يفتح ملفاتك أو يقرأ الهارد.

Node شال الزجاجة وقالك: إنت بقيت في المطبخ، إيدك في كل حاجة — الملفات، الشبكة، نظام التشغيل. بس انت المسؤول. الحرية دي خلت Node يبقى سيرفر كامل مش مجرد سكريبتات في المتصفح.


إزاي الكمبيوتر بيفهم JavaScript؟ الـ V8#

تمهيد: ليه الكمبيوتر مش بيفهم الكود بتاعنا؟#

تعال نرجع للأساس. البروسيسور (CPU) اللي في جهازك مايفهمش لا JavaScript ولا Python ولا أي لغة بشرية. هو بيفهم حاجة واحدة بس: Machine Code — تعليمات ثنائية (أصفار وآحاد) بتقول له بالظبط “اجمع الرقمين دول”، “انقل القيمة دي من الـ Register للميموري”، “اقفز لعنوان كذا”. دي لغة الـ Hardware فعلاً.

فطبيعي أي لغة عالية المستوى (زي C++ أو JavaScript) لازم تتحول لـ Machine Code عشان الـ CPU ينفذها. الفرق بين اللغات بيظهر في متى وإزاي التحويل بيحصل:

  • لغات مُترجمة مقدماً (Ahead-of-Time / Compiled) زي C++: الكود بيتحول لـ Machine Code مرة واحدة وقت الـ Compile على جهاز المطور. الناتج ملف تنفيذي (.exe أو binary) جاهز يشتغل على الـ CPU مباشرة. السرعة عالية لأن كل الشغل اتعمل قبل التشغيل.
  • لغات مفسّرة (Interpreted) بالطريقة الكلاسيكية زي JavaScript الأولانية: الكود بيتقرأ سطر سطر وقت التشغيل، ونوع كل متغير (Number، String، Object) بيتحدد ديناميكياً وقت الـ Runtime. مفيش تحويل مسبق لـ Machine Code — فكانت JavaScript تاريخياً أبطأ من C++ بكتير.

السؤال اللي طرحته Google: إزاي نخلي لغة ديناميكية زي JavaScript تشتغل بسرعة قريبة من اللغات المُترجمة؟ الإجابة كانت V8 Engine — محرك كتبته Google لمتصفح Chrome سنة 2008 وقلب موازين أداء الـ Web. الفكرة الأساسية: بدل ما نفسر سطر سطر للابد، نعمل تحويل ذكي أثناء التشغيل (JIT — Just-In-Time Compilation): نبدأ تنفيذ سريع من وصفة وسيطة (Bytecode)، ولما نلاحظ إن جزء معين من الكود بيتكرر كتير، نحوله لـ Machine Code محسّن خصيصاً للـ CPU.

Ryan Dahl سنة 2009 أخد الـ V8 ده وقال: هشغله بره المتصفح وأربطه بنظام التشغيل — ودي كانت ولادة Node.js.

تعال نشوف رحلة الكود من JavaScript لـ Machine Code خطوة بخطوة:

مراحل تنفيذ الكود في V8 Engine

تعال نفكك المراحل بالتفصيل — مش ناسيب معلومة#

  • 1. الـ Parser (المحلل اللغوي):
    أول حاجة الـ V8 بيعملها إنه بيقرأ نص الكود زي ما الأستاذ بيقرأ إنشاء — بيكسره لوحدات صغيرة اسمها Tokens (كلمات: const, x, =, 5, +, 3) وبيتأكد إن الـ Syntax صحيح. لو كتبت const = 5 x هيطلعلك خطأ من هنا. الناتج النهائي من الـ Parser مش نص تاني، لا — ناتج عبارة عن شجرة بنيوية اسمها AST (Abstract Syntax Tree): كل عقدة في الشجرة تمثل تعبير أو أمر (متغير، عملية جمع، استدعاء دالة، إلخ). الـ AST دي هي اللي كل المراحل اللي بعد كده بتشتغل عليها؛ كده الـ V8 مش محتاج يعيد قراءة النص، كله تحويل من بنية لبنية.

  • 2. Ignition (المترجم السريع — الـ Interpreter):
    الـ AST بتتاخد وتتحول لـ Bytecode. الـ Bytecode مش Machine Code لسه؛ هو كود وسيط أقرب للـ CPU من نص JavaScript، بس مش مرتبط بنوع معين من المعالجات. ليه مبنحولش مباشرة لـ Machine Code؟ عشان التحويل المباشر لـ Machine Code بياخد وقت (Compile) — وده كان هيمنع Node من يبدأ بسرعة. الـ Bytecode بيتم توليده بسرعة وبيتنفذ فوراً على مترجم داخل الـ V8 (Ignition). النتيجة: Node يشتغل من أول ثانية من غير انتظار Compile طويل. اسم Ignition (المشعل) جاي من فكرة إنه “يشعل” التنفيذ بسرعة.

  • 3. TurboFan (المحسّن — الـ JIT Compiler):
    وهنا السحر الحقيقي. الـ V8 وهو بيشغل الـ Bytecode بيكون بيراقب إحصائيات التنفيذ. لو لاحظ إن دالة معينة (أو حلقة معينة) بتتنادي آلاف المرات — دي اسمها Hot Function أو Hot Path — بيتدخل TurboFan ويحول الـ Bytecode بتاع الجزء ده لـ Machine Code مُحسّن خصيصاً لمعمارية الـ CPU بتاعتك. يعني الكود بقى بيتنفذ بنفس سرعة C++ تقريباً في الأجزاء الحرجة. دي فكرة الـ JIT (Just-In-Time): التحويل لـ Machine Code بيحصل أثناء التشغيل مش قبلها، وبس للكود اللي ثبت إنه “حار” (بيتكرر كتير).

بس في فخ مهم — الـ Deoptimization:
الـ TurboFan لما يعمل Optimization بيفترض افتراضات عن الكود. مثلاً: “المتغير x هنا دايماً Number”. لو في تنفيذ لاحق المتغير اتغير (مثلاً جت قيمة String)، الافتراض يبقى غلط والـ Machine Code المحسّن مش هيبقى صحيح. الـ V8 هنا بيعمل Deoptimization: بيلغي الكود المحسّن ويرجع ينفذ من الـ Bytecode تاني (اللي صحيح لكل الأنواع). فثبات أنواع المتغيرات في الأجزاء الحرجة بيخلي الـ V8 يظبط أداءك؛ التغيير المتكرر للأنواع (مرة Number مرة String) بيخلي المحرك يرجع ويلف للـ Bytecode ويفقد فايدة الـ Optimization.

مثال يوضح الفكرة:

// كود بيساعد الـ V8 يعمل Optimization — الأنواع ثابتة:
function add(a, b) { return a + b; }
add(5, 3);    // Number + Number
add(10, 20);  // Number + Number — TurboFan يثبت إن الدالة بتتعامل مع Numbers ويحسّن عليها

// كود بيدمر الـ Optimization:
add("hello", "world"); // فجأة Strings! — TurboFan: الافتراض انكسر → Deoptimization، ويرجع للـ Bytecode

يعني من الآخر: الـ V8 زي طاهي شاطر. أول يوم (Ignition) بيطبخ من الوصفة (Bytecode) سطر سطر. لما يحفظ الأوردرات المتكررة (Hot Paths) بيجهز الأكلة جاهزة مقدماً (Machine Code) ويقدمها في ثانية. لو الزبون غيّر الطلب فجأة (Type Change) الطاهي يرجع للوصفة (Deoptimization) عشان يظبط الناتج.


ليه الـ Event Loop؟ المشكلة والحل#

تمهيد: إيه اللي كان بيحصل في السيرفرات التقليدية؟#

تعال نتخيل السيناريو. يوزر دخل على موقع وطلب صفحة. السيرفر محتاج يعمل حاجات: يقرأ من الداتابيز، يفتح ملف، يكلم API تاني. كل الحاجات دي اسمها I/O (Input/Output) — يعني الشغل مش كله على البروسيسور؛ جزء كبير منه “انتظار رد من برة” (الداتابيز ترد، القرص الصلب يقرأ، الشبكة ترجع بيانات).

في المعمار التقليدي (Apache مع PHP، أو سيرفرات Java القديمة)، كل طلب (Request) بياخد Thread مخصوص. الـ Thread ده شغله إنه يمسك الطلب من أوله لآخره. فإيه اللي بيحصل؟ الـ Thread يبعت استعلام للداتابيز مثلاً، وبعدين — يوقف وينتظر. مفيش حاجة تانية يعملها؛ الداتابيز ممكن ترد بعد 50 ملي ثانية أو 200. طول الوقت ده الـ Thread واقف (Blocked) — مش بيستهلك CPU فعلاً، بس شغال من ناحية الـ OS: هو منتظر فمش متاح يعمل طلب تاني. فلو جاك 100 طلب في نفس اللحظة، السيرفر بيفتح 100 Thread؛ كل واحد واقف ينتظر I/O. ده اسمه Thread-Per-Request أو Blocking I/O.

Multi-Threaded

المشكلة بتبدأ لما الأرقام تكبر. كل Thread في نظام التشغيل بياخد حيز من الـ RAM (عادة حوالي 1–2 ميجا للـ Stack والبيانات التابعة ليه). يبقى إيه؟ لو عندك 10,000 اتصال متزامن، يبقى 10,000 Thread — يعني 10 إلى 20 جيجا رام بس عشان الـ Threads اللي واقفين ينتظروا ومش بيعملوا شغل CPU فعلي! دي مشكلة قديمة اسمها C10K Problem (The 10,000 connections problem): إزاي سيرفر واحد يخدم 10,000 اتصال في نفس الوقت من غير ما ينهار من الرام والـ CPU؟

والكارثة مش في الرام بس. الـ CPU عندك عدد محدود من الـ Cores. الـ OS بيوزع الـ Threads على الـ Cores وكل شوية يبدّل مين اللي شغال دلوقتي — دي اسمها Context Switching. كل ما يعمل Switch، المحرك بيحفظ حالة الـ Thread اللي خلّاه ويحمل حالة الـ Thread الجديد (Registers، مؤشرات، إلخ). يعني وقت وطاقة ضايعين في “نقل الانتباه” بين آلاف الـ Threads، مش في تنفيذ الكود الفعلي. النتيجة: السيرفر يقدر يقع أو يبطئ جداً مع عدد كبير من الاتصالات.

إيه C10K بالظبط؟ وإيه اللي Node عملته مختلف؟ وليه “ثريد واحد” ميزة؟#

تعال نربط الخيوط عشان مفيش لبس.

إيه مشكلة C10K؟
الاسم معناه: “مشكلة الـ 10,000 اتصال”. السؤال اللي كانت البنية القديمة مش قادرة تجاوب عليه: إزاي سيرفر واحد يخدم 10,000 عميل متصلين في نفس الوقت من غير ما ينهار؟ المشكلة مش إن السيرفر “ضعيف” — المشكلة إن الطريقة اللي Apache و PHP و Java القديمة بتعمل بيها هي نفسها اللي بتوصّل لـ الانهيار: كل اتصال = Thread واقف ينتظر I/O، فـ 10,000 اتصال = 10,000 Thread = رام هائلة + Context Switching فظيع. يعني C10K هي المشكلة، والـ Thread-Per-Request (الطريقة القديمة) هي السبب اللي خلاها صعبة.

إيه الفرق بين الطريقة القديمة و Node؟

Apache / PHP / Java القديمةNode.js
مين اللي ينتظر الـ I/O؟الـ Thread بتاع الطلب — يوقف (Block) لحد ما الداتابيز ترد أو الملف يفتح.محدش من كودك ينتظر. الـ Thread الرئيسي يودي الطلب لـ الـ OS (أو لـ Worker) ويقول “نبهني لما تخلص” ويكمل يخدم طلبات تانية. الـ نظام التشغيل أو الـ 4 Workers هم اللي واقفين ينتظروا.
عدد الـ Threads مع 10,000 اتصال10,000 Thread (واحد لكل اتصال).1 Thread رئيسي + 4 Workers (أو أقل حسب الشغل). الـ 10,000 اتصال مش 10,000 Thread؛ هم 10,000 “طلب” الـ Thread الرئيسي بيعدّيهم ويديهم للـ OS أو للـ Pool، والانتظار بيحصل في الخلفية.
الرام والـ Context Switchingرام عالية جداً (1–2 MB × 10,000) وتبديل مستمر بين آلاف الـ Threads.رام قليلة (ثريد واحد رئيسي + 4 workers)، وتبديل شبه معدوم — فالسيرفر يتحمل.

إيه اللي Node “عملته” عشان يحل المشكلة؟
مافهمش “كل الطلبات على ثريد واحد” بالمعنى الغلط. الـ ضغط (الانتظار) مش على الثريد الواحد. اللي حصل إننا نقلنا الانتظار من عندنا.
في الطريقة القديمة: الـ Thread هو اللي ينتظر — فكل طلب محتاج Thread ينتظر.
في Node: الـ Thread مش ينتظر. بيقول للـ OS: “انتظر أنت، ونبهني بالـ Callback لما يجه الرد.” فالـ انتظار بيحصل عند الـ OS (epoll / kqueue) أو عند الـ 4 Workers، مش عند الـ Thread الرئيسي. الـ Thread الرئيسي شغله: ياخد طلب → يوديه لمن ينتظر → ياخد طلب تاني أو ينفذ Callback خلص. يعني التنسيق على ثريد واحد، لكن الانتظار مش عليه — فمش “كل الضغط على ثريد واحد”، بالعكس: الضغط (الوقت الضايع في الانتظار) اتوزع على الـ OS والـ Pool.

ليه ثريد واحد ميزة مش عبء؟
لأن الثريد الواحد مش بيعمل 10,000 حاجة في نفس الوقت ولا “يحمل كل الـ 10,000 على ظهره”. هو بيعمل حاجة واحدة في اللحظة (كود JavaScript سطر سطر)، بس مابيقعدش ينتظر. كل ما يخلص شغل سريع (يدي طلب للـ OS أو ينفذ Callback)، يروح للحاجة اللي بعدها. الـ 10,000 اتصال كلهم “مسجّلين” عند الـ OS والـ Pool؛ الثريد الرئيسي بس بيوزع الشغل ويستقبل النتائج. فالميزة: نفس عدد الاتصالات، بس بعدد ثريدات قليل جداً — فرام أقل، Context Switching أقل، والسيرفر يقدر يخدم الـ 10,000 من غير ما يقع. كده C10K اتحلت بالأسلوب (Non-blocking I/O + Event Loop)، مش بزيادة الـ Threads.

تعال نشوف الحل: جرسون واحد ذكي + مطبخ في الخلفية#

الفكرة اللي Node بيبني عليها: مش ضروري كل طلب ياخد Thread واقف ينتظر. ممكن يكون عندك مسار واحد (Thread واحد) بيعمل الشغل اللي فعلاً محتاج CPU (يفهم الطلب، يبعت الأوامر، يوزع النتائج)، وكل الـ “انتظار رد من برة” (I/O) يتفوّض لـ نظام التشغيل أو لـ مجموعة صغيرة من الـ Workers في الخلفية. لما الرد ييجي، النظام يبلغ الـ Thread الرئيسي: “الرد جه، نفّذ الـ Callback اللي انت حاططه”. الـ Thread الرئيسي ده هو اللي بيشغل الـ Event Loop — حلقة بتلف باستمرار: “في حاجة جاهزة؟ نفّذها. في طلب جديد؟ ابدأ شغله واديه لمن ينتظر. في رد من شبكة أو ملف؟ نفّذ الـ Callback.” كده جرسون واحد يخدم آلاف الطلبات لأن مش واقف ينتظر؛ بيودي الأوردر للمطبخ ويلف ياخد أوردر تاني، ولما المطبخ يخلص يرن جرس (Callback) فيروح يودي النتيجة.

Single-ThreadedEventLoop-Workers

التشبيه الكامل: مطعم بجرسون واحد ومطبخ#

تعال نربط كل حاجة بتشبيه يثبت في المخ. تخيل مطعم فيه جرسون واحد (ده الـ Event Loop — المسار الواحد اللي بيدير الطلبات) ومطبخ فيه 4 طباخين (ده الـ libuv Thread Pool — العمال اللي بيعملوا الشغل الثقيل اللي نظام التشغيل أو الـ Node بيحطه في الخلفية).

  • الخطوة 1: الجرسون يروح ترابيزة 1، الزبون طلب “مكرونة”. الجرسون يكتب الأوردر ويديه للمطبخ. المطبخ هينتظر المكرونة تتطبخ (ده الـ I/O — انتظار نتيجة من برة). الجرسون مش بيقعد قدام المطبخ؛ بيلف فوراً.
  • الخطوة 2: يروح ترابيزة 2 — “سلطة”. يديها للمطبخ ويلف تاني.
  • الخطوة 3: ترابيزة 3 — “عصير”. نفس الفكرة.
  • الخطوة 4: المطبخ بيرن جرس: “يا جرسون، مكرونة ترابيزة 1 جاهزة!” الـ “جرس” ده هو الـ Callback — إشارة إن الشغل خلص والنتيجة جاهزة. الجرسون يروح يودي المكرونة لترابيزة 1.
  • الخطوة 5: جرس تاني: “سلطة ترابيزة 2 خلصت!” — يوديها.
  • وهكذا. الجرسون واحد بس بياخد كل الأوردرات وبيوزع كل النتائج؛ المطبخ هو اللي بيعمل الشغل اللي بياخد وقت. الفكرة إن الـ “انتظار” مش على الجرسون — الانتظار في المطبخ. والجرسون طول الوقت شغال: إما بياخد أوردر جديد، وإما بيسلم أكل جاهز. كده جرسون واحد يقدر يخدم آلاف الترابيزات طالما الشغل الثقيل (الطبخ = I/O) منفصل عنه.

شرط واحد مهم: لو الجرسون وقف في مكانه وحسب فاتورة معقدة لمدة 30 ثانية (شغل ثقيل على الجرسون نفسه — ده اسمه CPU-bound)، المطعم كله يتشل — مفيش حد بياخد أوردرات ولا بيوصل أكل. ده اللي هيحصل في Node لو حطيت شغل CPU ثقيل على الـ Thread الرئيسي؛ هنكلم عليه بالتفصيل في قسم الـ CPU-bound.

هندسة Node من جوه — المكونات الأساسية#

تعال نشوف إزاي القطع دي مرتبطة ببعض. الـ كود اللي انت بيكتبه (JavaScript) بيتنفذ على Thread واحد (الـ Main Thread). الـ Thread ده بيشغّل الـ V8 (محرك تنفيذ الجافاسكريبت)، والـ V8 بيستخدم حاجتين أساسيتين: Call Stack و Memory Heap. فوقهم في طبقة اسمها Node.js Bindings تربط عالم الـ JavaScript بعالم الـ C++ ونظام التشغيل. تحتهم كلهم في libuv — المكتبة اللي بتدير الـ Event Loop والـ Thread Pool والـ Async I/O وبتتكلم مع الـ OS. الرسمة دي توضح العلاقة:

هندسة التشغيل في Node.js

تعال نفكك كل مكون عشان مفيش حاجة تفضل غامضة:

  • الـ V8 Engine: المحرك اللي بينفذ الـ JavaScript (اتكلمنا عليه في القسم اللي فات). جواه حاجتين: الـ Call Stack والـ Memory Heap. الـ Call Stack هو اللي البروسيسور بيشتغل عليه سطر سطر — كل استدعاء دالة بيحط فوق الـ Stack، ولما الدالة تخلص بتنزل (LIFO: آخر حاجة دخلت أول حاجة تطلع). يعني في أي لحظة الـ CPU شغال على حاجة واحدة من كودك. الـ Heap هو مكان تخزين الـ Objects والـ متغيرات اللي عايشة في الذاكرة (مش على الـ Stack) — الـ V8 بيديرها عشان الجافاسكريبت.

  • الـ Node.js Bindings: الجافاسكريبت لوحده ميقدرش يفتح ملف أو يفتح اتصال شبكة — دي حاجات نظام التشغيل. الـ Bindings هي الشيفت اللي يربط بين الدوال اللي انت بتكتبها في JS (زي fs.readFile() أو http.createServer()) وبين تنفيذ C++ الحقيقي اللي يكلم الـ OS. فلو انت كتبت fs.readFile('file.txt', callback)، الـ Binding بياخد الطلب ويمرره لـ libuv (أو مباشرة لآليات الـ OS)، ومش بيوقف الـ Main Thread ينتظر — بيحط الـ callback في الطابور ويسمح للـ Event Loop تكمل.

  • الـ libuv: دي مكتبة C مفتوحة المصدر وبتعتبر قلب الـ Node من ناحية I/O. هي اللي بتدير تلات حاجات: (1) الـ Event Loop — الحلقة اللي بتلف وتشوف “في callbacks جاهزة؟ في طلبات I/O خلصت؟” وتنفذ بالترتيب؛ (2) الـ Thread Pool — افتراضياً فيه 4 Worker Threads بيتعاملوا مع الشغل اللي الـ OS مش بيدعمه async كويس (زي قراءة/كتابة ملفات، تشفير، ضغط)؛ (3) الـ Async I/O مع نظام التشغيل — استخدام epoll في Linux و kqueue في macOS و IOCP في Windows عشان الـ Node ينتظر الردود من الشبكة والملفات من غير ما يحرق CPU في حلقة انتظار. من غير libuv الـ Node كان هيبقى مجرد V8 فوق C++ بدون نموذج الـ Event Loop والـ Non-blocking I/O اللي خلقوا السحر.

  • نظام التشغيل (OS Kernel): الـ libuv بتتكلم مع الـ Kernel عشان تعمل الـ I/O الحقيقي. الـ epoll / kqueue / IOCP دي آليات من الـ OS اسمها event notification — يعني البرنامج بيقول للـ OS “انتظر لحد ما يكون في بيانات جاهزة على الـ Socket أو الملف، ونبهني”. فالـ CPU مش شغال في انتظار؛ الـ OS هو اللي بينتظر ويرجع للبرنامج لما يحصل حاجة. ده اللي يخلي الـ Single Thread يخدم آلاف الاتصالات.

من الآخر: الـ Event Loop موجود عشان نتفادى “موظف واقف لكل طلب”. بدل كده عندنا موظف واحد (الـ Main Thread) بياخد الطلبات ويديها لمن ينتظر (الـ OS أو الـ Thread Pool)، ولما النتيجة تيجي بياخد إشارة (Callback) وينفذها. كده نفس الموظف يخدم آلاف الطلبات، والشرط الوحيد إن الشغل الثقيل على الـ CPU يتبعد عن الموظف ده (نتكلم عليه في قسم الـ CPU-bound).


إزاي الـ Event Loop بيلف؟ المراحل (كل Tick)#

تمهيد: إيه الـ Tick وإيه المراحل أصلاً؟#

الـ Event Loop في Node مش “طابور واحد” ياخد منه callbacks بالعشوائي. هو حلقة مرتبة بتمرّ على مراحل ثابتة بالترتيب، وكل مرحلة ليها طابور (Queue) خاص بيها. ليه كده؟ عشان نحدد مين يُنفَّذ قبل مين: المؤقتات (Timers) لها مرحلة، أخطاء الـ OS لها مرحلة، ردود الملفات والشبكة (I/O) لها مرحلة، وهكذا. كده مفيش فوضى ولا سباق بين أنواع الـ callbacks المختلفة.

الـ Tick معناها لفة واحدة كاملة على كل المراحل. يعني الـ Loop يعدي على المرحلة 1، بعدين 2، بعدين 3، … لحد آخر مرحلة، وبعدين يرجع من أول المرحلة 1 تاني. كل لفة دي = Tick واحد. وبين كل مرحلتين (وفضلاً بعد كل callback من الـ Macrotasks)، الـ Loop بيشوف: في Microtasks (زي Promises و process.nextTick)؟ بينفذها كلها الأول، وبعدين يكمل للمرحلة اللي بعدها. دي هنتكلم عليها في القسم اللي بعد كده.

الرسمة دي توضح ترتيب المراحل واتجاه الدوران:

event-loop-phases

تعال نشرح كل مرحلة بالتفصيل عشان تبقى فاهمها من جوه:


  • 1. Timers Phase (مرحلة المؤقتات)
    هنا بيتنفذ الـ callbacks بتاعت setTimeout() و setInterval() اللي وقتها خلص. يعني لو كتبت setTimeout(fn, 100)، الـ fn مش هتتنفذ بعد 100ms بالظبط؛ هتتنفذ أقرب مرة الـ Event Loop يعدي على مرحلة Timers وبيكون وقت الـ 100ms قد خلص. لو في اللحظة دي الـ Loop كان في مرحلة Poll أو Check، الـ Timer هيستنى لحد ما الـ Loop يلف ويرجع لـ Timers. عشان كده بنقول: setTimeout(fn, 100) معناها “على الأقل بعد 100ms” مش “بالظبط بعد 100ms”. المرحلة دي أول حاجة في الـ Tick عشان الـ Timers يكونوا المرجع الأساسي للوقت.

  • 2. Pending Callbacks Phase (مرحلة الـ Callbacks المؤجلة)
    في أحيان كتير نظام التشغيل بيؤجل تنفيذ بعض الـ callbacks لظروف معينة (زي أخطاء شبكة أو I/O). المرحلة دي مخصصة لـ callbacks مؤجلة من الدورة اللي فاتت — غالباً أخطاء من الـ OS زي أخطاء TCP (مثلاً ECONNREFUSED لما تحاول تتصل بسيرفر مغلق، أو أخطاء DNS). Node بينفذهم هنا عشان يضمن إن أخطاء النظام متتسابش وتمشي من غير معالجة.

  • 3. Idle / Prepare Phase (مرحلة التحضير)
    مرحلة داخلية بيستخدمها Node نفسه لتحضيراته (إحصائيات، تحضير للـ Poll اللي جاي، إلخ). مش بتتعامل معاها من الكود بتاعك؛ معرفتها كفاية إنها جزء من ترتيب الـ Loop.

  • 4. Poll Phase (مرحلة الـ Polling — ودي القلب النابض)
    هنا الـ Event Loop بيستقبل نتائج الـ I/O — يعني الردود اللي جاية من قراءة ملف، من الداتابيز، من الشبكة (مثلاً HTTP response). كل الـ callbacks اللي انت حاططها لـ fs.readFile أو لـ استقبال بيانات من السوكيت بتتجمع هنا في طابور الـ Poll.

    • لو الطابور فيه callbacks جاهزة: الـ Loop بينفذهم واحد واحد لحد ما الطابور يخلص أو يوصل لحد معين.
    • لو الطابور فاضي: الـ Loop بيقول: “مفيش I/O جاهز دلوقتي، يبقى أنتظر شوية.” هنا في خيارين: لو في setImmediate مسجل، الـ Loop يعدي فوراً لمرحلة الـ Check عشان ينفذهم؛ لو مفيش، الـ Loop يقعد في Poll ينتظر لحد ما يحصل حاجة — إما callback I/O يوصّل، وإما timer يخلص، وبعدين يكمل.

    الخدعة إن الانتظار ده مش بيحرق الـ CPU. Node بيستخدم آليات نظام التشغيل (epoll في Linux، kqueue في macOS، IOCP في Windows) اللي بتقول للـ OS: “انتظر أنت لحد ما يكون في بيانات جاهزة على السوكيت أو الملف، ونبهني.” فالـ CPU مش شغال في حلقة while(true)؛ الـ OS هو اللي بينتظر ويرجع لـ Node لما يحصل حاجة. كده مرحلة Poll هي اللي تربط بين “استنى رد من برة” و”نفّذ الـ callback لما الرد يجه”.

  • 5. Check Phase (مرحلة الفحص)
    هنا بيتنفذ الـ callbacks بتاعت setImmediate(). معناها بالحرف: “نفّذني بعد ما تخلص مرحلة الـ Poll مباشرة.” فلو عايز حاجة تتنفذ بعد كل الـ I/O اللي جاهز دلوقتي، setImmediate هي المناسبة. وده اللي يفرقها عن setTimeout(fn, 0): الـ setTimeout بيدخل في Timers (أول مرحلة في الـ Tick)، والـ setImmediate بيدخل في Check (بعد Poll). فترتيبهم في الـ Tick مختلف، وده بيظهر خصوصاً لما تكون جوه I/O callback (هنشوفه في أمثلة الـ Microtasks).

  • 6. Close Callbacks Phase (مرحلة callbacks الإغلاق)
    هنا بتتنفذ الـ events الخاصة بالإغلاق — مثلاً socket.on('close') أو server.on('close'). يعني لو اتصال اتقفل أو السيرفر اتقفل، الـ callbacks اللي انت ربطتها بالإغلاق بتتنفذ في المرحلة دي. بعد ما الـ Loop يخلصها، بيكمل لفة جديدة من أول Timers (Tick جديد).


من الآخر: الـ Event Loop بيلف على 6 مراحل بالترتيب في كل Tick. مرحلة Poll هي اللي بتستقبل ردود الـ I/O وبتنتظر لو مفيش حاجة جاهزة (بدون ما تحرق CPU بفضل الـ OS). Timers و Check يحددوا متى تنفذ مؤقتاتك و الـ setImmediate؛ والترتيب بينهم مهم. وبين كل مرحلتين الـ Loop بينفذ الـ Microtasks (زي Promises و nextTick) — دي موضوع القسم اللي جاي.


الـ Microtasks والـ Macrotasks — مين الأول؟#

تعال أقربهالك: الدكتور (Event Loop) بيكشف بالدور (Macrotasks). فجأة مريض سكتة قلبية — هل هيقوله “انتظر دورك”؟ لأ. في طوارئ بتتنفذ فوراً. الـ Microtasks هي الطوارئ دي.

التصنيف:#

النوعأمثلةالأولوية
Super VIP Microtaskprocess.nextTick()تتنفذ فوراً — قبل أي Microtask تانية
VIP MicrotaskPromise.then(), queueMicrotask()تتنفذ بعد nextTick وقبل أي Macrotask
MacrotasksetTimeout, setInterval, setImmediate, I/O callbacksتتنفذ في المراحل العادية للـ Event Loop

القاعدة: بين كل مرحلتين (وبعد كل Macrotask)، الـ Event Loop بينفذ كل الـ Microtasks الأول — nextTick ثم Promises — وبعدين يكمل.

المثال الكلاسيكي — حاول توقع الترتيب قبل ما تقرأ الإجابة:#

console.log("1️⃣  Sync — أول سطر كود عادي");

setTimeout(() => {
    console.log("6️⃣  Macrotask — setTimeout (Timers Phase)");
}, 0);

setImmediate(() => {
    console.log("7️⃣  Macrotask — setImmediate (Check Phase)");
});

Promise.resolve().then(() => {
    console.log("4️⃣  Microtask — Promise.then (VIP)");
}).then(() => {
    console.log("5️⃣  Microtask — Promise.then (VIP) التاني");
});

process.nextTick(() => {
    console.log("3️⃣  Microtask — nextTick (Super VIP!)");
});

console.log("2️⃣  Sync — آخر سطر كود عادي");

الناتج:

1️⃣  Sync — أول سطر كود عادي
2️⃣  Sync — آخر سطر كود عادي
3️⃣  Microtask — nextTick (Super VIP!)
4️⃣  Microtask — Promise.then (VIP)
5️⃣  Microtask — Promise.then (VIP) التاني
6️⃣  Macrotask — setTimeout (Timers Phase)
7️⃣  Macrotask — setImmediate (Check Phase)

ليه الترتيب ده بالظبط؟

  • 1 و 2: الكود المتزامن (Synchronous) بيتنفذ أولاً عشان هو على الـ Call Stack مباشرة.
  • 3: process.nextTick Super VIP — بمجرد ما الـ Call Stack يفضى، بيتنفذ قبل أي حاجة تانية.
  • 4 و 5: الـ Promises كـ Microtask VIP بتتنفذ بعد الـ nextTick وقبل أي Macrotask.
  • 6: setTimeout(fn, 0) بتروح لـ Timers Phase — أول Macrotask Phase.
  • 7: setImmediate بتروح لـ Check Phase — بعد الـ Timers.

تعال بقي أصدمك — المثال اللي بيقع فيه الكل:#

const fs = require('fs');

// خارج أي I/O callback — الترتيب بين setTimeout و setImmediate غير مضمون!
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
// ممكن setTimeout الأول أو setImmediate الأول — يعتمد على سرعة الـ Event Loop!

// لكن جوة I/O callback — setImmediate دايماً الأول:
fs.readFile(__filename, () => {
    setTimeout(() => console.log("setTimeout inside I/O"), 0);
    setImmediate(() => console.log("setImmediate inside I/O")); // دي دايماً الأول!
});

ليه؟ لأن جوة I/O callback إحنا في مرحلة Poll. أقرب مرحلة بعدها Check (فيها setImmediate)، بعدين الدورة الجاية تبدأ بـ Timers (فيها setTimeout). فبالتالي setImmediate دايماً الأول جوة I/O.


ليه Single-Threaded يقدر يخدم الملايين؟#

تعال نصحح مفهوم: Node مش Single-Threaded في كل حاجة#

الكود اللي انت بتكتبه (JavaScript) بيشتغل على Thread واحد (Main Thread). لكن ورا الكواليس libuv عندها Thread Pool (4 threads افتراضياً) للشغل اللي الـ OS مش بيدعمه async كويس:

نوع العمليةبتتعامل إزاي؟
Network I/O (HTTP, TCP, DNS lookups)نظام التشغيل مباشرة (epoll/kqueue) — بدون Thread Pool
File System (قراءة/كتابة ملفات)Thread Pool (لأن OS مش بيدعم async fs كويس)
DNS (dns.lookup تحديداً)Thread Pool
Crypto (تشفير/هاشينج)Thread Pool
Zlib (ضغط/فك ضغط)Thread Pool

يعني من الآخر: fs.readFile() — مش الـ Main Thread اللي بيقرأ. الطلب بيروح لـ Worker من الـ Pool، والـ Main Thread يكمل. لما الـ Worker يخلص بيرمي الـ Callback والـ Event Loop ينفذها.

لو عاوز تغير حجم الـ Thread Pool:

// بزيادة الرقم ده، بتزود عدد العمليات المتوازية للـ I/O:
process.env.UV_THREADPOOL_SIZE = 8; // الافتراضي 4، الحد الأقصى 1024

// مثال عملي — لو عندك سيرفر بيعمل Crypto كتير:
// UV_THREADPOOL_SIZE=16 node server.js

ليه الـ Single Thread كافي أصلاً؟ تعال نحسبها سوا. تخيل سيرفر بيخدم 10,000 طلب في الثانية، كل طلب بيروح الداتابيز وينتظر 50ms. في السيرفر التقليدي هتحتاج 10,000 Thread كلهم واقفين ينتظروا.

بس في Node: الـ Main Thread بياخد الطلب في ~0.1ms (يقرأ الـ Request، يعمل Validation، يبعت Query). الباقي (الـ 50ms بتوع الانتظار) مش على الـ Main Thread — نظام التشغيل أو الـ Thread Pool بيتكفلوا بيه. يعني الـ Main Thread الفعلي بيشتغل 0.1ms وبعدين بيروح للطلب اللي بعده على طول. في ثانية واحدة (1000ms)، الـ Main Thread يقدر يعالج 1000 / 0.1 = 10,000 طلب بالراحة!

من الآخر ده سر Node: مش أسرع من Java في التنفيذ الخالص — أذكى في إدارة وقت الانتظار.


عدو Node: الـ CPU-Bound على الـ Main Thread#

تعال نشوف إيه اللي بيحصل لما الـ Main Thread يتشل:

// الكود الكارثي — حساب الأعداد الأولية بطريقة بدائية:
app.get('/heavy', (req, res) => {
    // العملية دي بتاخد 5 ثواني على الـ CPU!
    const result = calculatePrimesUpTo(10_000_000);
    res.json({ primes: result.length });
});

app.get('/fast', (req, res) => {
    // الـ API ده بسيط ومفروض يرد في 1ms
    res.json({ status: "ok" });
});

function calculatePrimesUpTo(n) {
    const primes = [];
    for (let i = 2; i <= n; i++) {
        let isPrime = true;
        for (let j = 2; j <= Math.sqrt(i); j++) {
            if (i % j === 0) { isPrime = false; break; }
        }
        if (isPrime) primes.push(i);
    }
    return primes;
}

الكارثة إيه؟ يوزر واحد بعت /heavy — الـ Main Thread يتشل 5 ثواني. في الـ 5 ثواني دول: مفيش أي Request تاني يترد عليه (حتى /fast)، ولا أي Callbacks (DB، ملفات) تتنفذ، والـ Event Loop واقف. كل اليوزرز يحسوا الموقع وقع.

يعني الجرسون وقف في نص المطعم ماسك آلة حاسبة — مفيش أوردرات، مفيش توصيل، المطبخ بيرن ومحدش بيرد.

ساحة الاختبار — أسئلة الإنترفيو و War Room#

أ) أسئلة الانترفيو القاتلة:#

  • 1. لغز setTimeout(fn, 0): لو كتبت setTimeout(() => console.log("hi"), 0)، هل الـ callback هتتنفذ بعد 0ms بالظبط؟ وليه لأ؟

إجابة : لأ. setTimeout(fn, 0) معناها “حط الـ callback في طابور Timers وهتتنفذ أقرب ما يكون لما الـ Loop يوصل للمرحلة دي”. الـ 0 = “أقل مهلة ممكنة” مش “فوراً”. Node بيحط حد أدنى (~1ms)، والمتصفحات ~4ms بعد أول 5 تكرارات. وكمان لو Call Stack مشغول أو Microtasks كتير هتتأخر. من الآخر setTimeout أداة تأجيل مش توقيت دقيق.

  • 2. لغز process.nextTick الـ Recursive: لو nextTick جوة نفسها لا نهائي — إيه اللي يحصل؟ وإيه الفرق مع setImmediate recursive؟

إجابة : nextTick recursive = I/O Starvation. ليه؟ nextTick بتتنفذ بين كل مرحلتين؛ لو كل تنفيذة بتضيف واحدة جديدة، الـ Loop مش هيعدي لأي مرحلة تانية — لا setTimeout ولا I/O. السيرفر حي بس ميت. setImmediate recursive كل واحدة بتتحط في الدورة الجاية فالـ Loop بيكمل. في الـ Production نتجنب nextTick recursive ونستخدم setImmediate.

  • 3. السيرفر البطيء والداتابيز السريعة: API بسيط يجيب من الداتابيز، الداتا بترد في 2ms والـ API بياخد 500ms. الـ CPU والـ RAM كويسين. فين المشكلة؟

إجابة : غالباً CPU-bound مخفي على الـ Main Thread: JSON.stringify لأوبجكت ضخم، أو logging synchronous، أو validation ثقيلة، أو RegExp معقد. الحل: Flame Graph (clinic.js أو 0x أو --prof) عشان تلاقي الدالة اللي بتاكل الوقت.

ب) سيناريو War Room:#

“لعبة أونلاين 50,000 لاعب WebSocket. كل 10 ثواني السيرفر بيعمل Lag 200ms والـ CPU Spike 100%.”

تحليل : الـ Spike المنتظم كل 10 ثواني = Scheduled Task (زي setInterval) على الـ Main Thread. غالباً: Leaderboard بيعمل sort لـ 50,000 عنصر، أو broadcast بـ state ضخم (JSON.stringify)، أو GC pause. الحل: انقل الشغل لـ Worker Thread — Leaderboard يحسب في Worker ويرجع النتيجة؛ الـ broadcast يعمل JSON.stringify مرة واحدة في Worker ويرجع الـ Buffer. وضبط الـ GC بـ --max-old-space-size.


من الآخر — الـ Mental Model#

Visualizing Node.js Architecture

يعني من الآخر: فهم الـ Event Loop والـ V8 هو اللي يفرق بين “مطور بيكتب Node” و”مهندس Backend يقدر يحل أزمات الأداء”. في اليوم اللي جاي هنستغل الفهم ده ونبني سيرفر حقيقي من الصفر.


قاموس المصطلحات — شرح دقيق لكل حاجة هتقابله#

عشان مفيش مصطلح يفضل غامض، دي تعريفات مرتبة أبجديًا (بالعربي) لكل المصطلحات التقنية اللي هتتكرر في المقال. تقدر ترجع لها أي وقت.

  • AST (Abstract Syntax Tree — شجرة التجريد النحوي): تمثيل بنيوي للكود بعد ما الـ Parser يقراه. مش نص؛ شجرة فيها عُقد، كل عقدة = تعبير أو أمر (متغير، عملية، استدعاء دالة). الـ V8 والـ Compilers بيعتمدوا عليها عشان التحويلات اللي بعد كده (Bytecode، Optimization).

  • Blocking (الانحصار / التشغيل المحظور): لما البرنامج (أو الـ Thread) يوقف وينتظر لحد ما عملية I/O تخلص (مثلاً قراءة ملف أو رد من الداتابيز). طول ما واقف، مش قادر يعمل حاجة تانية. الطريقة القديمة في السيرفرات كانت Blocking I/O (Thread ينتظر لكل طلب).

  • Bytecode (الكود البايتي): كود وسيط بين لغة عالية المستوى (زي JavaScript) والـ Machine Code. أقرب للـ CPU من النص، بس مش مرتبط بمعالج معين. الـ V8 بيولّده من الـ AST وبيتنفذ عليه بسرعة (عبر Ignition) من غير انتظار Compile كامل لـ Machine Code.

  • Callback (الدالة الرديفة): دالة انت بتبعتلها كـ argument لدالة تانية، والدي تانية بتناديها لما حاجة تحصل (مثلاً I/O يخلص، أو Timer يخلص). مثال: fs.readFile('file.txt', (err, data) => { ... }) — الـ (err, data) => { ... } هي الـ Callback اللي هتتنفذ لما قراءة الملف تخلص.

  • Call Stack (مكدس الاستدعاءات): مكان في الذاكرة اللي الـ CPU بيستخدمه عشان يعرف “أنا دلوقتي جوه إيه من الدوال”. كل ما تدخل دالة بتتضاف فوق الـ Stack؛ كل ما الدالة تخلص بتنزل (LIFO). في أي لحظة الـ CPU شغال على الدالة اللي فوق بس.

  • Compile / Compiled (التجميع / مُجمّع): تحويل الكود من لغة عالية المستوى لـ Machine Code (أو Bytecode) قبل التشغيل أو مرة واحدة عند البداية. اللغات المُجمّعة (زي C++) بتنتج ملف تنفيذي يشتغل مباشرة على الـ CPU.

  • Context Switching (تبديل السياق): لما نظام التشغيل يبدّل الـ CPU من تنفيذ Thread لتنفيذ Thread تاني. المحرك بيحفظ حالة الأول (Registers، مؤشر التعليمات، إلخ) ويحمل حالة التاني. كلما زاد عدد الـ Threads، الـ Switching يزيد ووقت/طاقة ضايعين في “نقل الانتباه” مش في الشغل الفعلي.

  • CPU (وحدة المعالجة المركزية): الجزء في الجهاز اللي ينفذ التعليمات (الحسابات، المقارنات، نقل البيانات). بيفهم Machine Code فقط — أصفار وآحاد. كل الكود اللي نكتبه (JS، C++) لازم يتحول لـ Machine Code عشان الـ CPU ينفذّه.

  • CPU-bound (مرتبط بالمعالج): شغل ثقيل على الـ CPU — حسابات، معالجة صور، تشفير، تحليل بيانات. لو حصل على الـ Main Thread في Node، الـ Thread بيقعد مشغول ومش بيسمح للـ Event Loop تكمل؛ كل الطلبات والـ Callbacks بتتأخر. الحل: نقل الشغل ده لـ Worker Thread أو خدمة تانية.

  • C10K Problem (مشكلة العشرة آلاف اتصال): سؤال قديم: إزاي سيرفر واحد يخدم 10,000 اتصال متزامن من غير ما ينهار؟ الطريقة اللي كانت شائعة (Thread لكل اتصال + Blocking I/O) خلت الرام والـ Context Switching يطلعوا عن السيطرة. Node (وغيره) حلوها بـ Non-blocking I/O و Event Loop وثريدات قليلة.

  • Deoptimization (إلغاء التحسين): لما الـ V8 يكون عمل Optimization (تحويل لـ Machine Code محسّن) على افتراض معين (مثلاً إن متغير دايماً Number)، وبعدين الافتراض يتبطل (جت قيمة String). الـ V8 يلغي الكود المحسّن ويرجع ينفذ من الـ Bytecode اللي صحيح لكل الأنواع.

  • epoll / kqueue / IOCP: آليات في نظام التشغيل (epoll في Linux، kqueue في macOS، IOCP في Windows) اسمها event notification. البرنامج بيقول للـ OS: “انتظر لحد ما يكون في بيانات جاهزة على السوكيت أو الملف، ونبهني.” فالـ CPU مش بيضيع وقت في حلقة انتظار؛ الـ OS هو اللي بينتظر ويرجع إشارة للبرنامج.

  • Event Loop (حلقة الأحداث): حلقة في Node (مُدارة بواسطة libuv) بتمرّ على مراحل ثابتة (Timers، Pending، Poll، Check، Close). في كل مرحلة بتنفذ الـ Callbacks اللي في الطابور بتاعها، وبين المراحل بتنفذ الـ Microtasks. الهدف: تنفيذ كل الـ Callbacks بالترتيب من غير ما الـ Main Thread ينتظر (Block) على I/O.

  • Heap (الكومة / منطقة الذاكرة): منطقة ذاكرة الـ V8 بيستخدمها عشان يخزن Objects والمتغيرات اللي عايشة لفترة (مش على الـ Call Stack). الـ Garbage Collector بينظف الـ Heap من القيم اللي مبقاش ليها مراجع.

  • Hot Function / Hot Path: جزء من الكود (دالة أو حلقة) اتنادى أو اتنفذ آلاف المرات. الـ V8 بيعتبره “حار” ويدخله لـ TurboFan عشان يتحول لـ Machine Code محسّن (JIT).

  • I/O (Input/Output — الإدخال والإخراج): أي شغل يكون تبادل بيانات مع برة البرنامج — قراءة/كتابة ملف، طلب من الداتابيز، طلب HTTP، استقبال من السوكيت. جزء كبير من الوقت بيقضي في انتظار الرد (القرص، الشبكة، الداتابيز) مش في حساب على الـ CPU.

  • I/O-bound (مرتبط بالإدخال والإخراج): الشغل اللي معظم وقته انتظار I/O (ملفات، شبكة، DB). Node مصمم يخدمه كويس لأن الـ Main Thread مش واقف؛ بيودي الطلب لمن ينتظر (OS أو Thread Pool) ويكمل.

  • JIT (Just-In-Time Compilation — التجميع في اللحظة): تحويل جزء من الكود لـ Machine Code أثناء التشغيل (مش قبلها)، عادة للكود “الحار” اللي بيتكرر كتير. الـ V8 بيستخدم JIT عبر TurboFan عشان JavaScript توصل لسرعة قريبة من C++ في الأجزاء الحرجة.

  • libuv: مكتبة C مفتوحة المصدر وبتعتبر قلب Node من ناحية I/O. بتدير: (1) الـ Event Loop، (2) الـ Thread Pool (افتراضي 4 threads)، (3) الـ Async I/O مع الـ OS (epoll / kqueue / IOCP). من غيرها Node مكنش هيقدّر يعمل Non-blocking I/O بالشكل ده.

  • LIFO (Last In, First Out): آخر حاجة دخلت تطلع أول حاجة. الـ Call Stack شغال بالطريقة دي: آخر دالة اتستدعت تكون فوق وتتنفذ أولاً؛ لما تخلص بتنزل وتكمل اللي تحتها.

  • Machine Code (الكود الآلي): تعليمات ثنائية (أصفار وآحاد) اللي الـ CPU بيفهمها مباشرة. كل لغة برمجة عالية المستوى بتتحول لـ Machine Code (أو لـ Bytecode يتحول بدوره) عشان التنفيذ الفعلي.

  • Macrotask (المهمة الكبرى): مهمة بتنفذ في مراحل الـ Event Loop العادية — زي callbacks الـ setTimeout، setInterval، setImmediate، و callbacks الـ I/O. بين كل Macrotask واللي بعدها (وبين كل مرحلتين) الـ Loop بينفذ كل الـ Microtasks الأول.

  • Main Thread (المسار الرئيسي): الـ Thread الواحد اللي بينفذ كود الـ JavaScript بتاعك في Node. الـ Event Loop شغالة عليه؛ كل الـ Callbacks بتتنفذ عليه. الشغل اللي مش I/O (زي الـ CPU-bound) لو حصل عليه يوقف الـ Loop كلها.

  • Microtask (المهمة الصغرى): مهمة أولوية أعلى من الـ Macrotasks — بتتنفذ فوراً بعد كل Macrotask (وبين كل مرحلتين). أمثلة: Promise.then()، queueMicrotask()، و process.nextTick() (أعلى أولوية من كل الـ Microtasks).

  • Non-blocking I/O (الإدخال والإخراج غير المحظور): نموذج الـ I/O اللي البرنامج مابيقعدش ينتظر فيه. بيبعت الطلب (قراءة ملف، اتصال شبكة) ويحط Callback؛ لما النتيجة تيجي الـ OS (أو الـ Worker) بينبه البرنامج فينفذ الـ Callback. كده الـ Main Thread يكمل يخدم طلبات تانية بدل ما يقف.

  • Node.js Bindings (ربط Node): الطبقة اللي تربط بين دوال JavaScript (زي fs.readFile، http.createServer) وبين تنفيذ C++ اللي يكلم نظام التشغيل. الـ Binding بياخد الطلب من JS ويمرره لـ libuv أو للـ OS، ومش بيوقف الـ Main Thread — بيحط الـ Callback في الطابور.

  • Optimization (التحسين): في سياق V8: تحويل جزء من الـ Bytecode لـ Machine Code أسرع مخصّص لمعمارية الـ CPU، عادة للكود “الحار”. لو الافتراضات اللي التحسين مبني عليها اتكسرت، بيحصل Deoptimization.

  • Parser (المحلل): المرحلة اللي بتقرأ نص الكود وتكسره لـ Tokens (وحدات صغيرة: كلمات، رموز) وتتأكد من صحة الـ Syntax. الناتج عادة AST مش نص تاني.

  • Phase (المرحلة): واحدة من المراحل الثابتة اللي الـ Event Loop بتمر عليها بالترتيب في كل Tick: Timers، Pending Callbacks، Idle/Prepare، Poll، Check، Close. كل مرحلة ليها Queue (طابور) من الـ Callbacks.

  • process.nextTick(): في Node، دالة بتحط الـ Callback في طابور خاص بيُنفَّذ قبل أي حاجة تانية — قبل الـ Promises وقبل أي مرحلة من مراحل الـ Event Loop. يعني “نفّذني في أقرب لحظة بعد ما الـ Call Stack يفضى.” استخدامها كتير أو بشكل recursive بيقدر يسبب I/O Starvation (تأخير كل الـ I/O).

  • Queue (الطابور): قائمة مرتبة من الـ Callbacks (أو المهام) اللي هتتنفذ بالدور. كل مرحلة في الـ Event Loop ليها Queue خاصة بيها. غالباً FIFO (أول حاجة دخلت تطلع أول حاجة).

  • Request (الطلب): في سياق السيرفرات: طلب HTTP (أو اتصال) من العميل للسيرفر — مثلاً “اعرض لي الصفحة الرئيسية” أو “أرسل بيانات الـ API”. في الطريقة القديمة كل Request كان ياخد Thread؛ في Node الـ Main Thread يخدم آلاف الـ Requests بالتناوب عبر الـ Event Loop.

  • Runtime (البيئة التشغيلية): البيئة اللي فيها الكود بيتنفذ — مش بس اللغة. JavaScript في المتصفح ليها Runtime (DOM، BOM، Web APIs)؛ في Node ليها Runtime تانية (fs، http، نظام التشغيل). نفس الـ Syntax، بيئة مختلفة.

  • setImmediate(): في Node، دالة بتحط الـ Callback في مرحلة Check من الـ Event Loop. معناها: “نفّذني بعد ما تخلص مرحلة الـ Poll مباشرة.” مختلفة عن setTimeout(fn, 0) اللي بيدخل في Timers (أول مرحلة في الـ Tick).

  • Socket (المقبس): قناة اتصال شبكي بين برنامجين (مثلاً السيرفر والعميل). قراءة/كتابة من السوكيت = I/O؛ الـ OS بيقدر ينتظر بيانات على السوكيت ويُنبه البرنامج (عبر epoll/kqueue/IOCP) من غير ما البرنامج يحرق CPU.

  • Syntax (النحو / البنية): قواعد كتابة الكود الصحيحة (أين توضع الأقواس، الفواصل، الكلمات المحجوزة). الـ Parser بيتأكد إن الكود متوافق مع الـ Syntax؛ لو لأ بيطلع خطأ.

  • Tick (اللفة / النبضة): لفة واحدة كاملة للـ Event Loop على كل المراحل الستة (من Timers لـ Close). بعد الـ Close الـ Loop ترجع من أول Timers = Tick جديد.

  • Thread (المسار / الخيط): أصغر وحدة تنفيذ يمكن لـ نظام التشغيل أن يجدولها. برنامج واحد يقدر يفتح عدة Threads؛ كل Thread بيشتغل بشكل مستقل (لكن بيشارك ذاكرة البرنامج). في Node كود الـ JS شغال على Thread واحد (Main Thread)، و libuv بتشغل Thread Pool صغير (4 افتراضي) لشغل معين.

  • Thread Pool (مجموعة المسارات): مجموعة ثابتة من الـ Worker Threads جاهزة تنفذ مهام (زي قراءة ملف، تشفير). بدل ما تفتح Thread جديد لكل طلب، الطلبات بتتوضع في طابور والـ Workers اللي فاضيين بياخدوا منها. في Node الـ libuv بتدير Thread Pool (افتراضي 4) لـ fs، crypto، DNS، zlib، إلخ.

  • Tokens (الرموز اللغوية): أصغر وحدات ذات معنى بعد تحليل النص — مثلاً const، x، =، 5، +، 3. الـ Parser بيحول الكود لـ Tokens أولاً ثم يبني منها الـ AST.

  • V8: محرك تنفيذ JavaScript (وكمان WebAssembly) من تطوير Google، مستخدم في Chrome وفي Node.js. بيحول JS لـ Bytecode ثم لـ Machine Code (عبر Ignition و TurboFan) ويدير الـ Call Stack والـ Heap.

  • Worker / Worker Thread: Thread منفصل عن الـ Main Thread ينفذ شغل ثقيل (غالباً CPU-bound) عشان الـ Main Thread ميتشلش. في Node: إما من libuv Thread Pool (لـ I/O معين)، أو Worker Threads من الـ worker_threads module (لشغل CPU-bound زي معالجة صور).

  • Worker Pool (مجموعة العمال): نفس فكرة الـ Thread Pool لكن للـ Worker Threads اللي انت بتبنيها (من worker_threads). بتفتح عدد ثابت من الـ Workers عند البداية وتوزع عليهم المهام؛ لو كلهم مشغولين المهام تقعد في Queue بدل ما تفتح مئات الـ Threads.


Join our whatsapp group here
My Channel here