أساسيات JavaScript

المؤقتات: setTimeout و setInterval

45 دقيقة الدرس 28 من 60

مقدمة في المؤقتات في JavaScript

JavaScript هي لغة ذات خيط واحد مما يعني انها تستطيع تنفيذ قطعة واحدة فقط من الكود في كل مرة. ومع ذلك يوفر المتصفح دوال مؤقتات تتيح لك جدولة تنفيذ الكود بعد تاخير او على فترات منتظمة. هذه المؤقتات ليست جزءا من لغة JavaScript نفسها -- بل هي واجهات برمجة تطبيقات ويب يوفرها بيئة المتصفح. فهم المؤقتات ضروري لبناء ميزات مثل ساعات العد التنازلي والحفظ التلقائي وعروض الشرائح والرسوم المتحركة ومدخلات البحث المرتدة ومعالجات التمرير المخنوقة. في هذا الدرس ستتقن setTimeout وsetInterval وrequestAnimationFrame وانماط مهمة مثل الارتداد والخنق التي تستخدم في كل تطبيق ويب احترافي.

setTimeout: تشغيل الكود بعد تاخير

دالة setTimeout تجدول دالة ليتم استدعاؤها مرة واحدة بعد تاخير محدد. يقاس التاخير بالمللي ثانية (1000 مللي ثانية تساوي ثانية واحدة). بعد التاخير يتم وضع الدالة في طابور المهام وتنفيذها عندما يكون مكدس الاستدعاء فارغا.

الصيغة الاساسية

مثال: الصيغة الاساسية لـ setTimeout

// الصيغة: setTimeout(callback, delayInMilliseconds)
setTimeout(function() {
    console.log('هذا ينفذ بعد ثانيتين');
}, 2000);

// باستخدام دالة سهمية
setTimeout(() => {
    console.log('هذا ايضا ينفذ بعد ثانيتين');
}, 2000);

// باستخدام دالة مسماة
function showMessage() {
    console.log('مرحبا من setTimeout!');
}
setTimeout(showMessage, 3000); // ينفذ بعد 3 ثوانٍ
ملاحظة: التاخير الذي تحدده هو الحد الادنى للتاخير وليس وقتا دقيقا مضمونا. اذا كان الخيط الرئيسي مشغولا بتنفيذ كود اخر فان استدعاء المؤقت سينتظر في الطابور حتى يصبح مكدس الاستدعاء فارغا. هذا يعني ان setTimeout(fn, 100) قد ينفذ فعليا بعد 105 مللي ثانية او اكثر اذا كان الخيط مشغولا.

تمرير الوسائط الى setTimeout

يمكنك تمرير وسائط اضافية الى setTimeout سيتم توجيهها الى دالة الاستدعاء الراجع. هذه الوسائط تاتي بعد معامل التاخير.

مثال: تمرير الوسائط الى دالة الاستدعاء

// الطريقة 1: وسائط اضافية بعد التاخير
function greet(name, greeting) {
    console.log(greeting + '، ' + name + '!');
}
setTimeout(greet, 1000, 'احمد', 'مرحبا');
// بعد ثانية واحدة: "مرحبا، احمد!"

// الطريقة 2: استخدام دالة مغلفة (الاكثر شيوعا)
const userName = 'سارة';
setTimeout(function() {
    greet(userName, 'اهلا');
}, 1500);

// الطريقة 3: استخدام دالة سهمية (الاكثر شعبية)
const city = 'الرياض';
setTimeout(() => {
    console.log('تحديث الطقس لـ ' + city);
}, 2000);

الغاء مؤقت الانتظار

دالة setTimeout تعيد معرفا رقميا يحدد المؤقت بشكل فريد. يمكنك استخدام هذا المعرف مع clearTimeout لالغاء المؤقت قبل ان ينطلق. هذا ضروري للميزات التي قد يلغي فيها المستخدم اجراء قبل اكتماله.

مثال: الغاء مؤقت الانتظار

// تخزين معرف المؤقت
const timerId = setTimeout(function() {
    console.log('لن ينفذ هذا اذا تم الغاؤه');
}, 5000);

console.log('معرف المؤقت:', timerId); // عدد صحيح موجب

// الغاء المؤقت قبل ان ينطلق
clearTimeout(timerId);
console.log('تم الغاء المؤقت!');

// مثال عملي: الغاء الحفظ التلقائي اذا استمر المستخدم بالكتابة
let autoSaveTimer = null;

function onUserInput() {
    // الغاء اي حفظ تلقائي معلق
    if (autoSaveTimer) {
        clearTimeout(autoSaveTimer);
    }

    // جدولة حفظ تلقائي جديد بعد 3 ثوانٍ
    autoSaveTimer = setTimeout(function() {
        saveDocument();
        console.log('تم الحفظ التلقائي!');
    }, 3000);
}

function saveDocument() {
    // منطق الحفظ هنا
}

setInterval: تشغيل الكود بشكل متكرر

