مؤقتات JavaScript: كل ما تريد معرفته

مرحبا زملائي. ذات مرة في حبري ، تم تأليف مقال تحت تأليف جون ريزيج حول هذا الموضوع. لقد مرت 10 سنوات ، ولا يزال الموضوع بحاجة إلى توضيح. لذلك ، نقدم للمهتمين قراءة المقالة بقلم Samer Buna ، والتي لا تقدم نظرة عامة نظرية للمؤقتات في JavaScript (في سياق Node.js) فحسب ، بل أيضًا مهام عليها.




قبل بضعة أسابيع غردت عن السؤال التالي من مقابلة واحدة:

"أين هو رمز المصدر لوظائف setTimeout و setInterval؟ أين تبحث عنه؟ لا يمكنك البحث عنه :) "

*** أجب عنه بنفسك ، ثم اقرأ على ***



حوالي نصف الردود على هذه التغريدة كانت خاطئة. لا ، القضية ليست ذات صلة بـ V8 (أو VMs أخرى) !!! وظائف مثل setTimeout و setInterval ، تسمى بفخر JavaScript JavaScript Timers ، ليست جزءًا من أي مواصفات ECMAScript أو تطبيق محرك JavaScript. يتم تنفيذ وظائف المؤقت على مستوى المتصفح ، لذلك يختلف تنفيذها في المتصفحات المختلفة. يتم تنفيذ المؤقتات أيضًا في Node.js. runtime نفسه.

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

في Node ، تعد المؤقتات جزءًا من الكائن العام ، والذي تم تصميمه مثل واجهة المتصفح الخاصة بـ Window . يظهر رمز المصدر للمؤقتات في العقدة هنا .

قد يبدو لشخص ما أن هذا مجرد سؤال سيء من المقابلة - ما فائدة معرفة ذلك ؟! أنا ، كمطور جافا سكريبت ، أفكر بهذه الطريقة: من المفترض أنه يجب أن تعرف ذلك ، لأن العكس قد يشير إلى أنك لا تفهم تمامًا كيفية تفاعل V8 (والأجهزة الظاهرية الأخرى) مع المتصفحات والعقدة.

دعونا نلقي نظرة على بعض الأمثلة ونحل بعض المهام المؤقتة ، دعنا؟

يمكنك استخدام أمر العقدة لتشغيل الأمثلة في هذه المقالة. ظهرت معظم الأمثلة التي تمت مناقشتها هنا في دورة " البدء مع Node.js" حول Pluralsight.

تنفيذ وظيفة مؤجلة

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

فيما يلي مثال على التنفيذ المؤجل:

 // example1.js setTimeout( () => { console.log('Hello after 4 seconds'); }, 4 * 1000 ); 

في هذا المثال ، باستخدام setTimeout تتأخر رسالة الترحيب لمدة 4 ثوانٍ. الوسيطة الثانية ل setTimeout هي التأخير (بالمللي ثانية). أضرب 4 في 1000 لتحصل على 4 ثوان.

الوسيطة الأولى لـ setTimeout هي وظيفة سيتم تأجيل تنفيذها.
إذا قمت بتنفيذ ملف example1.js باستخدام أمر العقدة ، ستتوقف العقدة مؤقتًا لمدة 4 ثوانٍ ثم تعرض رسالة ترحيب (متبوعة بخروج).

يرجى ملاحظة: الوسيطة الأولى setTimeout هي مجرد مرجع دالة . لا يجب أن تكون دالة example1.js - مثل example1.js . هذا هو نفس المثال دون استخدام الوظيفة المضمنة:

 const func = () => { console.log('Hello after 4 seconds'); }; setTimeout(func, 4 * 1000); 

تمرير الحجج

إذا كانت الدالة التي تُستخدم لها setTimeout لتأخير تقبل أي وسيطات ، فيمكنك استخدام الوسيطات المتبقية للدالة setTimeout نفسها (بعد 2 التي درسناها بالفعل) لنقل قيم الوسيطات إلى الدالة المؤجلة.

 // : func(arg1, arg2, arg3, ...) //  : setTimeout(func, delay, arg1, arg2, arg3, ...) 

هنا مثال:

 // example2.js const rocks = who => { console.log(who + ' rocks'); }; setTimeout(rocks, 2 * 1000, 'Node.js'); 

تأخذ وظيفة rocks المذكورة أعلاه ، التي تأخرت لمدة ثانيتين ، الوسيطة who ، واستدعاء setTimeout يمررها إلى القيمة "Node.js" مثل الوسيطة who .

