دليل شامل لـ setTimeout في JavaScript: محاكاة وظائف الإيقاف المؤقت والانتظار
ES5. تتوالى الأفكار والميزات التي تُستورد من لغات برمجة أخرى وتُدمج في JavaScript باستمرار. من أبرز هذه الميزات هي “الوعود” (Promises)، التي أصبحت على الأرجح الأكثر استخداماً بعد ES5. ومع ذلك، تفتقر JavaScript إلى طريقة مباشرة لإيقاف التنفيذ مؤقتاً واستئنافه لاحقاً، وهو ما يُعرف بـ “الإيقاف المؤقت” (pause) أو “النوم” (sleep). في هذا المقال، سنتعمق في كيفية تحقيق هذا السلوك وماذا يعني حقاً “الإيقاف المؤقت” أو “النوم” في سياق JavaScript. مفاجأة: JavaScript لا تتوقف حقاً عن العمل.
إليك الكود الجاهز الذي ينجز المهمة، والذي سنشرح تفاصيله لاحقاً:
/**
*
* @param duration Enter duration in seconds
*/
function sleep ( duration ) {
return new Promise ( resolve => {
setTimeout ( () => {
resolve()
}, duration * 1000 )
})
}
فهم setTimeout والوعود الوهمية
ما الذي يحدث هنا بالضبط؟ لنلقِ نظرة سريعة على مثال يستخدم المقتطف البرمجي أعلاه (وسنناقش تفاصيله لاحقاً):
async function performBatchActions ( ) {
// perform an API call
await performAPIRequest()
// sleep for 5 seconds
await sleep( 5 )
// perform an API call again
await performAPIRequest()
}
عند استدعاء الدالة performBatchActions، فإنها ببساطة تقوم بتنفيذ الدالة performAPIRequest، ثم تنتظر “حوالي” 5 ثوانٍ، وبعدها تستدعي نفس الدالة مرة أخرى. لاحظ أنني استخدمت تعبير “حوالي 5 ثوانٍ” بدلاً من “5 ثوانٍ” بالضبط. من المهم جداً الإشارة إلى أن الكود أعلاه لا يضمن إيقافاً مؤقتاً مثالياً. هذا يعني أنه إذا حددت مدة زمنية، ولنفترض ثانية واحدة، فإن JavaScript لا تضمن أن الكود سيبدأ التنفيذ بعد الإيقاف المؤقت بالضبط بعد ثانية واحدة. قد تتساءل: لماذا لا؟ لسوء الحظ، يعود السبب إلى كيفية عمل المؤقتات (timers) في JavaScript، وبشكل عام، إلى آلية “حلقة الأحداث” (Event Loop). ومع ذلك، تضمن JavaScript بشكل قاطع أن الجزء البرمجي الذي يلي الإيقاف المؤقت لن يتم تنفيذه أبداً قبل الوقت المحدد. لذا، نحن لا نواجه موقفاً غير محدد تماماً، بل هو جزئي فقط، وفي معظم الحالات يكون الهامش في حدود بضعة أجزاء من الثانية (ميلي ثانية) فقط.
JavaScript: بيئة أحادية المسار (Single-Threaded)
يعني مفهوم “المسار الواحد” (single thread) أن عملية JavaScript لا يمكنها الانحراف عن مسارها الأصلي على الإطلاق. يجب عليها تنفيذ جميع المهام – من مستمعي الأحداث (event listeners) إلى استدعاءات HTTP – على نفس المسار الرئيسي. وعندما يتم تنفيذ شيء واحد، لا يمكن لشيء آخر أن يُنفذ في نفس اللحظة بشكل متزامن. تخيل صفحة ويب تحتوي على أزرار متعددة وقمت بتشغيل الكود أعلاه لمحاكاة إيقاف مؤقت لمدة 10 ثوانٍ. ما الذي تتوقع حدوثه؟ لا شيء على الإطلاق! ستعمل صفحة الويب الخاصة بك بشكل طبيعي، وستظل أزرارك تستجيب للنقر، وبمجرد انتهاء فترة الإيقاف المؤقت التي تبلغ 10 ثوانٍ، سيتم تنفيذ الكود التالي. من الواضح إذاً أن JavaScript لا تقوم بحظر المسار الرئيسي بأكمله، لأنه لو فعلت ذلك، لتجمدت صفحة الويب وأصبحت الأزرار غير قابلة للنقر. إذاً، كيف تمكنت JavaScript من “إيقاف” مسار واحد مؤقتاً دون أن توقفه حقاً؟
تعرف على حلقة الأحداث (Event Loop)
على عكس لغات البرمجة الأخرى، لا تكتفي JavaScript بتنفيذ الكود بطريقة خطية من الأعلى إلى الأسفل. إنها لغة غير متزامنة (asynchronous) ومدفوعة بالأحداث (event-driven)، وتحتوي على قدر كبير من “السحر” في شكل حلقة الأحداث (Event Loop). تقوم حلقة الأحداث بتقسيم الكود الخاص بك إلى أجزاء متزامنة (synchronous) وأحداث معينة – مثل المؤقتات (timers) وطلبات HTTP. وبشكل أكثر دقة، هناك قائمتان للانتظار: قائمة المهام (task queue) وقائمة المهام المصغرة (microtask queue). كلما قمت بتشغيل JavaScript وظهر شيء غير متزامن (مثل حدث نقرة بالماوس أو وعد Promise)، فإن JavaScript تلقي به في قائمة المهام (أو قائمة المهام المصغرة) وتستمر في التنفيذ. عندما تكمل “دورة واحدة” (single tick)، تتحقق مما إذا كانت قوائم المهام والمهام المصغرة تحتوي على أي عمل لها. إذا كان الأمر كذلك، فستقوم بتنفيذ وظيفة الاستدعاء (callback) أو إجراء الإجراء المطلوب. لمن يرغب في التعمق أكثر في تفاصيل عمل حلقة الأحداث، يوصى بالبحث عن مصادر متخصصة في هذا الموضوع.
الخاتمة
لقد جئت إلى هنا بحثاً عن طريقة بسيطة لإيقاف التنفيذ مؤقتاً في JavaScript، وانتهى بك الأمر بتعلم أحد المفاهيم الأساسية في JavaScript: حلقة الأحداث! أليس هذا مدهشاً؟ إن فهم هذه الآلية أمر بالغ الأهمية لأي مطور JavaScript يسعى لكتابة أكواد فعالة وغير معرقلة.
الخلاصة التقنية
إن فهم كيفية عمل setTimeout في JavaScript يتجاوز مجرد معرفة بناء الجملة؛ إنه يتطلب استيعاباً عميقاً لنموذج التزامن والمسار الواحد (single-threaded concurrency model) وحلقة الأحداث. على الرغم من أن setTimeout توفر وسيلة لمحاكاة وظائف الإيقاف المؤقت أو الانتظار، إلا أنه من الضروري إدراك أنها لا “توقف” التنفيذ فعلياً. بدلاً من ذلك، تقوم بجدولة وظيفة استدعاء ليتم تنفيذها بعد فترة زمنية محددة، مما يتيح للمسار الرئيسي الاستمرار في معالجة المهام الأخرى. هذا السلوك غير المتزامن هو حجر الزاوية في بناء تطبيقات ويب سريعة الاستجابة وغير معرقلة لتجربة المستخدم. الاستخدام الفعال لـ setTimeout يتطلب مراعاة دقة التوقيتات وتأثيرها على تدفق التطبيق، خاصة عند التعامل مع التفاعلات المعقدة أو عمليات الشبكة.