دالة setInterval تجدول دالة ليتم استدعاؤها بشكل متكرر على فترات زمنية ثابتة. تستمر في الانطلاق حتى توقفها صراحة باستخدام clearInterval. الصيغة مطابقة لـ setTimeout لكن بدلا من التنفيذ مرة واحدة تتكرر.

مثال: الاستخدام الاساسي لـ setInterval

// الصيغة: setInterval(callback, intervalInMilliseconds)
let count = 0;

const intervalId = setInterval(function() {
    count++;
    console.log('تك #' + count);

    // التوقف بعد 5 تكات
    if (count >= 5) {
        clearInterval(intervalId);
        console.log('توقف الفاصل الزمني');
    }
}, 1000); // ينفذ كل ثانية

مسح الفاصل الزمني

تماما مثل setTimeout تعيد دالة setInterval معرفا رقميا. يجب دائما تخزين هذا المعرف واستدعاء clearInterval عندما تريد ايقاف التنفيذ المتكرر. نسيان مسح الفواصل الزمنية هو مصدر شائع لتسريبات الذاكرة والاخطاء خاصة في تطبيقات الصفحة الواحدة حيث يتم انشاء المكونات وتدميرها ديناميكيا.

مثال: تنظيف الفاصل الزمني بشكل صحيح

// خزن دائما معرف الفاصل الزمني
let pollingInterval = null;

function startPolling() {
    // منع تشغيل عدة فواصل زمنية
    if (pollingInterval) {
        console.log('الاستطلاع نشط بالفعل');
        return;
    }

    pollingInterval = setInterval(function() {
        console.log('جاري البحث عن تحديثات...');
        fetchUpdates();
    }, 5000);
}

function stopPolling() {
    if (pollingInterval) {
        clearInterval(pollingInterval);
        pollingInterval = null; // اعادة التعيين لـ null للامان
        console.log('توقف الاستطلاع');
    }
}

function fetchUpdates() {
    // منطق الجلب هنا
}

// بدء الاستطلاع عند فتح المستخدم الصفحة
startPolling();

// ايقاف الاستطلاع عند مغادرة المستخدم
window.addEventListener('beforeunload', stopPolling);
خطا شائع: استدعاء setInterval عدة مرات بدون مسح الفاصل الزمني السابق ينشئ عدة فواصل زمنية متداخلة تعمل جميعها في وقت واحد. تحقق دائما ان الفاصل الزمني يعمل بالفعل قبل بدء واحد جديد وامسح الفاصل الزمني دائما عندما لم يعد مطلوبا. اضبط المتغير على null بعد المسح لتسهيل ادارة الحالة.

مشكلة انحراف الفاصل الزمني

مشكلة حرجة مع setInterval هي انها لا تحسب الوقت الذي تستغرقه دالة الاستدعاء نفسها في التنفيذ. اذا استغرق استدعاؤك 10 مللي ثانية وفاصلك الزمني 100 مللي ثانية فان التنفيذ التالي يبدا بعد 100 مللي ثانية من بدء السابق وليس بعد 100 مللي ثانية من انتهائه. والاسوا اذا استغرق الاستدعاء وقتا اطول من الفاصل الزمني يمكن ان تتراكم عمليات التنفيذ وتعمل بشكل متتابع بدون فجوة. بالاضافة الى ذلك تخنق المتصفحات المؤقتات في علامات التبويب الخلفية مما يسبب انحرافا كبيرا بمرور الوقت.

مثال: عرض انحراف الفاصل الزمني

// هذه الساعة ستنحرف بمرور الوقت لان setInterval
// لا يحسب وقت التنفيذ او خنق المتصفح

let expectedTime = Date.now() + 1000;

const driftInterval = setInterval(function() {
    const drift = Date.now() - expectedTime;
    console.log('الانحراف: ' + drift + 'ms');
    expectedTime += 1000;
}, 1000);

// بعد عدة دقائق يمكن ان يتراكم الانحراف
// الى مئات المللي ثانية او اكثر

// التوقف بعد 10 ثوانٍ للعرض
setTimeout(function() {
    clearInterval(driftInterval);
}, 10000);

setTimeout التكراري مقابل setInterval

بديل قوي لـ setInterval هو استخدام setTimeout التكراري. بدلا من ضبط فاصل زمني تستدعي setTimeout داخل الاستدعاء الراجع لجدولة التنفيذ التالي. هذا النهج له عدة مزايا: يضمن تاخيرا ثابتا بين عمليات التنفيذ (وليس من بداية الى بداية) ويمنع التراكم اذا استغرق الاستدعاء وقتا اطول من المتوقع ويسهل تعديل التاخير ديناميكيا.

مثال: setTimeout التكراري مقابل setInterval

// نهج setInterval -- الفجوة بين عمليات التنفيذ تتغير
setInterval(function() {
    // اذا استغرق هذا 50ms فان الفجوة للاستدعاء التالي 50ms فقط
    // (الفاصل=100ms ناقص التنفيذ=50ms)
    heavyComputation();
}, 100);

