استكشاف عناصر Node.js الداخلية
نشرت: 2022-03-10منذ تقديم Node.js بواسطة Ryan Dahl في JSConf الأوروبي في 8 نوفمبر 2009 ، شهد استخدامًا واسعًا في صناعة التكنولوجيا. تمنح شركات مثل Netflix و Uber و LinkedIn مصداقية للادعاء بأن Node.js يمكنها تحمل قدر كبير من حركة المرور والتزامن.
مسلحين بالمعرفة الأساسية ، يعاني مطورو Node.js المبتدئين والمتوسطين من العديد من الأشياء: "إنه مجرد وقت تشغيل!" "بها حلقات أحداث!" "Node.js هو خيط واحد مثل جافا سكريبت!"
في حين أن بعض هذه الادعاءات صحيحة ، سنبحث بشكل أعمق في وقت تشغيل Node.js ، لفهم كيفية تشغيل JavaScript ، ومعرفة ما إذا كانت في الواقع أحادية الترابط ، وأخيراً ، فهم أفضل للترابط بين تبعياتها الأساسية ، V8 و libuv .
المتطلبات الأساسية
- معرفة أساسية بجافا سكريبت
- الإلمام بدلالات Node.js (
require
،fs
)
ما هو Node.js؟
قد يكون من المغري افتراض ما يعتقده الكثير من الناس حول Node.js ، التعريف الأكثر شيوعًا له هو أنه وقت تشغيل لغة JavaScript . للنظر في هذا ، يجب أن نفهم ما أدى إلى هذا الاستنتاج.
غالبًا ما يتم وصف Node.js على أنه مزيج من C ++ و JavaScript. يتكون الجزء C ++ من روابط تقوم بتشغيل رمز منخفض المستوى يجعل من الممكن الوصول إلى الأجهزة المتصلة بالكمبيوتر. يأخذ جزء JavaScript JavaScript كرمز مصدر لها ويقوم بتشغيلها في مترجم شائع للغة ، يُسمى محرك V8.
من خلال هذا الفهم ، يمكننا وصف Node.js كأداة فريدة تجمع بين JavaScript و C ++ لتشغيل البرامج خارج بيئة المتصفح.
لكن هل يمكن أن نسميها وقت تشغيل؟ لتحديد ذلك ، دعنا نحدد ما هو وقت التشغيل.
ما هو وقت التشغيل؟ https://t.co/eaF4CoWecX
- كريستيان نوامبا (codebeast) 5 مارس 2020
في إحدى إجاباته على StackOverflow ، يعرّف DJNA بيئة وقت التشغيل بأنها "كل ما تحتاجه لتنفيذ برنامج ، ولكن لا توجد أدوات لتغييره". وفقًا لهذا التعريف ، يمكننا أن نقول بثقة أن كل ما يحدث أثناء تشغيل الكود الخاص بنا (بأي لغة على الإطلاق) يعمل في بيئة وقت التشغيل.
اللغات الأخرى لها بيئة تشغيل خاصة بها. بالنسبة إلى Java ، فهي بيئة Java Runtime Environment (JRE). بالنسبة إلى .NET ، يعد هذا هو وقت تشغيل اللغة العامة (CLR). بالنسبة لـ Erlang ، إنها BEAM.
ومع ذلك ، فإن بعض أوقات التشغيل هذه لها لغات أخرى تعتمد عليها. على سبيل المثال ، تحتوي Java على Kotlin ، وهي لغة برمجة يتم تجميعها لترميز يمكن لـ JRE فهمه. إرلانج لديه إكسير. ونحن نعلم أن هناك العديد من المتغيرات لتطوير .NET ، والتي تعمل جميعها في CLR ، والمعروفة باسم .NET Framework.
نحن الآن نفهم أن وقت التشغيل عبارة عن بيئة يتم توفيرها لبرنامج ليكون قادرًا على التنفيذ بنجاح ، ونحن نعلم أن V8 ومجموعة من مكتبات C ++ تجعل من الممكن تنفيذ تطبيق Node.js. Node.js نفسه هو وقت التشغيل الفعلي الذي يربط كل شيء معًا لجعل هذه المكتبات كيانًا ، وهو لا يفهم سوى لغة واحدة - JavaScript - بغض النظر عما تم إنشاء Node.js به.
الهيكل الداخلي لـ Node.js
عندما نحاول تشغيل برنامج Node.js (مثل index.js
) من سطر الأوامر باستخدام node index.js
، فإننا نستدعي وقت تشغيل Node.js. يتكون وقت التشغيل هذا ، كما ذكرنا ، من تبعيتين مستقلتين ، V8 و libuv.