عند تنفيذ example2.js باستخدام الأمر node ، سيتم عرض العبارة "Node.js rocks" بعد ثانيتين.

مهمة المؤقتات # 1

لذلك ، استنادًا إلى المادة التي تم دراستها بالفعل حول setTimeout ، setTimeout رسالتين التاليتين بعد التأخير المقابل.

  • يتم عرض رسالة "مرحبًا بعد 4 ثوانٍ" بعد 4 ثوانٍ.
  • يتم عرض رسالة "مرحبًا بعد 8 ثوانٍ" بعد 8 ثوانٍ.

حدود

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

الحل

إليك كيفية حل هذه المشكلة:

 // solution1.js const theOneFunc = delay => { console.log('Hello after ' + delay + ' seconds'); }; setTimeout(theOneFunc, 4 * 1000, 4); setTimeout(theOneFunc, 8 * 1000, 8); 

بالنسبة لي ، يتلقى theOneFunc وسيطة delay ويستخدم قيمة وسيطة delay هذه في الرسالة المعروضة على الشاشة. وبالتالي ، يمكن أن تعرض الوظيفة رسائل مختلفة اعتمادًا على قيمة التأخير التي سنبلغها بها.

ثم استخدمت theOneFunc في theOneFunc setTimeout ، مع تشغيل المكالمة الأولى بعد 4 ثوانٍ والثانية بعد 8 ثوانٍ. يتلقى كل من مكالمات setTimeout هذه أيضًا وسيطة ثالثة ، تمثل وسيطة delay لـ theOneFunc .

من خلال تنفيذ ملف solution1.js باستخدام أمر العقدة ، سنعرض متطلبات المهمة ، وستظهر الرسالة الأولى بعد 4 ثوانٍ ، والثانية بعد 8 ثوانٍ.

كرر الوظيفة

ولكن ماذا لو طلبت منك عرض رسالة كل 4 ثوانٍ لفترة غير محدودة؟
بالطبع ، يمكنك setTimeout في حلقة ، ولكن واجهة برمجة تطبيقات المؤقت تقدم أيضًا وظيفة setInterval ، والتي يمكنك من خلالها برمجة التنفيذ "الأبدي" لأي عملية.

فيما يلي مثال على setInterval :

 // example3.js setInterval( () => console.log('Hello every 3 seconds'), 3000 ); 

سيعرض هذا الرمز رسالة كل 3 ثوانٍ. إذا قمت بتنفيذ example3.js باستخدام الأمر node ، example3.js Node هذا الأمر حتى تقوم example3.js إنهاء العملية (CTRL + C).

إلغاء الموقتات

نظرًا لأنه يتم تعيين إجراء عندما يتم استدعاء وظيفة المؤقت ، يمكن أيضًا التراجع عن هذا الإجراء قبل تنفيذه.

تقوم استدعاء setTimeout بإرجاع معرّف مؤقت ، ويمكنك استخدام معرّف المؤقت عند الاتصال بـ clearTimeout لإلغاء المؤقت. هنا مثال:

 // example4.js const timerId = setTimeout( () => console.log('You will not see this one!'), 0 ); clearTimeout(timerId); 

يجب تشغيل هذا المؤقت البسيط بعد 0 مللي ثانية (أي على الفور) ، ولكن هذا لن يحدث ، لأننا نلتقط قيمة timerId هذا المؤقت فورًا عن طريق استدعاء clearTimeout .

عند تنفيذ example4.js باستخدام أمر node ، لن تطبع العقدة أي شيء - ستنتهي العملية ببساطة على الفور.

بالمناسبة ، يوفر Node.js أيضًا طريقة أخرى setTimeout بقيمة 0 مللي ثانية. هناك وظيفة أخرى في واجهة برمجة تطبيقات جهاز setImmediate الوقت Node.js تسمى setImmediate ، وهي تقوم بشكل أساسي بنفس الشيء مثل setTimeout بقيمة 0 مللي ثانية ، ولكن في هذه الحالة يمكنك حذف التأخير:

 setImmediate( () => console.log('I am equivalent to setTimeout with 0 ms'), ); 

وظيفة setImmediate غير مدعومة في جميع المتصفحات . لا تستخدمه في كود العميل.

إلى جانب clearTimeout هناك وظيفة clearInterval تفعل الشيء نفسه ، ولكن مع مكالمات setInerval ، وهناك أيضًا مكالمة clearImmediate .

تأخير الموقت - شيء غير مضمون