// نهج setTimeout التكراري -- فجوة 100ms مضمونة
function repeatWithDelay() {
    heavyComputation();

    // جدولة الاستدعاء التالي بعد انتهاء الحالي
    setTimeout(repeatWithDelay, 100);
}
repeatWithDelay();

// setTimeout تكراري بتاخير ديناميكي
let retryDelay = 1000; // البدء بثانية واحدة

function fetchWithRetry() {
    fetch('/api/data')
        .then(function(response) {
            if (response.ok) {
                retryDelay = 1000; // اعادة التعيين عند النجاح
                return response.json();
            }
            throw new Error('فشل الطلب');
        })
        .then(function(data) {
            console.log('تم استلام البيانات:', data);
            setTimeout(fetchWithRetry, retryDelay);
        })
        .catch(function(error) {
            console.log('خطا، اعادة المحاولة بعد ' + retryDelay + 'ms');
            retryDelay = Math.min(retryDelay * 2, 30000); // تراجع اسي، حد اقصى 30 ثانية
            setTimeout(fetchWithRetry, retryDelay);
        });
}

fetchWithRetry();

function heavyComputation() {
    // محاكاة عمل ثقيل
    let sum = 0;
    for (let i = 0; i < 1000000; i++) sum += i;
}
نصيحة احترافية: في قواعد الاكواد الاحترافية غالبا ما يفضل setTimeout التكراري على setInterval لمهام مثل استطلاع واجهات البرمجة ومنطق اعادة المحاولة والتحديثات الدورية. يمنحك تحكما اكبر في التوقيت ويمنع تراكم التنفيذ ويسهل تنفيذ انماط مثل التراجع الاسي لمعالجة الاخطاء.

setTimeout بتاخير صفري وحلقة الاحداث

سؤال شائع في المقابلات ومصدر للارتباك هو setTimeout(fn, 0). قد تتوقع ان مؤقتا بتاخير صفري ينفذ فورا لكنه لا يفعل. حتى مع تاخير 0 مللي ثانية يتم وضع الاستدعاء في طابور المهام ولن ينفذ الا بعد انتهاء الكود المتزامن الحالي وان يصبح مكدس الاستدعاء فارغا. هذا بسبب كيفية عمل حلقة احداث JavaScript.

مثال: سلوك setTimeout بتاخير صفري

console.log('الخطوة 1: قبل setTimeout');

setTimeout(function() {
    console.log('الخطوة 3: داخل setTimeout(fn, 0)');
}, 0);

console.log('الخطوة 2: بعد setTimeout');

// ترتيب الناتج:
// "الخطوة 1: قبل setTimeout"
// "الخطوة 2: بعد setTimeout"
// "الخطوة 3: داخل setTimeout(fn, 0)"

// استخدام عملي: تاجيل التنفيذ الى دورة حلقة الاحداث التالية
function processLargeArray(array) {
    const chunk = array.splice(0, 100);
    chunk.forEach(function(item) {
        // معالجة كل عنصر
    });

    if (array.length > 0) {
        // اعطاء المتصفح فرصة للحفاظ على استجابة واجهة المستخدم
        setTimeout(function() {
            processLargeArray(array);
        }, 0);
    }
}

// هذا يمنع واجهة المستخدم من التجمد اثناء المعالجة الثقيلة
processLargeArray([/* الاف العناصر */]);
ملاحظة: الحد الادنى للتاخير لـ setTimeout في المتصفحات الحديثة هو عادة 4 مللي ثانية للمؤقتات المتداخلة (بعد الاستدعاء المتداخل الخامس). في علامات التبويب الخلفية قد تخنق المتصفحات المؤقتات لتنطلق مرة واحدة على الاكثر في الثانية لتوفير البطارية والمعالج. كن على دراية بهذه السلوكيات عند تصميم ميزات تعتمد على المؤقتات.

requestAnimationFrame: رسوم متحركة سلسة

للرسوم المتحركة والتحديثات المرئية requestAnimationFrame افضل بكثير من setTimeout او setInterval. يخبر المتصفح انك تريد تنفيذ رسم متحرك ويطلب من المتصفح استدعاء دالتك قبل اعادة الرسم التالية. يعيد المتصفح عادة الرسم بمعدل 60 اطارا في الثانية (كل ~16.67 مللي ثانية) لذا يعمل استدعاؤك في الوقت الامثل لمرئيات سلسة. المزايا الرئيسية تشمل المزامنة التلقائية مع معدل تحديث الشاشة والتوقف المؤقت التلقائي عندما تكون علامة التبويب في الخلفية (توفير المعالج والبطارية) ورسوم متحركة اكثر سلاسة لان المتصفح يمكنه تحسين خط انابيب العرض.

مثال: رسم متحرك سلس باستخدام requestAnimationFrame

const box = document.getElementById('animated-box');
let position = 0;
let animationId = null;

function animate() {
    position += 2;
    box.style.transform = 'translateX(' + position + 'px)';

    // التوقف عند الوصول الى 500 بكسل
    if (position < 500) {
        animationId = requestAnimationFrame(animate);
    }
}

