دليل Node.js ، الجزء 6: حلقة الأحداث ، Call Stack ، Timers

اليوم ، في الجزء السادس من ترجمة دليل Node.js ، سنتحدث عن حلقة الحدث ، وكومة المكالمة ، ووظيفة process.nextTick() ، والمؤقتات. يعتبر فهم هذه الآليات وغيرها من آليات Node.js أحد الأركان الأساسية لتطوير التطبيقات الناجح لهذا النظام الأساسي.




حلقة الحدث


إذا كنت ترغب في فهم كيفية تنفيذ تعليمات JavaScript البرمجية ، فإن Event Loop هي واحدة من أهم المفاهيم التي تحتاج إلى فهمها. سنتحدث هنا عن كيفية عمل JavaScript في وضع الخيوط الفردية ، وكيف يتم التعامل مع الوظائف غير المتزامنة.

لقد كنت أقوم بتطوير جافا سكريبت لسنوات عديدة ، ولكن لا يمكنني القول أنني فهمت تمامًا كيف يعمل كل شيء ، إذا جاز التعبير ، "تحت الغطاء". قد لا يكون المبرمج على دراية بتعقيدات جهاز الأنظمة الفرعية الداخلية للبيئة التي يعمل فيها. ولكن من المفيد عادةً أن تكون لديك فكرة عامة على الأقل عن مثل هذه الأشياء.

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

في الواقع ، يحتاج مبرمج JS إلى الانتباه فقط إلى الإجراءات التي تؤديها شفرته بالضبط ، ومحاولة تجنب المواقف التي تتسبب في حظر سلسلة المحادثات الرئيسية. على سبيل المثال - إجراء مكالمات الشبكة في وضع متزامن ودورات لا نهاية لها.

عادةً ، يكون للمتصفحات ، في كل علامة تبويب مفتوحة ، حلقة حدث خاصة بها. يسمح لك هذا بتنفيذ التعليمات البرمجية لكل صفحة في بيئة منعزلة وتجنب المواقف عندما تكون صفحة معينة ، في الشفرة التي بها حلقة لا نهائية أو حسابات ثقيلة ، قادرة على "تعليق" المتصفح بأكمله. يدعم المتصفح عمل العديد من حلقات الأحداث الموجودة في وقت واحد ، والتي يتم استخدامها ، على سبيل المثال ، لمعالجة المكالمات إلى واجهات برمجة تطبيقات مختلفة. بالإضافة إلى ذلك ، يتم استخدام حلقة حدث ملكية لدعم عمال الويب .

أهم شيء يجب أن يتذكره مبرمج جافا سكريبت باستمرار هو أن شفرته تستخدم حلقة الحدث الخاصة بها ، لذلك يجب كتابة الشفرة بحيث لا يتم حظر حلقة الحدث هذه.

قفل حلقة الحدث


أي شفرة JavaScript تستغرق وقتًا طويلاً للتنفيذ ، أي رمز لا يتحكم في حلقة الحدث لفترة طويلة جدًا ، يمنع تنفيذ أي رمز صفحة آخر. هذا يؤدي إلى منع معالجة أحداث واجهة المستخدم ، وهو ما ينعكس في حقيقة أنه لا يمكن للمستخدم التفاعل مع عناصر الصفحة والعمل بشكل طبيعي معها ، على سبيل المثال ، التمرير.

تقريبا كل آليات جافا سكريبت I / O الأساسية لا تمنع. ينطبق هذا على كل من المتصفح و Node.js. من بين هذه الآليات ، على سبيل المثال ، يمكننا أن نذكر الأدوات اللازمة لأداء طلبات الشبكة المستخدمة في كل من بيئتي العميل والخادم ، وأدوات للعمل مع ملفات Node.js. هناك طرق متزامنة لأداء مثل هذه العمليات ، ولكن يتم استخدامها فقط في حالات خاصة. هذا هو السبب في أن عمليات الاسترداد التقليدية والآليات الأحدث - الوعود والبنية غير المتزامنة / التي تنتظر - لها أهمية كبيرة في جافا سكريبت.

مكدس الاستدعاء