V8 هو مشروع تم إنشاؤه وصيانته بواسطة Google. يأخذ كود مصدر JavaScript ويقوم بتشغيله خارج بيئة المتصفح. عندما نقوم بتشغيل برنامج من خلال أمر node
، يتم تمرير كود المصدر من خلال وقت تشغيل Node.js إلى V8 للتنفيذ.
تحتوي مكتبة libuv على كود C ++ الذي يتيح الوصول منخفض المستوى إلى نظام التشغيل. لا يتم شحن الوظائف مثل الشبكات والكتابة إلى نظام الملفات والتزامن افتراضيًا في V8 ، وهو جزء من Node.js يقوم بتشغيل كود JavaScript الخاص بنا. مع مجموعة المكتبات الخاصة به ، يوفر libuv هذه الأدوات المساعدة والمزيد في بيئة Node.js.
Node.js هو الغراء الذي يربط المكتبتين معًا ، وبالتالي يصبح حلاً فريدًا. خلال تنفيذ البرنامج النصي ، تفهم Node.js المشروع الذي سيمرر التحكم إليه ومتى.
واجهات برمجة تطبيقات مثيرة للاهتمام للبرامج من جانب الخادم
إذا درسنا القليل من تاريخ JavaScript ، فسنعلم أنه من المفترض إضافة بعض الوظائف والتفاعل إلى صفحة في المتصفح. وفي المتصفح ، نتفاعل مع عناصر نموذج كائن المستند (DOM) التي تتكون منها الصفحة. لهذا ، توجد مجموعة من واجهات برمجة التطبيقات ، يشار إليها مجتمعة باسم DOM API.
DOM موجود فقط في المتصفح ؛ إنه ما يتم تحليله لتقديم صفحة ، وهو مكتوب أساسًا بلغة الترميز المعروفة باسم HTML. أيضًا ، يوجد المتصفح في نافذة ، ومن ثم كائن window
، الذي يعمل كجذر لجميع الكائنات على الصفحة في سياق JavaScript. تسمى هذه البيئة بيئة المتصفح ، وهي بيئة وقت تشغيل لجافا سكريبت.