هل لاحظت أنه في المثال السابق ، عند تنفيذ عملية باستخدام setTimeout بعد 0 مللي ثانية ، لا تحدث هذه العملية على الفور (بعد setTimeout ) ، ولكن فقط بعد تنفيذ كل كود البرنامج النصي بالكامل (بما في ذلك استدعاء clearTimeout

دعني أوضح هذه النقطة بمثال. فيما يلي مكالمة setTimeout بسيطة يجب أن تعمل في نصف ثانية - ولكن هذا لا يحدث:

 // example5.js setTimeout( () => console.log('Hello after 0.5 seconds. MAYBE!'), 500, ); for (let i = 0; i < 1e10; i++) { //    } 

مباشرة بعد تحديد المؤقت في هذا المثال ، نقوم بحظر بيئة وقت التشغيل بشكل متزامن مع حلقة كبيرة. قيمة 1e10 هي 1 مع 10 أصفار ، لذا تستمر الدورة 10 مليار دورة معالج (من حيث المبدأ ، هذا يحاكي المعالج الزائد). لا تستطيع العقدة فعل أي شيء حتى تكتمل هذه الحلقة.

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

مشكلة المؤقتات # 2

اكتب برنامجًا نصيًا يعرض رسالة "Hello World" مرة واحدة في الثانية ، ولكن 5 مرات فقط. بعد 5 تكرارات ، يجب أن يعرض البرنامج النصي رسالة "تم" ، وبعد ذلك ستكتمل عملية العقدة.

القيد : عند حل هذه المشكلة ، لا يمكنك استدعاء setTimeout .

تلميح : بحاجة إلى عداد.

الحل

إليك كيفية حل هذه المشكلة:

 let counter = 0; const intervalId = setInterval(() => { console.log('Hello World'); counter += 1; if (counter === 5) { console.log('Done'); clearInterval(intervalId); } }, 1000); 

قمت بتعيين 0 كقيمة أولية counter ، ثم سميت setInterval ، والتي تأخذ معرفها.

ستعرض الوظيفة المؤجلة رسالة ، وفي كل مرة تزيد العداد بمقدار رسالة. داخل الدالة المؤجلة ، لدينا بيان if ، والذي سيتحقق مما إذا كانت 5 تكرارات قد مرت بالفعل. بعد 5 تكرارات ، يعرض البرنامج "تم" ويزيل قيمة الفاصل الزمني باستخدام ثابت intervalId التقاطه. الفاصل الزمني هو 1000 مللي ثانية.

من الذي يدعو بالضبط الوظائف المؤجلة؟

عند استخدام JavaScript this داخل دالة عادية ، مثل هذه على سبيل المثال:

 function whoCalledMe() { console.log('Caller is', this); } 

ستتطابق القيمة في this مع المتصل . إذا قمت بتعريف الوظيفة المذكورة أعلاه داخل عقدة REPL ، فسيطلق عليها الكائن العام. إذا قمت بتحديد وظيفة في وحدة تحكم المستعرض ، window كائن window .

دعنا نحدد دالة كخاصية كائن لجعلها أكثر وضوحا قليلا:

 const obj = { id: '42', whoCalledMe() { console.log('Caller is', this); } }; //     : obj.whoCallMe 

الآن ، عندما نستخدم الرابط الخاص به مباشرةً عند العمل مع وظيفة obj.whoCallMe ، obj.whoCallMe كائن obj (المحدد obj.whoCallMe :



السؤال الآن هو: من سيكون المتصل إذا مررت الرابط إلى obj.whoCallMe setTimetout ؟

 //       ?? setTimeout(obj.whoCalledMe, 0); 

من هو المتصل في هذه الحالة؟

ستختلف الإجابة اعتمادًا على مكان تنفيذ وظيفة المؤقت. في هذه الحالة ، فإن الاعتماد على من هو المتصل غير مقبول ببساطة. ستفقد السيطرة على المتصل ، لأنه سيعتمد على تنفيذ المؤقت الذي في هذه الحالة يستدعي وظيفتك. إذا قمت باختبار هذا الرمز في Node REPL ، فسيكون كائن Timeout هو المتصل:



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

مشكلة المؤقتات # 3

اكتب نصًا سينتج باستمرار رسالة "Hello World" بتأخيرات متفاوتة. ابدأ بتأخير ثانية واحدة ، ثم زدها بمقدار ثانية واحدة عند كل تكرار. في التكرار الثاني ، سيكون التأخير ثانيتين. على الثالث - ثلاثة ، وهلم جرا.

قم بتضمين تأخير في الرسالة المعروضة. يجب أن تحصل على شيء مثل هذا:

Hello World. 1
Hello World. 2
Hello World. 3
...


القيود : لا يمكن تحديد المتغيرات إلا باستخدام const. استخدام let أو var ليس كذلك.

الحل

نظرًا لأن مدة التأخير في هذه المهمة متغيرة ، فلا يمكنك استخدام setInterval هنا ، ولكن يمكنك تكوين التنفيذ الفاصل يدويًا باستخدام setTimeout داخل مكالمة عودية. ستقوم الوظيفة الأولى التي تم تنفيذها باستخدام setTimeout بإنشاء المؤقت التالي ، وهكذا.

بالإضافة إلى ذلك ، نظرًا لأنه لا يمكنك استخدام let / var ، فلا يمكن أن يكون لدينا عداد لزيادة التأخير لكل مكالمة عودية ؛ بدلاً من ذلك ، يمكنك استخدام وسيطات دالة عودية لإجراء زيادة أثناء استدعاء عودي.

إليك كيفية حل هذه المشكلة:

 const greeting = delay => setTimeout(() => { console.log('Hello World. ' + delay); greeting(delay + 1); }, delay * 1000); greeting(1); 

مهمة المؤقتات # 4

اكتب نصًا برمجيًا يعرض رسالة "Hello World" بنفس بنية التأخير كما في المهمة رقم 3 ، ولكن هذه المرة في مجموعات من 5 رسائل ، وستكون للمجموعة فترة تأخير رئيسية. بالنسبة للمجموعة الأولى المكونة من 5 رسائل ، نختار التأخير الأولي البالغ 100 مللي ثانية ، للمدة التالية - 200 مللي ثانية ، للثالثة - 300 مللي ثانية وما إلى ذلك.

إليك كيفية عمل هذا النص البرمجي:

  • عند 100 مللي ثانية ، يعرض البرنامج النصي "Hello World" لأول مرة ، ويفعل ذلك 5 مرات بفاصل زمني يزيد عن 100 مللي ثانية. ستظهر الرسالة الأولى بعد 100 مللي ثانية ، والثانية بعد 200 مللي ثانية ، إلخ.
  • بعد أول 5 رسائل ، يجب أن يزيد البرنامج النصي التأخير الرئيسي بمقدار 200 مللي ثانية. وبالتالي ، سيتم عرض الرسالة السادسة بعد 500 مللي ثانية + 200 مللي ثانية (700 مللي ثانية) ، و7-900 مللي ثانية ، والرسالة الثامنة - بعد 1100 مللي ثانية ، وهكذا.
  • بعد 10 رسائل ، يجب أن يزيد البرنامج النصي الفاصل الزمني للتأخير الرئيسي بمقدار 300 مللي ثانية. يجب عرض الرسالة الحادية عشرة بعد 500 مللي ثانية + 1000 مللي ثانية + 300 مللي ثانية (18000 مللي ثانية). يجب عرض الرسالة الثانية عشرة بعد 2100 مللي ثانية ، إلخ.

وفقا لهذا المبدأ ، يجب أن يعمل البرنامج إلى أجل غير مسمى.

قم بتضمين تأخير في الرسالة المعروضة. يجب أن تحصل على شيء مثل هذا (لا تعليق):

Hello World. 100 // 100
Hello World. 100 // 200
Hello World. 100 // 300
Hello World. 100 // 400
Hello World. 100 // 500
Hello World. 200 // 700
Hello World. 200 // 900
Hello World. 200 // 1100
...


القيود : يمكنك استخدام المكالمات فقط لـ setInterval (وليس setTimeout ) و ONE if .

الحل

نظرًا setInterval لا يمكننا العمل إلا مع مكالمات setInterval ، فنحن هنا نحتاج إلى استخدام العودية وأيضًا زيادة تأخير مكالمة setInterval التالية. بالإضافة إلى ذلك ، نحتاج إلى if لتحقيق ذلك فقط بعد 5 مكالمات إلى هذه الوظيفة العودية.

فيما يلي حل ممكن:

 let lastIntervalId, counter = 5; const greeting = delay => { if (counter === 5) { clearInterval(lastIntervalId); lastIntervalId = setInterval(() => { console.log('Hello World. ', delay); greeting(delay + 100); }, delay); counter = 0; } counter += 1; }; greeting(100); 

شكرا لكل من قرأها.

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


All Articles