يعتمد JavaScript Call Stack على مبدأ LIFO (Last In ، First Out - Last In ، First Out). تتحقق حلقة الأحداث باستمرار من مكدس الاستدعاءات لمعرفة ما إذا كانت تحتوي على دالة يجب تنفيذها. إذا تم استدعاء دالة ، عند تنفيذ الكود ، تتم إضافة معلومات عنها إلى مكدس الاستدعاء ويتم تنفيذ هذه الوظيفة.

حتى إذا لم تكن مهتمًا بمفهوم "مكدس الاستدعاءات" حتى قبل ذلك ، إذا واجهت رسائل خطأ تتضمن تتبع مكدس ، فإنك تتخيل بالفعل ما يبدو عليه. هنا ، على سبيل المثال ، يبدو هذا في متصفح.


رسالة خطأ المتصفح

يقوم المتصفح ، عند حدوث خطأ ، بالإبلاغ عن تسلسل المكالمات إلى الوظائف ، والمعلومات التي يتم تخزينها في مكدس المكالمات ، والتي تسمح لك بالعثور على مصدر الخطأ وفهم أي المكالمات التي تؤدي إلى الوظائف التي أدت إلى الموقف.

الآن بعد أن تحدثنا عن حلقة الحدث ومكدس المكالمة بعبارات عامة ، ضع في اعتبارك مثالاً يوضح تنفيذ جزء التعليمات البرمجية وكيف تبدو هذه العملية من حيث حلقة الحدث ومكدس المكالمة.

حلقة الحدث و Call Stack


إليك الشفرة التي سنجربها:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') bar() baz() } foo() 

إذا تم تنفيذ هذا الرمز ، فسيصل ما يلي إلى وحدة التحكم:

 foo bar baz 

ومن المتوقع تماما مثل هذه النتيجة. وبالتحديد ، عند تشغيل هذا الرمز ، يتم استدعاء وظيفة foo() أولاً. داخل هذه الوظيفة ، نسمي أولاً الدالة bar() ، ثم الدالة baz() . في الوقت نفسه ، يخضع مكدس الاستدعاء أثناء تنفيذ هذا الرمز للتغييرات الموضحة في الشكل التالي.


تغيير حالة مكدس المكالمة عند تنفيذ التعليمات البرمجية

تتحقق حلقة الحدث ، في كل تكرار ، لمعرفة ما إذا كان هناك أي شيء في مكدس المكالمة ، وإذا كان الأمر كذلك ، فإنها تفعل ذلك حتى فارغة مكدس الاستدعاء.


تكرار حلقة الحدث

طابور وظيفة


يبدو المثال أعلاه عاديًا تمامًا ، ولا يوجد شيء خاص به: تعثر جافا سكريبت على الشفرة التي يجب تنفيذها وتنفيذها بالترتيب. سنتحدث عن كيفية تأجيل تنفيذ الوظائف حتى يتم مسح مكدس الاستدعاء. للقيام بذلك ، يتم استخدام البناء التالي:

 setTimeout(() => {}), 0) 

يسمح لك بتنفيذ الوظيفة التي تم تمريرها إلى وظيفة setTimeout() بعد تنفيذ جميع الوظائف الأخرى التي يتم استدعاؤها في رمز البرنامج.

فكر في مثال:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) baz() } foo() 

قد يبدو ما يطبعه هذا الرمز غير متوقع:

 foo baz bar 

عند تشغيل هذا المثال ، يتم استدعاء الدالة foo() أولاً. في ذلك ، نسمي setTimeout() ، بتمرير هذه الدالة ، كالوسيطة الأولى ، bar . بتمريرها 0 كوسيطة ثانية ، نبلغ النظام بأنه يجب أداء هذه الوظيفة في أقرب وقت ممكن. ثم نسمي وظيفة baz() .

هذه هي الطريقة التي سيظهر بها مكدس المكالمة الآن.


تغيير حالة مكدس المكالمة عند تنفيذ الرمز قيد التحقيق

هذا هو الترتيب الذي سيتم فيه تنفيذ الوظائف في برنامجنا الآن.


تكرار حلقة الحدث

لماذا يحدث هذا بهذه الطريقة؟

قائمة انتظار الأحداث


عندما يتم استدعاء وظيفة setTimeout() ، يبدأ المستعرض أو النظام الأساسي Node.js جهاز ضبط الوقت. بعد عمل المؤقت (في حالتنا ، يحدث هذا على الفور ، نظرًا لأننا قمنا بتعيينه على 0) ، تدخل وظيفة رد الاتصال التي تم تمريرها إلى setTimeout() في قائمة انتظار الأحداث.

