1. لماذا تتمثل JavaScript في الخيال؟
ميزة رئيسية للغة JavaScript هي خيط واحد ، مما يعني أنه لا يمكنك فعل شيء واحد إلا في نفس الوقت. فلماذا لا تستطيع JavaScript أن يكون لها عدة مؤشرات ترابط؟ هذا سوف يحسن الكفاءة.
يرتبط خيوط JavaScript الفردية بغرضها. كلغة البرمجة النصية للمتصفح ، الغرض الرئيسي من JavaScript هو التفاعل مع المستخدمين وتشغيل DOM. هذا يحدد أنه لا يمكن أن يكون سوى خيوط واحدة ، وإلا فإنه سيؤدي إلى مشاكل تزامن معقدة للغاية. على سبيل المثال ، لنفترض أن JavaScript لديها مؤشر ترابط في نفس الوقت ، ويضيف مؤشر ترابط واحد محتوى على عقدة DOM معينة ، ويحذف مؤشر الترابط الآخر هذه العقدة ، أي مؤشر ترابط يجب أن يأخذه المستعرض في هذا الوقت؟
لذلك ، من أجل تجنب التعقيد ، يعد JavaScript موضوعًا واحدًا من ولادته ، والذي أصبح الميزة الأساسية لهذه اللغة ولن يتغير في المستقبل.
من أجل الاستفادة من قوة الحوسبة في وحدات المعالجة المركزية متعددة النواة ، اقترح HTML5 معيار عامل الويب ، مما يسمح برامج نصوص JavaScript لإنشاء مؤشرات ترابط متعددة ، ولكن يتم التحكم في مؤشرات الترابط الطفل بالكامل بواسطة مؤشر الترابط الرئيسي ولا يمكن تشغيل DOM. لذلك ، لا يغير هذا المعيار الجديد طبيعة الخيوط الفردية JavaScript.
2. قائمة انتظار المهمة
يعني الخيوط الفردية أن جميع المهام تحتاج إلى قائمة الانتظار ، وسيتم تنفيذ المهمة السابقة قبل تنفيذ المهمة التالية. إذا استغرقت المهمة السابقة وقتًا طويلاً ، فيجب أن تنتظر المهمة التالية.
إذا كانت قائمة الانتظار بسبب الكمية الكبيرة من الحوسبة وكانت وحدة المعالجة المركزية مشغولة للغاية ، فسيكون ذلك جيدًا ، ولكن في كثير من الأحيان تكون وحدة المعالجة المركزية في وضع الخمول لأن جهاز IO (جهاز الإدخال والإخراج) بطيء للغاية (مثل عملية Ajax يقرأ البيانات من الشبكة) ، وعليك الانتظار حتى يتم التوصل إلى النتيجة قبل تنفيذها.
أدرك مصمم لغة JavaScript أنه في هذا الوقت ، يمكن لوحدة المعالجة المركزية تجاهل جهاز IO تمامًا ، وتعليق مهام الانتظار وتشغيل المهام التالية أولاً. انتظر حتى يقوم جهاز IO بإرجاع النتيجة ، ثم يستدير ومتابعة المهمة المعلقة.
لذلك ، لدى JavaScript طريقتان للتنفيذ: الأول هو أن وحدة المعالجة المركزية تنفذ بالتسلسل ، وتنتهي المهمة السابقة ، ثم يتم تنفيذ المهمة التالية ، والتي تسمى التنفيذ المتزامن ؛ والآخر هو أن وحدة المعالجة المركزية تتخطى مهام مع وقت انتظار طويل ويعالج المهام اللاحقة أولاً ، والتي تسمى التنفيذ غير المتزامن. يختار المبرمجون بشكل مستقل نوع طريقة التنفيذ التي يجب تبنيها.
على وجه التحديد ، آلية التشغيل للتنفيذ غير المتزامن كما يلي. (نفس الشيء صحيح بالنسبة للتنفيذ المتزامن ، حيث يمكن اعتباره تنفيذًا غير متزامن دون مهام غير متزامنة.)
(1) يتم تنفيذ جميع المهام على مؤشر الترابط الرئيسي لتشكيل مكدس سياق التنفيذ.
(2) بالإضافة إلى الموضوع الرئيسي ، هناك أيضًا "قائمة انتظار مهمة". يضع النظام المهام غير المتزامنة في "قائمة انتظار المهمة" ثم يستمر في تنفيذ المهام اللاحقة.
(3) بمجرد تنفيذ جميع المهام في "مكدس التنفيذ" ، سيقرأ النظام "قائمة انتظار المهمة". إذا كانت المهمة غير المتزامنة في هذا الوقت قد أنهت حالة الانتظار ، فستدخل مكدس التنفيذ من "قائمة انتظار المهمة" ويستأنف التنفيذ.
(4) يستمر الخيط الرئيسي في تكرار الخطوة الثالثة أعلاه.
الشكل التالي هو رسم تخطيطي للخيط الرئيسي وقائمة انتظار المهمة.
طالما أن الخيط الرئيسي فارغ ، فسوف يقرأ "قائمة انتظار المهمة". هذه هي الآلية الجارية لجافا سكريبت. سيتم تكرار هذه العملية بشكل مستمر.
3. الأحداث ووظائف رد الاتصال
"قائمة انتظار المهمة" هي في الأساس قائمة انتظار للأحداث (يُفهم أيضًا على أنها قائمة انتظار للرسائل). عندما يكمل جهاز IO المهمة ، فإنه يضيف حدثًا إلى "قائمة انتظار المهمة" ، مما يشير إلى أن المهام غير المتزامنة ذات الصلة يمكنها إدخال "مكدس التنفيذ". يقرأ الخيط الرئيسي "قائمة انتظار المهمة" ، مما يعني قراءة الأحداث الموجودة في الداخل.
تتضمن الأحداث في "قائمة انتظار المهام" الأحداث بالإضافة إلى الأحداث من أجهزة IO ، ولكن أيضًا الأحداث التي أنشأها المستخدمون (مثل نقرات الماوس ، تمرير الصفحة ، إلخ). طالما تم تحديد وظيفة رد الاتصال ، ستدخل هذه الأحداث "قائمة انتظار المهمة" عند حدوثها وانتظر قراءتها.
ما يسمى "رد الاتصال" هو الرمز الذي سيتم تعليقه بواسطة الخيط الرئيسي. يجب أن تحدد المهام غير المتزامنة وظيفة رد الاتصال. عندما تعود المهمة غير المتزامنة من "قائمة انتظار المهمة" إلى مكدس التنفيذ ، سيتم تنفيذ وظيفة رد الاتصال.
"قائمة انتظار المهمة" هي بنية بيانات الأولى في الأول ، مع الأحداث التي يتم تصنيفها أولاً ويفضل العودة إلى الخيط الرئيسي. عملية قراءة الخيط الرئيسي تلقائي في الأساس. طالما تم مسح مكدس التنفيذ ، سيعود الحدث الأول في "قائمة انتظار المهمة" تلقائيًا إلى مؤشر الترابط الرئيسي. ومع ذلك ، نظرًا لوظيفة "المؤقت" المذكورة لاحقًا ، يحتاج الخيط الرئيسي إلى التحقق من وقت التنفيذ ، ويجب أن تعود بعض الأحداث إلى الخيط الرئيسي في الوقت المحدد.
4. حلقة الحدث
يقرأ الخيط الرئيسي الأحداث من "قائمة انتظار المهمة". هذه العملية تحلق بشكل مستمر ، وبالتالي فإن آلية الجري بأكملها تسمى أيضًا حلقة الحدث.
لفهم حلقة الحدث بشكل أفضل ، يرجى الاطلاع على الصورة أدناه (مقتبس من خطاب فيليب روبرتس "المساعدة ، أنا عالق في حلقة الحدث").
في الشكل أعلاه ، عند تشغيل الخيط الرئيسي ، فإنه يولد كومة ومكدس. يستدعي الرمز الموجود في المكدس مختلف واجهات برمجة التطبيقات الخارجية ، والتي تضيف أحداثًا مختلفة (انقر ، تحميل ، تم القيام بها) إلى "قائمة انتظار المهمة". طالما تم تنفيذ الرمز الموجود في المكدس ، سيقرأ مؤشر الترابط الرئيسي "قائمة انتظار المهمة" وتنفيذ وظائف رد الاتصال المقابلة لتلك الأحداث بدورها.
قم بتنفيذ الرمز في المكدس ، ويتم تنفيذه دائمًا قبل قراءة "قائمة انتظار المهمة". يرجى الاطلاع على المثال التالي.
نسخة الكود كما يلي:
var req = new xmlhttprequest () ؛
req.open ('get' ، url) ؛
req.onload = function () {} ؛
req.onerror = function () {} ؛
req.send () ؛
طريقة req.send في الكود أعلاه هي عملية AJAX لإرسال البيانات إلى الخادم. إنها مهمة غير متزامنة ، مما يعني أن النظام سيقرأ "قائمة انتظار المهمة" فقط بعد تنفيذ كل الرمز في البرنامج النصي الحالي. لذلك ، فإنه يعادل طريقة الكتابة التالية.
نسخة الكود كما يلي:
var req = new xmlhttprequest () ؛
req.open ('get' ، url) ؛
req.send () ؛
req.onload = function () {} ؛
req.onerror = function () {} ؛
بمعنى أن أجزاء وظيفة رد الاتصال المحددة (Onload و Onerror) ليست مهمة قبل أو بعد طريقة Send () ، لأنها جزء من مكدس التنفيذ ، وسيقوم النظام دائمًا بتنفيذها قبل قراءة "قائمة انتظار المهمة".
5. مؤقت
بالإضافة إلى وضع المهام غير المتزامنة ، فإن "قائمة انتظار المهمة" لها أيضًا وظيفة أخرى ، وهي وضع أحداث توقيت ، أي تحديد المدة التي سيتم تنفيذها بعد. وهذا ما يسمى وظيفة "المؤقت" ، وهي الرمز الذي تم تنفيذه بانتظام.
يتم إكمال وظيفة المؤقت بشكل أساسي من خلال وظيفتين: setTimeOut () و setInterval (). آليات الجري الداخلية هي نفسها بالضبط. الفرق هو أن الرمز الذي يحدده الأول يتم تنفيذه في وقت واحد ، بينما يتم تنفيذ الأخير مرارًا وتكرارًا. ما يلي يناقش بشكل أساسي setTimeOut ().
يقبل SetTimeOut () معلمتين ، الأول هو وظيفة رد الاتصال ، والثاني هو عدد ميلي ثانية لتأجيل التنفيذ.
نسخة الكود كما يلي:
console.log (1) ؛
setTimeOut (function () {console.log (2) ؛} ، 1000) ؛
console.log (3) ؛
نتيجة التنفيذ للرمز أعلاه هي 1 ، 3 ، 2 ، لأن setTimeOut () تؤخر السطر الثاني حتى بعد 1000 مللي ثانية.
إذا تم تعيين المعلمة الثانية لـ setTimeOut () على 0 ، فهذا يعني أنه يتم تنفيذ وظيفة رد الاتصال المحدد (الفاصل الزمني 0 مللي ثانية) مباشرة بعد تنفيذ الكود الحالي (تم مسح مكدس التنفيذ).
نسخة الكود كما يلي:
setTimeOut (function () {console.log (1) ؛} ، 0) ؛
console.log (2) ؛
تكون نتائج تنفيذ الكود أعلاه دائمًا 2 و 1 ، لأن النظام سيقوم بتنفيذ وظيفة رد الاتصال في "قائمة انتظار المهمة" فقط بعد تنفيذ السطر الثاني.
يحدد معيار HTML5 أن الحد الأدنى لقيمة (أقصر فاصل) للمعلمة الثانية من setTimeOut () يجب ألا يقل عن 4 مللي ثانية. إذا كانت أقل من هذه القيمة ، فستزيد تلقائيًا. قبل ذلك ، حددت المتصفحات القديمة الحد الأدنى للفاصل الزمني إلى 10 مللي ثانية.
بالإضافة إلى ذلك ، بالنسبة لتلك التغييرات DOM (خاصة الأجزاء التي تتضمن إعادة تقديم الصفحة) ، لا يتم تنفيذها عادةً على الفور ، ولكن كل 16 ميلي ثانية. في هذا الوقت ، يكون تأثير استخدام requestAnimationFrame () أفضل من setTimeOut ().
تجدر الإشارة إلى أن SetTimeOut () فقط إدراج الحدث في "قائمة انتظار المهمة". يجب أن تنتظر حتى يتم تنفيذ الكود الحالي (مكدس التنفيذ) قبل تنفيذ مؤشر الترابط الرئيسي وظيفة رد الاتصال الذي يحدده. إذا استغرق الرمز الحالي وقتًا طويلاً ، فقد يستغرق الانتظار وقتًا طويلاً ، لذلك ليس هناك ما يضمن تنفيذ وظيفة رد الاتصال في الوقت المحدد بواسطة SetTimeOut ().
6. حلقة حدث Node.js
Node.js هي أيضًا حلقة أحداث واحدة ، لكن آلية الجري تختلف عن تلك في بيئة المتصفح.
يرجى الاطلاع على الرسم البياني أدناه (مؤلف busyrich).
وفقًا للشكل أعلاه ، فإن آلية التشغيل لـ Node.js هي كما يلي.
(1) محرك V8 يخفف نصوص JavaScript.
(2) رمز التحليل يستدعي API العقدة.
(3) مكتبة Libuv مسؤولة عن تنفيذ API العقدة. يقوم بتعيين مهام مختلفة لمؤشرات الترابط المختلفة ، ويشكل حلقة حدث ، ويعيد نتائج تنفيذ المهمة إلى محرك V8 بطريقة غير متزامنة.
(4) محرك V8 يعيد النتيجة إلى المستخدم.
بالإضافة إلى الطريقتين setTimeOut و SetInterval ، توفر Node.js أيضًا طريقتين أخريين مرتبطين بـ "قائمة انتظار المهمة": Process.NextTick و SetImmediate. يمكنهم مساعدتنا في تعميق فهمنا لـ "قوائم انتظار المهام".
يمكن أن تؤدي طريقة Process.NextTick إلى تشغيل وظيفة رد الاتصال في نهاية "مكدس التنفيذ" الحالي قبل أن يقرأ مؤشر الترابط الرئيسي "قائمة انتظار المهمة" في المرة القادمة. أي أن المهام التي تحددها تحدث دائمًا قبل كل المهام غير المتزامنة. تؤدي طريقة SetImmediate إلى تشغيل وظيفة رد الاتصال في ذيل "قائمة انتظار المهمة" الحالية ، أي أن المهمة التي تحددها يتم تنفيذها دائمًا في المرة التالية التي يقرأ فيها مؤشر الترابط الرئيسي "قائمة انتظار المهمة" ، والتي تشبه إلى حد كبير setTimeOut (FN ، 0). يرجى الاطلاع على المثال التالي (عبر stackoverflow).
نسخة الكود كما يلي:
Process.NextTick (الوظيفة A () {
console.log (1) ؛
process.nextTick (function b () {console.log (2) ؛}) ؛
}) ؛
setTimeout (timeout timeout () {
console.log ('timeout fired') ؛
} ، 0)
// 1
// 2
// مهلة أطلقت
في الكود أعلاه ، نظرًا لأن وظيفة رد الاتصال المحددة بواسطة Process.NextTick يتم تشغيلها دائمًا في ذيل "مكدس التنفيذ" الحالي ، وليس فقط يتم تنفيذ الوظيفة A أولاً من مهلة وظيفة رد الاتصال المحددة بواسطة SetTimeOut ، ولكن يتم تنفيذ الوظيفة B أولاً من الوقت. هذا يعني أنه إذا كان هناك عمليات متعددة. بيانات nexttick (بغض النظر عما إذا كانت متداخلة أم لا) ، فسيتم تنفيذها جميعًا على "كومة التنفيذ" الحالية.
الآن ، دعونا ننظر إلى setImmediate.
نسخة الكود كما يلي:
setImMediate (الوظيفة A () {
console.log (1) ؛
setImMediate (function b () {console.log (2) ؛}) ؛
}) ؛
setTimeout (timeout timeout () {
console.log ('timeout fired') ؛
} ، 0)
// 1
// مهلة أطلقت
// 2
في الكود أعلاه ، هناك نوعان من setimmediats. تحدد SetImmediate الأول أن وظيفة رد الاتصال A يتم تشغيلها في ذيل "قائمة انتظار المهمة" الحالية ("حلقة الحدث" التالية) ؛ بعد ذلك ، يحدد SetTimeout أيضًا أن مهلة وظيفة رد الاتصال يتم تشغيلها في ذيل "قائمة انتظار المهمة" الحالية ، لذلك في نتيجة الإخراج ، يتم تصنيف المهلة التي تم إطلاقها خلف 1. أما بالنسبة للترتيب 2 وراء المهلة التي تم إطلاقها ، فذلك لأن ميزة أخرى مهمة من setImmediate: "حلقة حدث" لا يمكن إلا أن تؤدي إلى وظيفة في الاتصال المحدد بواسطة setimdemediate.
لقد حصلنا على اختلاف مهم من هذا: عملية متعددة. يتم تنفيذ عبارات NextTick دائمًا في وقت واحد ، بينما تتطلب إجراءات SetImmediats عدة مرات تنفيذها عدة مرات. في الواقع ، هذا هو بالضبط السبب في أن Node.js الإصدار 10.0 يضيف طريقة setImmediate. خلاف ذلك ، فإن الدعوة المتكررة للمعالجة. nexttick مثل ما يلي ستكون لا نهاية لها ، ولن يقرأ الخيط الرئيسي "قائمة انتظار الحدث" على الإطلاق!
نسخة الكود كما يلي:
Process.NextTick (وظيفة foo () {
Process.NextTick (FOO) ؛
}) ؛
في الواقع ، الآن إذا كتبت عملية عودية. nexttick ، ستقوم Node.js بتحذير يطلب منك التغيير إلى setImmediate.
بالإضافة إلى ذلك ، نظرًا لأن وظيفة رد الاتصال المحددة بواسطة Process.NextTick يتم تشغيلها في "حلقة الحدث" ويحدد SetImmediate أنه يتم تشغيله في "حلقة الحدث" التالية ، فمن الواضح أن الأول يحدث دائمًا في وقت مبكر من الأخير وهو أيضًا أكثر كفاءة في التنفيذ (لأنه لا توجد حاجة إلى التحقق من "قائمة انتظار المهمة").