// بدء الرسم المتحرك
animationId = requestAnimationFrame(animate);

// لالغاء الرسم المتحرك:
// cancelAnimationFrame(animationId);

cancelAnimationFrame

تماما كما يلغي clearTimeout مؤقتا ويلغي clearInterval فاصلا زمنيا يلغي cancelAnimationFrame طلب اطار رسم متحرك معلقا. خزن دائما المعرف الذي يعيده requestAnimationFrame والغه عندما يحتاج الرسم المتحرك للتوقف.

مثال: التحكم في الرسم المتحرك بالبدء والايقاف

const element = document.getElementById('spinner');
let angle = 0;
let frameId = null;
let isRunning = false;

function spin() {
    angle = (angle + 3) % 360;
    element.style.transform = 'rotate(' + angle + 'deg)';
    frameId = requestAnimationFrame(spin);
}

function startAnimation() {
    if (!isRunning) {
        isRunning = true;
        frameId = requestAnimationFrame(spin);
    }
}

function stopAnimation() {
    if (isRunning) {
        cancelAnimationFrame(frameId);
        isRunning = false;
        frameId = null;
    }
}

// تبديل الرسم المتحرك عند نقر الزر
document.getElementById('toggle-btn').addEventListener('click', function() {
    if (isRunning) {
        stopAnimation();
    } else {
        startAnimation();
    }
});

استخدام معامل الطابع الزمني

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

مثال: رسم متحرك مستقل عن معدل الاطارات

const ball = document.getElementById('ball');
const duration = 2000; // الرسم المتحرك يستمر ثانيتين
let startTime = null;

function animateBall(timestamp) {
    if (!startTime) startTime = timestamp;

    const elapsed = timestamp - startTime;
    const progress = Math.min(elapsed / duration, 1); // من 0 الى 1

    // منحنى تسهيل الخروج
    const eased = 1 - Math.pow(1 - progress, 3);

    const distance = eased * 400; // تحريك 400 بكسل اجمالا
    ball.style.transform = 'translateX(' + distance + 'px)';

    if (progress < 1) {
        requestAnimationFrame(animateBall);
    } else {
        console.log('اكتمل الرسم المتحرك!');
    }
}

// بدء الرسم المتحرك عند النقر
document.getElementById('start-btn').addEventListener('click', function() {
    startTime = null; // اعادة تعيين وقت البدء
    requestAnimationFrame(animateBall);
});

الارتداد باستخدام المؤقتات

الارتداد هو تقنية تضمن استدعاء دالة فقط بعد فترة معينة من عدم النشاط. يستخدم بشكل شائع لمدخلات البحث حيث تريد الانتظار حتى يتوقف المستخدم عن الكتابة قبل ارسال طلب API او لمعالجات تغيير حجم النافذة حيث تريد اعادة حساب التخطيط مرة واحدة فقط بعد ان ينتهي المستخدم من تغيير الحجم. يستخدم النمط clearTimeout وsetTimeout معا: كل حدث جديد يلغي المؤقت السابق ويضبط واحدا جديدا.

مثال: تنفيذ دالة الارتداد

function debounce(func, delay) {
    let timeoutId = null;

    return function() {
        const context = this;
        const args = arguments;

        // الغاء المؤقت السابق
        clearTimeout(timeoutId);

        // ضبط مؤقت جديد
        timeoutId = setTimeout(function() {
            func.apply(context, args);
        }, delay);
    };
}

// الاستخدام: مدخل بحث مرتد
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(function(event) {
    const query = event.target.value;
    console.log('جاري البحث عن:', query);
    // اجراء استدعاء API هنا
    fetchSearchResults(query);
}, 300);

searchInput.addEventListener('input', debouncedSearch);

// الاستخدام: تغيير حجم النافذة المرتد
const debouncedResize = debounce(function() {
    console.log('تم تغيير حجم النافذة الى:',
        window.innerWidth, 'x', window.innerHeight);
    recalculateLayout();
}, 250);

window.addEventListener('resize', debouncedResize);

function fetchSearchResults(query) { /* استدعاء API */ }
function recalculateLayout() { /* منطق التخطيط */ }

الارتداد بالحافة الامامية

احيانا تريد ان تنطلق الدالة فورا عند الاستدعاء الاول ثم تنتظر عدم النشاط قبل السماح لها بالانطلاق مرة اخرى. يسمى هذا ارتداد "الحافة الامامية".

مثال: ارتداد الحافة الامامية

function debounceLeading(func, delay) {
    let timeoutId = null;

    return function() {
        const context = this;
        const args = arguments;
        const shouldCallImmediately = !timeoutId;

        clearTimeout(timeoutId);

        timeoutId = setTimeout(function() {
            timeoutId = null;
        }, delay);

        if (shouldCallImmediately) {
            func.apply(context, args);
        }
    };
}

// الدالة تنطلق فورا عند النقرة الاولى
// ثم تتجاهل النقرات الاخرى لمدة ثانية
const debouncedClick = debounceLeading(function() {
    console.log('تم نقر الزر!');
    submitForm();
}, 1000);