في بيئة Node.js ، ليس لدينا شيء مثل الصفحة أو المتصفح - وهذا يبطل معرفتنا بكائن النافذة العامة. ما لدينا هو مجموعة من واجهات برمجة التطبيقات التي تتفاعل مع نظام التشغيل لتوفير وظائف إضافية لبرنامج JavaScript. واجهات برمجة التطبيقات هذه لـ Node.js ( fs
، path
، buffer
، events
، HTTP
، وما إلى ذلك) ، كما لدينا ، موجودة فقط لـ Node.js ، ويتم توفيرها بواسطة Node.js (وهي نفسها وقت تشغيل) حتى نتمكن من يمكنه تشغيل البرامج المكتوبة لـ Node.js.
التجربة: كيف fs.writeFile
بإنشاء ملف جديد
إذا تم إنشاء V8 لتشغيل JavaScript خارج المتصفح ، وإذا كانت بيئة Node.js لا تحتوي على نفس السياق أو البيئة مثل المتصفح ، فكيف نفعل شيئًا مثل الوصول إلى نظام الملفات أو إنشاء خادم HTTP؟
كمثال ، لنأخذ تطبيق Node.js بسيط يكتب ملفًا إلى نظام الملفات في الدليل الحالي:
const fs = require("fs") fs.writeFile("./test.txt", "text");
كما هو موضح ، نحاول كتابة ملف جديد إلى نظام الملفات. هذه الميزة غير متوفرة بلغة جافا سكريبت ؛ يتوفر فقط في بيئة Node.js. كيف يتم تنفيذ هذا؟
لفهم هذا ، دعنا نأخذ جولة في قاعدة كود Node.js.
بالانتقال إلى مستودع GitHub لـ Node.js ، نرى مجلدين رئيسيين ، src
و lib
. يحتوي مجلد lib
على كود JavaScript الذي يوفر مجموعة لطيفة من الوحدات المضمنة افتراضيًا مع كل تثبيت Node.js. يحتوي المجلد src
على مكتبات C ++ لـ libuv.
إذا نظرنا إلى مجلد lib
fs.js
ملف fs.js ، فسنرى أنه مليء بشفرة JavaScript الرائعة. في السطر 1880 ، سنلاحظ بيان exports
. تصدر هذه العبارة كل ما يمكننا الوصول إليه عن طريق استيراد الوحدة fs
، ويمكننا أن نرى أنها تصدر دالة باسم writeFile
.
البحث عن function writeFile(
(حيث يتم تحديد الوظيفة) يقودنا إلى السطر 1303 ، حيث نرى أن الوظيفة محددة بأربع معلمات:
function writeFile(path, data, options, callback) { callback = maybeCallback(callback || options); options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' }); const flag = options.flag || 'w'; if (!isArrayBufferView(data)) { validateStringAfterArrayBufferView(data, 'data'); data = Buffer.from(data, options.encoding || 'utf8'); } if (isFd(path)) { const isUserFd = true; writeAll(path, isUserFd, data, 0, data.byteLength, callback); return; } fs.open(path, flag, options.mode, (openErr, fd) => { if (openErr) { callback(openErr); } else { const isUserFd = false; writeAll(fd, isUserFd, data, 0, data.byteLength, callback); } }); }
في السطرين 1315 و 1324 ، نرى أنه يتم استدعاء دالة واحدة ، writeAll
، بعد بعض عمليات التحقق من الصحة. نجد هذه الوظيفة في السطر 1278 في نفس ملف fs.js
function writeAll(fd, isUserFd, buffer, offset, length, callback) { // write(fd, buffer, offset, length, position, callback) fs.write(fd, buffer, offset, length, null, (writeErr, written) => { if (writeErr) { if (isUserFd) { callback(writeErr); } else { fs.close(fd, function close() { callback(writeErr); }); } } else if (written === length) { if (isUserFd) { callback(null); } else { fs.close(fd, callback); } } else { offset += written; length -= written; writeAll(fd, isUserFd, buffer, offset, length, callback); } }); }
من المثير للاهتمام أيضًا ملاحظة أن هذه الوحدة تحاول استدعاء نفسها. نرى هذا في السطر 1280 ، حيث يتم استدعاء fs.write
. عند البحث عن وظيفة write
، سوف نكتشف القليل من المعلومات.

تبدأ وظيفة write
في السطر 571 ، وهي تعمل بحوالي 42 سطرًا. نرى نمطًا متكررًا في هذه الوظيفة: الطريقة التي تستدعي بها وظيفة في وحدة binding
، كما هو موضح في السطور 594 و 612. لا يتم استدعاء وظيفة في وحدة binding
ليس فقط في هذه الوظيفة ، ولكن تقريبًا في أي وظيفة يتم تصديرها في ملف fs.js
يجب أن يكون هناك شيء مميز للغاية حيال ذلك.
يتم الإعلان عن متغير binding
في السطر 58 ، في أعلى الملف ، والنقر فوق استدعاء الوظيفة هذا يكشف بعض المعلومات ، بمساعدة GitHub.

تم العثور على وظيفة internalBinding
هذه في الوحدة النمطية المسماة لوادر. تتمثل الوظيفة الرئيسية لوحدة اللوادر في تحميل جميع مكتبات libuv وربطها من خلال مشروع V8 باستخدام Node.js. إن كيفية القيام بذلك أمر سحري إلى حد ما ، ولكن لمعرفة المزيد يمكننا أن ننظر عن كثب في وظيفة writeBuffer
التي يطلق عليها الوحدة fs
.
يجب أن ننظر إلى المكان الذي يرتبط فيه هذا بـ libuv ، وأين يأتي V8. في الجزء العلوي من وحدة اللودر ، تنص بعض الوثائق الجيدة على هذا:
// This file is compiled and run by node.cc before bootstrap/node.js // was called, therefore the loaders are bootstraped before we start to // actually bootstrap Node.js. It creates the following objects: // // C++ binding loaders: // - process.binding(): the legacy C++ binding loader, accessible from user land // because it is an object attached to the global process object. // These C++ bindings are created using NODE_BUILTIN_MODULE_CONTEXT_AWARE() // and have their nm_flags set to NM_F_BUILTIN. We do not make any guarantees // about the stability of these bindings, but still have to take care of // compatibility issues caused by them from time to time. // - process._linkedBinding(): intended to be used by embedders to add // additional C++ bindings in their applications. These C++ bindings // can be created using NODE_MODULE_CONTEXT_AWARE_CPP() with the flag // NM_F_LINKED. // - internalBinding(): the private internal C++ binding loader, inaccessible // from user land unless through `require('internal/test/binding')`. // These C++ bindings are created using NODE_MODULE_CONTEXT_AWARE_INTERNAL() // and have their nm_flags set to NM_F_INTERNAL. // // Internal JavaScript module loader: // - NativeModule: a minimal module system used to load the JavaScript core // modules found in lib/**/*.js and deps/**/*.js. All core modules are // compiled into the node binary via node_javascript.cc generated by js2c.py, // so they can be loaded faster without the cost of I/O. This class makes the // lib/internal/*, deps/internal/* modules and internalBinding() available by // default to core modules, and lets the core modules require itself via // require('internal/bootstrap/loaders') even when this file is not written in // CommonJS style.
ما نتعلمه هنا هو أنه لكل وحدة يتم استدعاؤها من كائن binding
في قسم JavaScript في مشروع Node.js ، هناك ما يعادله في قسم C ++ ، في المجلد src
.
من خلال جولتنا fs
، نرى أن الوحدة التي تقوم بذلك موجودة في node_file.cc
. يتم تحديد كل وظيفة يمكن الوصول إليها من خلال الوحدة النمطية في الملف ؛ على سبيل المثال ، لدينا writeBuffer
في السطر 2258. التعريف الفعلي لهذه الطريقة في ملف C ++ موجود في السطر 1785. أيضًا ، يمكن العثور على الاستدعاء لجزء libuv الذي يقوم بالكتابة الفعلية للملف في الأسطر 1809 و 1815 ، حيث يتم استدعاء دالة uv_fs_write
بشكل غير متزامن.
ماذا نكسب من هذا الفهم؟
تمامًا مثل العديد من أوقات تشغيل اللغة المفسرة ، يمكن اختراق وقت تشغيل Node.js. مع مزيد من الفهم ، يمكننا القيام بأشياء مستحيلة مع التوزيع القياسي فقط من خلال النظر في المصدر. يمكننا إضافة مكتبات لإجراء تغييرات على طريقة استدعاء بعض الوظائف. لكن قبل كل شيء ، هذا الفهم هو أساس لمزيد من الاستكشاف.
هل Node.js ذات مؤشر ترابط واحد؟
من خلال موقعي libuv و V8 ، يتمتع Node.js بإمكانية الوصول إلى بعض الوظائف الإضافية التي لا يتوفر بها محرك JavaScript نموذجي يعمل في المتصفح.
سيتم تنفيذ أي JavaScript يتم تشغيله في المستعرض في سلسلة واحدة. يشبه الخيط في تنفيذ البرنامج الصندوق الأسود الموجود أعلى وحدة المعالجة المركزية (CPU) حيث يتم تنفيذ البرنامج. في سياق Node.js ، يمكن تنفيذ بعض التعليمات البرمجية في العديد من سلاسل الرسائل التي يمكن أن تحملها أجهزتنا.
للتحقق من هذا الادعاء المحدد ، دعنا نستكشف مقتطف رمز بسيط.
const fs = require("fs"); // A little benchmarking const startTime = Date.now() fs.writeFile("./test.txt", "test", (err) => { If (error) { console.log(err) } console.log("1 Done: ", Date.now() — startTime) });
في المقتطف أعلاه ، نحاول إنشاء ملف جديد على القرص في الدليل الحالي. لمعرفة المدة التي قد يستغرقها ذلك ، أضفنا القليل من المعيار لمراقبة وقت بدء البرنامج النصي ، والذي يعطينا المدة بالمللي ثانية من البرنامج النصي الذي يقوم بإنشاء الملف.
إذا قمنا بتشغيل الكود أعلاه ، فسنحصل على نتيجة مثل هذه:

$ node ./test.js -> 1 Done: 0.003s
هذا مثير للإعجاب: 0.003 ثانية فقط.
لكن دعونا نفعل شيئًا مثيرًا للاهتمام حقًا. أولاً ، دعنا نكرر الكود الذي يُنشئ الملف الجديد ، ونحدِّث الرقم في بيان السجل ليعكس مواضعهم:
const fs = require("fs"); // A little benchmarking const startTime = Date.now() fs.writeFile("./test1.txt", "test", function (err) { if (err) { console.log(err) } console.log("1 Done: %ss", (Date.now() — startTime) / 1000) }); fs.writeFile("./test2.txt", "test", function (err) { if (err) { console.log(err) } console.log("2 Done: %ss", (Date.now() — startTime) / 1000) }); fs.writeFile("./test3.txt", "test", function (err) { if (err) { console.log(err) } console.log("3 Done: %ss", (Date.now() — startTime) / 1000) }); fs.writeFile("./test4.txt", "test", function (err) { if (err) { console.log(err) } console.log("4 Done: %ss", (Date.now() — startTime) / 1000) });
إذا حاولنا تشغيل هذا الرمز ، فسنحصل على شيء يفجر عقولنا. ها هي نتيجتي:

أولاً ، سوف نلاحظ أن النتائج غير متسقة. ثانيًا ، نرى أن الوقت قد زاد. ماذا يحدث؟
احصل على تفويض المهام ذات المستوى المنخفض
Node.js هو مؤشر ترابط واحد ، كما نعلم الآن. تمت كتابة أجزاء من Node.js بلغة JavaScript ، وأجزاء أخرى بلغة C ++. يستخدم Node.js نفس مفاهيم حلقة الحدث ومكدس الاستدعاءات الذي نعرفه من بيئة المتصفح ، مما يعني أن أجزاء JavaScript في Node.js مترابطة. لكن المهمة ذات المستوى المنخفض التي تتطلب التحدث مع نظام التشغيل ليست ذات مؤشر ترابط واحد.

عندما يتعرف Node.js على مكالمة على أنها مخصصة لـ libuv ، فإنها تفوض هذه المهمة إلى libuv. تتطلب عملية libuv مؤشرات ترابط لبعض مكتباتها ، ومن ثم يتم استخدام تجمع مؤشرات الترابط في تنفيذ برامج Node.js عند الحاجة إليها.
بشكل افتراضي ، يحتوي تجمع مؤشرات الترابط Node.js الذي يوفره libuv على أربعة مؤشرات ترابط فيه. يمكننا زيادة أو تقليل تجمع مؤشرات الترابط هذا عن طريق استدعاء process.env.UV_THREADPOOL_SIZE
في الجزء العلوي من البرنامج النصي الخاص بنا.
// script.js process.env.UV_THREADPOOL_SIZE = 6; // … // …
ماذا يحدث مع برنامج إنشاء الملفات لدينا
يبدو أنه بمجرد استدعاء الكود لإنشاء ملفنا ، يقوم Node.js بضرب جزء libuv من الكود الخاص به ، والذي يخصص سلسلة رسائل لهذه المهمة. يحصل هذا القسم في libuv على بعض المعلومات الإحصائية حول القرص قبل العمل على الملف.
قد يستغرق هذا الفحص الإحصائي بعض الوقت حتى يكتمل ؛ ومن ثم ، يتم تحرير مؤشر الترابط لبعض المهام الأخرى حتى يتم الانتهاء من الفحص الإحصائي. عند اكتمال الفحص ، يحتل قسم libuv أي مؤشر ترابط متاح أو ينتظر حتى يصبح الخيط متاحًا له.
لدينا أربعة مكالمات وأربعة سلاسل فقط ، لذلك هناك ما يكفي من الخيوط للتغلب عليها. السؤال الوحيد هو مدى سرعة كل خيط في معالجة مهمته. سوف نلاحظ أن الكود الأول لجعله في تجمع مؤشرات الترابط سيعيد نتيجته أولاً ، وسيحظر جميع الخيوط الأخرى أثناء تشغيل الكود الخاص به.
خاتمة
نحن الآن نفهم ما هو Node.js. نحن نعلم أنه وقت التشغيل. لقد حددنا ما هو وقت التشغيل. ولقد تعمقنا في معرفة العناصر المكونة لوقت التشغيل الذي توفره Node.js.
لقد قطعنا شوطا طويلا. ومن جولتنا الصغيرة في مستودع Node.js على GitHub ، يمكننا استكشاف أي واجهة برمجة تطبيقات قد تكون مهتمًا بها ، باتباع نفس العملية التي اتخذناها هنا. Node.js مفتوح المصدر ، لذا يمكننا بالتأكيد الغوص في المصدر ، أليس كذلك؟
على الرغم من أننا تطرقنا إلى العديد من المستويات المنخفضة لما يحدث في وقت تشغيل Node.js ، يجب ألا نفترض أننا نعرف كل شيء. تشير الموارد أدناه إلى بعض المعلومات التي يمكننا بناء معرفتنا عليها:
- مقدمة إلى Node.js
لكونه موقعًا رسميًا ، يشرح Node.dev ماهية Node.js ، بالإضافة إلى مديري الحزم الخاصة به ، ويسرد أطر عمل الويب المبنية فوقه. - "JavaScript & Node.js" ، كتاب Node Beginner
يقوم هذا الكتاب من تأليف Manuel Kiessling بعمل رائع في شرح Node.js ، بعد تحذيره من أن JavaScript في المستعرض ليس هو نفسه الموجود في Node.js ، على الرغم من أن كليهما مكتوب بنفس اللغة. - بداية Node.js
يتخطى كتاب المبتدئين هذا شرح وقت التشغيل. إنه يعلم عن الحزم والتدفق وإنشاء خادم ويب باستخدام إطار عمل Express. - LibUV
هذا هو التوثيق الرسمي لرمز C ++ الداعم لوقت تشغيل Node.js. - V8
هذا هو التوثيق الرسمي لمحرك JavaScript الذي يجعل من الممكن كتابة Node.js باستخدام JavaScript.