تتضمن قائمة انتظار الأحداث ، عندما يتعلق الأمر بالمتصفح ، الأحداث التي بدأها المستخدم - الأحداث التي تسببها نقرات الماوس على عناصر الصفحة ، والأحداث التي يتم تشغيلها عند إدخال البيانات من لوحة المفاتيح. توجد معالجات onload DOM مثل onload ، والوظائف التي يتم استدعاؤها عند تلقي إجابات للطلبات غير المتزامنة لتحميل البيانات ، على الفور. هنا ينتظرون دورهم للعملية.

تعطي حلقة الحدث أولوية لما هو موجود في مكدس المكالمة. أولاً ، يقوم بكل شيء يمكنه العثور عليه على المكدس ، وبعد أن يكون المكدس فارغًا ، فإنه يبدأ في معالجة ما هو موجود في قائمة انتظار الحدث.

لا نحتاج إلى الانتظار حتى تنتهي وظيفة مثل setTimeout() ، نظرًا لأن وظائف مماثلة يتم توفيرها بواسطة المتصفح ويستخدمون مجموعات البث الخاصة بهم. لذا ، على سبيل المثال ، عند تعيين المؤقت لمدة ثانيتين باستخدام وظيفة setTimeout() ، يجب ألا تنتظر ، بعد إيقاف تنفيذ رمز آخر ، ثانيتين ، لأن المؤقت يعمل خارج الكود.

قائمة انتظار مهمة ES6


قدم ECMAScript 2015 (ES6) مفهوم Job Queue ، والذي يستخدمه الوعود (ظهرت أيضًا في ES6). بفضل قائمة انتظار المهام ، يمكن استخدام نتيجة تنفيذ الوظيفة غير المتزامنة في أسرع وقت ممكن ، دون الحاجة إلى الانتظار حتى يتم مسح مكدس الاستدعاءات.

إذا تم حل الوعد قبل نهاية الوظيفة الحالية ، فسيتم تنفيذ الشفرة المقابلة فور اكتمال الوظيفة الحالية.

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

خذ بعين الاعتبار المثال التالي:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) new Promise((resolve, reject) =>   resolve('should be right after baz, before bar') ).then(resolve => console.log(resolve)) baz() } foo() 

إليك ما سيتم إنتاجه بعد تنفيذه:

 foo baz should be right after baz, before bar bar 

يوضح ما يمكنك رؤيته هنا اختلافًا خطيرًا بين الوعود (والبنية غير المتزامنة / المنتظرة ، والتي تستند إليها) والوظائف غير المتزامنة التقليدية ، والتي يتم تنظيم تنفيذها باستخدام setTimeout() أو واجهات برمجة التطبيقات الأخرى للنظام الأساسي المستخدم.

process.nextTick ()