document.getElementById('submit-btn')
    .addEventListener('click', debouncedClick);

function submitForm() { /* منطق الارسال */ }

الخنق باستخدام المؤقتات

الخنق يضمن استدعاء دالة مرة واحدة على الاكثر خلال فترة زمنية محددة. على عكس الارتداد (الذي ينتظر عدم النشاط) يضمن الخنق تنفيذا منتظما بمعدل محكوم. هذا مثالي للاحداث التي تنطلق باستمرار مثل scroll وmousemove وresize حيث تريد تحديثات متسقة بدون اثقال المتصفح.

مثال: تنفيذ دالة الخنق

function throttle(func, limit) {
    let inThrottle = false;

    return function() {
        const context = this;
        const args = arguments;

        if (!inThrottle) {
            func.apply(context, args);
            inThrottle = true;

            setTimeout(function() {
                inThrottle = false;
            }, limit);
        }
    };
}

// الاستخدام: معالج تمرير مخنوق -- ينطلق كل 100 مللي ثانية كحد اقصى
const throttledScroll = throttle(function() {
    const scrollPosition = window.scrollY;
    console.log('موضع التمرير:', scrollPosition);
    updateScrollIndicator(scrollPosition);
}, 100);

window.addEventListener('scroll', throttledScroll);

// الاستخدام: تحريك فارة مخنوق -- يحدث كل 50 مللي ثانية كحد اقصى
const throttledMouseMove = throttle(function(event) {
    updateCursorPosition(event.clientX, event.clientY);
}, 50);

document.addEventListener('mousemove', throttledMouseMove);

function updateScrollIndicator(pos) { /* تحديث واجهة المستخدم */ }
function updateCursorPosition(x, y) { /* تحديث واجهة المستخدم */ }

الخنق مع استدعاء لاحق

الخنق الاساسي اعلاه يسقط الاستدعاءات التي تحدث خلال فترة الخنق. احيانا تريد ضمان تنفيذ الاستدعاء الاخير دائما حتى لو وصل خلال فترة خنق. يسمى هذا الخنق مع استدعاء لاحق.

مثال: الخنق مع استدعاء لاحق

function throttleWithTrailing(func, limit) {
    let inThrottle = false;
    let lastArgs = null;
    let lastContext = null;

    return function() {
        const context = this;
        const args = arguments;

        if (!inThrottle) {
            func.apply(context, args);
            inThrottle = true;

            setTimeout(function() {
                inThrottle = false;
                // تنفيذ الاستدعاء الاخير ان وجد
                if (lastArgs) {
                    func.apply(lastContext, lastArgs);
                    lastArgs = null;
                    lastContext = null;
                }
            }, limit);
        } else {
            // تخزين اخر استدعاء لوقت لاحق
            lastArgs = args;
            lastContext = context;
        }
    };
}

// هذا يضمن التقاط موضع التمرير النهائي دائما
const throttledHandler = throttleWithTrailing(function() {
    console.log('موضع التمرير:', window.scrollY);
}, 200);

window.addEventListener('scroll', throttledHandler);
نصيحة احترافية: اختر الارتداد عندما تريد الانتظار لتوقف في النشاط (مدخل البحث والتحقق من النماذج وتخطيط تغيير الحجم). اختر الخنق عندما تريد تحديثات متسقة ومحدودة المعدل اثناء النشاط المستمر (تجسس التمرير وتتبع الفارة وحلقات الالعاب). كلا النمطين ادوات اساسية في مجموعة ادوات كل مطور واجهات امامية.

بناء مؤقت عد تنازلي

مؤقت العد التنازلي هو مشروع عملي يجمع بين setInterval وحسابات الوقت. التحدي الرئيسي هو التعامل مع مشكلة الانحراف -- بدلا من عد الثواني بفاصل زمني يجب مقارنة الوقت الحالي بوقت الانتهاء المستهدف للدقة.

مثال: مؤقت عد تنازلي دقيق

function createCountdown(targetDate, displayElement) {
    function updateDisplay() {
        const now = new Date().getTime();
        const distance = targetDate - now;

        if (distance <= 0) {
            clearInterval(countdownInterval);
            displayElement.textContent = 'اكتمل العد التنازلي!';
            return;
        }

        const days = Math.floor(distance / (1000 * 60 * 60 * 24));
        const hours = Math.floor(
            (distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
        );
        const minutes = Math.floor(
            (distance % (1000 * 60 * 60)) / (1000 * 60)
        );
        const seconds = Math.floor(
            (distance % (1000 * 60)) / 1000
        );

        displayElement.textContent =
            days + 'ي ' + hours + 'س ' +
            minutes + 'د ' + seconds + 'ث';
    }

    // تحديث فوري ثم كل ثانية
    updateDisplay();
    const countdownInterval = setInterval(updateDisplay, 1000);

    // ارجاع دالة لايقاف العد التنازلي
    return function stop() {
        clearInterval(countdownInterval);
    };
}

// الاستخدام: العد التنازلي لراس السنة 2027
const display = document.getElementById('countdown');
const newYear = new Date('2027-01-01T00:00:00').getTime();
const stopCountdown = createCountdown(newYear, display);

// ايقاف العد التنازلي اذا لزم الامر:
// stopCountdown();

بناء ساعة ايقاف

ساعة الايقاف تحتاج لتتبع الوقت المنقضي مع وظائف البدء والايقاف واعادة التعيين. استخدام requestAnimationFrame بدلا من setInterval يوفر تحديثات اكثر سلاسة وتوقيتا اكثر دقة للعرض.

مثال: ساعة ايقاف باستخدام requestAnimationFrame

function createStopwatch(displayElement) {
    let startTime = 0;
    let elapsedTime = 0;
    let isRunning = false;
    let frameId = null;

    function formatTime(ms) {
        const totalSeconds = Math.floor(ms / 1000);
        const minutes = Math.floor(totalSeconds / 60);
        const seconds = totalSeconds % 60;
        const centiseconds = Math.floor((ms % 1000) / 10);

        return (
            String(minutes).padStart(2, '0') + ':' +
            String(seconds).padStart(2, '0') + '.' +
            String(centiseconds).padStart(2, '0')
        );
    }

    function update() {
        if (!isRunning) return;
        const currentTime = Date.now();
        const totalElapsed = elapsedTime + (currentTime - startTime);
        displayElement.textContent = formatTime(totalElapsed);
        frameId = requestAnimationFrame(update);
    }

    return {
        start: function() {
            if (!isRunning) {
                isRunning = true;
                startTime = Date.now();
                frameId = requestAnimationFrame(update);
            }
        },
        stop: function() {
            if (isRunning) {
                isRunning = false;
                elapsedTime += Date.now() - startTime;
                cancelAnimationFrame(frameId);
                displayElement.textContent = formatTime(elapsedTime);
            }
        },
        reset: function() {
            isRunning = false;
            elapsedTime = 0;
            startTime = 0;
            cancelAnimationFrame(frameId);
            displayElement.textContent = formatTime(0);
        },
        getElapsed: function() {
            if (isRunning) {
                return elapsedTime + (Date.now() - startTime);
            }
            return elapsedTime;
        }
    };
}

// الاستخدام
const display = document.getElementById('stopwatch-display');
const stopwatch = createStopwatch(display);

document.getElementById('start-btn').addEventListener('click', stopwatch.start);
document.getElementById('stop-btn').addEventListener('click', stopwatch.stop);
document.getElementById('reset-btn').addEventListener('click', stopwatch.reset);

بناء عرض شرائح باستخدام setInterval

عرض شرائح الصور هو حالة استخدام كلاسيكية لـ setInterval. يجب ان يتقدم العرض تلقائيا ويسمح بالتنقل اليدوي ويتوقف عند التحويم ويستانف عند مغادرة الفارة. اليك تنفيذ كامل يتعامل مع جميع هذه المتطلبات.

مثال: تنفيذ كامل لعرض الشرائح

function createSlideshow(container, intervalMs) {
    const slides = container.querySelectorAll('.slide');
    let currentSlide = 0;
    let slideshowInterval = null;
    const totalSlides = slides.length;

    function showSlide(index) {
        slides.forEach(function(slide) {
            slide.style.display = 'none';
        });
        slides[index].style.display = 'block';
    }

    function nextSlide() {
        currentSlide = (currentSlide + 1) % totalSlides;
        showSlide(currentSlide);
    }

    function prevSlide() {
        currentSlide = (currentSlide - 1 + totalSlides) % totalSlides;
        showSlide(currentSlide);
    }

    function startAutoplay() {
        if (!slideshowInterval) {
            slideshowInterval = setInterval(nextSlide, intervalMs);
        }
    }

    function stopAutoplay() {
        if (slideshowInterval) {
            clearInterval(slideshowInterval);
            slideshowInterval = null;
        }
    }

    // التهيئة: عرض الشريحة الاولى
    showSlide(0);
    startAutoplay();

    // التوقف عند التحويم
    container.addEventListener('mouseenter', stopAutoplay);
    container.addEventListener('mouseleave', startAutoplay);

    // ازرار التنقل اليدوي
    container.querySelector('.next-btn')
        .addEventListener('click', function() {
            stopAutoplay();
            nextSlide();
            startAutoplay();
        });

    container.querySelector('.prev-btn')
        .addEventListener('click', function() {
            stopAutoplay();
            prevSlide();
            startAutoplay();
        });

    // التنقل بلوحة المفاتيح
    document.addEventListener('keydown', function(event) {
        if (event.key === 'ArrowRight') {
            stopAutoplay();
            nextSlide();
            startAutoplay();
        } else if (event.key === 'ArrowLeft') {
            stopAutoplay();
            prevSlide();
            startAutoplay();
        }
    });

    // ارجاع عناصر التحكم للاستخدام الخارجي
    return {
        next: nextSlide,
        prev: prevSlide,
        goTo: function(index) {
            currentSlide = index;
            showSlide(index);
        },
        start: startAutoplay,
        stop: stopAutoplay
    };
}

// الاستخدام
const container = document.getElementById('slideshow');
const slideshow = createSlideshow(container, 4000); // 4 ثوانٍ لكل شريحة

مثال واقعي: حفظ تلقائي مع مؤشر حالة

العديد من تطبيقات الويب الحديثة مثل Google Docs تحفظ عملك تلقائيا على فترات منتظمة. اليك تنفيذ عملي يجمع بين الارتداد (لاكتشاف متى يتوقف المستخدم عن الكتابة) ومؤشر حالة يعرض حالة الحفظ.

مثال: نظام حفظ تلقائي

function createAutoSave(editor, statusElement, saveFunction) {
    let saveTimeout = null;
    let periodicInterval = null;
    let isDirty = false;
    let lastSavedContent = editor.value;

    function updateStatus(message) {
        statusElement.textContent = message;
    }

    function save() {
        const content = editor.value;
        if (content === lastSavedContent) {
            return; // لا توجد تغييرات للحفظ
        }

        updateStatus('جاري الحفظ...');
        saveFunction(content)
            .then(function() {
                lastSavedContent = content;
                isDirty = false;
                updateStatus('تم حفظ جميع التغييرات');
            })
            .catch(function(error) {
                updateStatus('فشل الحفظ -- اعادة المحاولة بعد 5 ثوانٍ');
                setTimeout(save, 5000);
            });
    }

    // حفظ مرتد: ينطلق بعد ثانيتين من توقف المستخدم عن الكتابة
    editor.addEventListener('input', function() {
        isDirty = true;
        updateStatus('تغييرات غير محفوظة');

        clearTimeout(saveTimeout);
        saveTimeout = setTimeout(save, 2000);
    });

    // حفظ دوري: كل 30 ثانية كشبكة امان
    periodicInterval = setInterval(function() {
        if (isDirty) {
            save();
        }
    }, 30000);

    // الحفظ قبل مغادرة المستخدم الصفحة
    window.addEventListener('beforeunload', function(event) {
        if (isDirty) {
            save();
            event.preventDefault();
            event.returnValue = '';
        }
    });

    // دالة التنظيف
    return function destroy() {
        clearTimeout(saveTimeout);
        clearInterval(periodicInterval);
    };
}

// الاستخدام
const editor = document.getElementById('editor');
const status = document.getElementById('save-status');

function saveToServer(content) {
    return fetch('/api/save', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: content })
    });
}