يتفاعل الأسلوب process.nextTick() مع حلقة الأحداث بطريقة خاصة. القراد هو دورة كاملة واحدة من الأحداث. بتمرير الوظيفة إلى الأسلوب process.nextTick() ، نبلغ النظام أن هذه الوظيفة تحتاج إلى استدعاء بعد اكتمال التكرار الحالي لحلقة الحدث ، قبل بدء المرحلة التالية. يبدو استخدام هذه الطريقة كالتالي:

 process.nextTick(() => { // -  }) 

افترض أن حلقة الحدث مشغولة في تنفيذ التعليمات البرمجية للدالة الحالية. عند اكتمال هذه العملية ، سيقوم مشغل JavaScript بتنفيذ جميع الوظائف التي تم تمريرها إلى process.nextTick() أثناء العملية السابقة. باستخدام هذه الآلية ، نسعى جاهدين لضمان تنفيذ وظيفة معينة بشكل غير متزامن (بعد الوظيفة الحالية) ، ولكن في أقرب وقت ممكن ، دون وضعها في قائمة الانتظار.

على سبيل المثال ، إذا كنت تستخدم بناء setTimeout(() => {}, 0) ، فسيتم تنفيذ الوظيفة في التكرار التالي لحلقة الحدث ، أي بعد وقت process.nextTick() جدًا من استخدام process.nextTick() في نفس الموقف. يجب استخدام هذه الطريقة عندما يكون ذلك ضروريًا لضمان تنفيذ بعض التعليمات البرمجية في بداية التكرار التالي لحلقة الحدث.

مجموعة فورية ()


وظيفة أخرى توفرها Node.js لتنفيذ التعليمات البرمجية غير المتزامنة هي setImmediate() . إليك كيفية استخدامه:

 setImmediate(() => { //   }) 

سيتم تنفيذ وظيفة رد الاتصال التي تم تمريرها إلى setImmediate() في التكرار التالي لحلقة الحدث.

كيف يختلف setImmediate() عن setTimeout(() => {}, 0) (اي من المؤقت الذي يجب ان يعمل في اقرب وقت ممكن) ومن process.nextTick() ؟

سيتم تنفيذ الوظيفة التي تم تمريرها إلى process.nextTick() بعد اكتمال التكرار الحالي لحلقة الحدث. بمعنى ، سيتم تنفيذ هذه الوظيفة دائمًا قبل الوظيفة التي تتم جدولة تنفيذها باستخدام setTimeout() أو setImmediate() .

استدعاء دالة setTimeout() بتأخير مجموعة 0 مللي ثانية يشبه إلى حد كبير استدعاء setImmediate() . يعتمد ترتيب تنفيذ الوظائف المحولة إليها على عوامل مختلفة ، ولكن في كلتا الحالتين سيتم استدعاء عمليات الاسترداد في التكرار التالي لحلقة الحدث.

المؤقتات


لقد تحدثنا بالفعل عن وظيفة setTimeout() ، والتي تسمح لك بجدولة المكالمات إلى عمليات الاسترداد التي تم تمريرها إليها. لنأخذ بعض الوقت لوصف ميزاته بمزيد من التفصيل والنظر في وظيفة أخرى ، setInterval() ، مشابهة لها. في Node.js ، يتم تضمين وظائف العمل مع المؤقتات في وحدة المؤقت ، ولكن يمكنك استخدامها دون توصيل هذه الوحدة النمطية في التعليمات البرمجية ، لأنها عامة.

▍ تعيين وظيفة Timeout ()


تذكر أنه عند استدعاء وظيفة setTimeout() ، فإنها تتلقى رد اتصال ووقت بالمللي ثانية ، وبعد ذلك سيتم استدعاء رد الاتصال. فكر في مثال:

 setTimeout(() => { //   2  }, 2000) setTimeout(() => { //   50  }, 50) 

هنا نقوم بتمرير setTimeout() وظيفة جديدة موصوفة على الفور ، ولكن هنا يمكننا استخدام الدالة الموجودة بتمرير اسم setTimeout() ومجموعة من المعلمات لتشغيلها. يبدو هذا:

 const myFunction = (firstParam, secondParam) => { //   } //   2  setTimeout(myFunction, 2000, firstParam, secondParam) 

تُرجع الدالة setTimeout() معرّف جهاز ضبط الوقت. عادة لا يتم استخدامه ، ولكن يمكنك حفظه ، وإذا لزم الأمر ، قم بحذف المؤقت إذا لم تعد هناك حاجة لرد الاتصال المجدول:

 const id = setTimeout(() => { //      2  }, 2000) //  ,       clearTimeout(id) 

delay تأخير صفر


في الأقسام السابقة ، استخدمنا setTimeout() ، setTimeout() ، setTimeout() الذي يلزم بعده استدعاء رد الاتصال ، 0 . هذا يعني أنه سيتم استدعاء رد الاتصال في أقرب وقت ممكن ، ولكن بعد الانتهاء من الوظيفة الحالية:

 setTimeout(() => { console.log('after ') }, 0) console.log(' before ') 

سيخرج هذا الرمز ما يلي:

 before after 

هذه التقنية مفيدة بشكل خاص في المواقف التي لا أريد فيها ، عند تنفيذ مهام حسابية ثقيلة ، حظر مؤشر الترابط الرئيسي ، مما يسمح بتنفيذ وظائف أخرى ، وتقسيم هذه المهام إلى عدة مراحل ، وتنفيذها setTimeout() .

إذا setImmediate() وظيفة setImmediate() أعلاه ، فهي قياسية في Node.js ، والتي لا يمكن قولها عن المتصفحات (يتم تنفيذها في IE و Edge ، ولكن ليس في الآخرين).

▍ تعيين وظيفة Interval ()


تشبه الدالة setInterval() وظيفة setTimeout() ، ولكن هناك اختلافات بينهما. بدلاً من تنفيذ رد الاتصال الذي تم تمريره إليها مرة واحدة ، سيقوم setInterval() بشكل دوري ، مع الفاصل الزمني المحدد ، باستدعاء رد الاتصال هذا. سيستمر هذا ، بشكل مثالي ، حتى اللحظة التي يتوقف فيها المبرمج صراحة عن هذه العملية. إليك كيفية استخدام هذه الميزة:

 setInterval(() => { //   2  }, 2000) 

سيتم استدعاء رد اتصال تم تمريره إلى الوظيفة الموضحة أعلاه كل ثانيتين. لتوفير إمكانية إيقاف هذه العملية ، تحتاج إلى إرجاع معرّف المؤقت بواسطة setInterval() واستخدام الأمر clearInterval() :

 const id = setInterval(() => { //   2  }, 2000) clearInterval(id) 

من الأساليب الشائعة استدعاء clearInterval() داخل رد الاتصال الذي تم تمريره إلى setInterval() عند استيفاء شرط معين. على سبيل المثال ، سيتم تشغيل التعليمات البرمجية التالية بشكل دوري حتى يتم App.somethingIWait الخاصية App.somethingIWait للوصول:

 const interval = setInterval(function() { if (App.somethingIWait === 'arrived') {   clearInterval(interval)   //    -  ,   -    } }, 100) 

setting تعيين الإعداد العودي TimeTimeout ()


ستقوم دالة setInterval() باستدعاء رد الاتصال الذي تم تمريره إليها كل n مللي ثانية ، دون القلق بشأن ما إذا كان هذا الاستدعاء قد اكتمل بعد المكالمة السابقة.

إذا كانت كل مكالمة إلى هذا الاستدعاء تتطلب دائمًا نفس الوقت أقل من n ، فلا تنشأ مشاكل هنا.


يتم استدعاء رد الاتصال بشكل دوري ، تستغرق كل جلسة تنفيذية نفس الوقت ، وتقع ضمن الفاصل الزمني بين المكالمات

ربما يستغرق الأمر وقتًا مختلفًا لإكمال رد اتصال ، والذي لا يزال أقل من n . على سبيل المثال ، إذا كنا نتحدث عن إجراء عمليات معينة على الشبكة ، فإن هذا الموقف متوقع تمامًا.


يتم استدعاء رد الاتصال بشكل دوري ، كل جلسة تنفيذ تستغرق وقتًا مختلفًا ، وتقع بين المكالمات

عند استخدام setInterval() ، قد ينشأ موقف عندما يستغرق رد الاتصال أكثر من n ، مما يؤدي إلى إكمال المكالمة التالية قبل إتمام المكالمة السابقة.


يتم استدعاء رد الاتصال بشكل دوري ، تستغرق كل جلسة وقتًا مختلفًا ، والذي لا يتناسب أحيانًا مع الفاصل الزمني بين المكالمات

لتجنب هذا الموقف ، يمكنك استخدام تقنية إعداد عداد الوقت setTimeout() باستخدام setTimeout() . النقطة هي أن مكالمة الاستدعاء التالية مخطط لها بعد الانتهاء من المكالمة السابقة:

 const myFunction = () => { //    setTimeout(myFunction, 1000) } setTimeout( myFunction() }, 1000) 

باستخدام هذا النهج ، يمكن تنفيذ السيناريو التالي:


استدعاء عودي إلى setTimeout () لجدولة تنفيذ رد الاتصال

الملخص


تحدثنا اليوم عن الآليات الداخلية لـ Node.js ، مثل حلقة الحدث ، مكدس الاستدعاء ، وناقشنا العمل مع المؤقتات التي تسمح لك بجدولة تنفيذ التعليمات البرمجية. في المرة القادمة سوف نتعمق في موضوع البرمجة غير المتزامنة.

أعزائي القراء! هل واجهت مواقف عندما كان عليك استخدام process.nextTick ()؟

Source: https://habr.com/ru/post/ar424553/


All Articles