const destroyAutoSave = createAutoSave(editor, status, saveToServer);

مثال واقعي: اشعار منبثق مع اخفاء تلقائي

الاشعارات المنبثقة هي رسائل صغيرة تظهر لفترة وجيزة ثم تختفي تلقائيا. هذا نمط واجهة مستخدم شائع يجمع بين setTimeout ومعالجة DOM.

مثال: نظام اشعارات منبثقة

function createToastSystem(containerElement) {
    function showToast(message, type, duration) {
        type = type || 'info';
        duration = duration || 3000;

        const toast = document.createElement('div');
        toast.className = 'toast toast-' + type;
        toast.textContent = message;

        // اضافة زر اغلاق
        const closeBtn = document.createElement('button');
        closeBtn.textContent = 'x';
        closeBtn.className = 'toast-close';
        toast.appendChild(closeBtn);

        containerElement.appendChild(toast);

        // اخفاء تلقائي بعد المدة
        let dismissTimer = setTimeout(function() {
            removeToast(toast);
        }, duration);

        // ايقاف الاخفاء التلقائي عند التحويم
        toast.addEventListener('mouseenter', function() {
            clearTimeout(dismissTimer);
        });

        toast.addEventListener('mouseleave', function() {
            dismissTimer = setTimeout(function() {
                removeToast(toast);
            }, duration);
        });

        // اغلاق يدوي
        closeBtn.addEventListener('click', function() {
            clearTimeout(dismissTimer);
            removeToast(toast);
        });
    }

    function removeToast(toast) {
        toast.style.opacity = '0';
        setTimeout(function() {
            if (toast.parentNode) {
                toast.parentNode.removeChild(toast);
            }
        }, 300); // انتظار رسم متحرك التلاشي
    }

    return {
        success: function(msg, dur) { showToast(msg, 'success', dur); },
        error: function(msg, dur) { showToast(msg, 'error', dur); },
        warning: function(msg, dur) { showToast(msg, 'warning', dur); },
        info: function(msg, dur) { showToast(msg, 'info', dur); }
    };
}

// الاستخدام
const toastContainer = document.getElementById('toast-container');
const toast = createToastSystem(toastContainer);

toast.success('تم حفظ الملف بنجاح!');
toast.error('انقطع الاتصال. جاري اعادة المحاولة...', 5000);
toast.warning('تنتهي جلستك بعد 5 دقائق');
toast.info('يتوفر تحديث جديد');

مثال واقعي: استدعاءات API محدودة المعدل

عند العمل مع واجهات برمجة تطبيقات طرف ثالث لها حدود معدل تحتاج للتحكم في عدد مرات ارسال الطلبات. هذا المثال يجمع بين طابور وsetTimeout لمعالجة استدعاءات API بمعدل محكوم.

مثال: محدد معدل استدعاءات API

function createRateLimiter(callsPerSecond) {
    const queue = [];
    let isProcessing = false;
    const interval = 1000 / callsPerSecond;

    function processQueue() {
        if (queue.length === 0) {
            isProcessing = false;
            return;
        }

        isProcessing = true;
        const task = queue.shift();

        task.execute()
            .then(task.resolve)
            .catch(task.reject);

        setTimeout(processQueue, interval);
    }

    return function limitedCall(apiCallFunction) {
        return new Promise(function(resolve, reject) {
            queue.push({
                execute: apiCallFunction,
                resolve: resolve,
                reject: reject
            });

            if (!isProcessing) {
                processQueue();
            }
        });
    };
}

// الاستخدام: تحديد المعدل الى 5 استدعاءات API في الثانية
const rateLimited = createRateLimiter(5);

// سيتم فصلها بـ 200 مللي ثانية تلقائيا
for (let i = 0; i < 20; i++) {
    rateLimited(function() {
        return fetch('/api/items/' + i);
    }).then(function(response) {
        console.log('استجابة للعنصر ' + i);
    });
}
خطا شائع: استخدام setInterval للرسوم المتحركة بدلا من requestAnimationFrame. نهج setInterval له عدة مشاكل: يعمل بمعدل ثابت قد لا يتطابق مع معدل تحديث الشاشة مما يسبب تقطعا ويستمر في العمل في علامات التبويب الخلفية مهدرا الموارد ولا يتزامن مع دورة رسم المتصفح. استخدم دائما requestAnimationFrame للرسوم المتحركة المرئية واحتفظ بـ setInterval للمهام الدورية غير المرئية مثل استطلاع البيانات او الحفظ التلقائي.

ملخص انماط المؤقتات

اليك مرجع سريع لاختيار نهج المؤقت الصحيح للسيناريوهات الشائعة:

  • اجراء مؤجل لمرة واحدة -- استخدم setTimeout. مثال: عرض نافذة ترحيب منبثقة بعد 5 ثوانٍ من تحميل الصفحة.
  • اجراء متكرر على فترات ثابتة -- استخدم setInterval او setTimeout التكراري. مثال: استطلاع API كل 10 ثوانٍ للرسائل الجديدة.
  • رسم متحرك مرئي سلس -- استخدم requestAnimationFrame. مثال: تحريك عنصر عبر الشاشة.
  • انتظار توقف المستخدم عن الكتابة -- استخدم الارتداد مع setTimeout. مثال: اقتراحات بحث تظهر بعد توقف المستخدم.
  • تحديد معدل الاحداث المستمرة -- استخدم الخنق مع setTimeout. مثال: تحديث مؤشر تقدم التمرير.
  • عد تنازلي دقيق -- استخدم setInterval مع مقارنة التاريخ. مثال: مؤقت عد تنازلي لحدث.
  • عرض وقت منقضي دقيق -- استخدم requestAnimationFrame مع Date.now(). مثال: تطبيق ساعة ايقاف.

تمرين عملي

ابنِ تطبيق مؤقتات شامل يوضح فهمك لجميع مفاهيم المؤقتات المشمولة في هذا الدرس. انشئ صفحة واحدة بالمكونات الاربعة التالية: (1) مؤقت عد تنازلي يقبل عددا من الدقائق والثواني يدخله المستخدم ويعرض الوقت المتبقي محدثا كل ثانية ويعرض اشعارا عند الوصول للصفر. استخدم مقارنة التاريخ للدقة بدلا من انقاص عداد. (2) ساعة ايقاف بازرار بدء وايقاف ولفة واعادة تعيين. اعرض الوقت المنقضي بتنسيق دقائق وثوانٍ واجزاء من الثانية. استخدم requestAnimationFrame لتحديثات سلسة. خزن اوقات اللفات في قائمة مرتبة تعرض اسفل ساعة الايقاف. (3) منطقة اختبار سرعة كتابة مع textarea يكتب فيها المستخدم نصا. استخدم الارتداد لحساب وعرض عدد الكلمات في الدقيقة بعد 500 مللي ثانية من توقف المستخدم عن الكتابة. اعرض حالة "جاري الحساب..." اثناء انتظار مؤقت الارتداد. (4) عرض شرائح يدور عبر 5 عناصر div على الاقل (كل منها بالوان خلفية ونصوص مختلفة) كل 3 ثوانٍ. اضف ازرار التالي والسابق وتوقف عند التحويم وتنقل بمفاتيح الاسهم. تاكد من تنظيف جميع الفواصل الزمنية واطارات الرسم المتحرك بشكل صحيح عند ايقاف او اعادة تعيين المكونات. اختبر كل مكون على حدة وتحقق من عدم وجود تسريبات ذاكرة ببدء وايقاف كل ميزة عدة مرات.