البرمجة مبتدئ 7 دقيقة

كيفية تأخير الأحداث (Debounce) في JavaScript

في كل مرة يكتب فيها المستخدم في حقل بحث أو يغير حجم النافذة أو يتمرر عبر الصفحة، يُطلق المتصفح عشرات الأحداث في الثانية الواحدة. بدون تقييد معدل التنفيذ، ستُرهق معالجاتك الشبكةَ وتُعيد رسم DOM وتُعيد حساب التخطيطات أكثر بكثير مما هو ضروري.

يحل Debounce هذه المشكلة بالانتظار حتى يهدأ تدفق الأحداث قبل تشغيل الدالة. إذا استمر المستخدم في الكتابة، يُعاد ضبط المؤقت. فقط بعد توقفه للحظة يُنفَّذ الكود. هذا بالضبط ما تحتاجه للبحث الفوري، والحفظ التلقائي، ومعالجات تغيير حجم النافذة.

يبني هذا الدليل دالة debounce مدمجة وقابلة لإعادة الاستخدام من الصفر، ويوضح متى تستخدمها — ومتى تلجأ بدلاً من ذلك إلى شقيقتها Throttle.

الخطوات

  1. 1

    الفرق بين Debounce و Throttle

    Debounce يؤجل التنفيذ حتى يصمت تدفق الأحداث لمدة N مللي ثانية. كل حدث جديد يُعيد ضبط المؤقت. استخدمه حين تهتم فقط بالقيمة النهائية — استعلامات البحث، وإعادة حسابات تغيير الحجم، والحفظ التلقائي.

    Throttle يسمح للدالة بالتشغيل مرة واحدة كحد أقصى كل N مللي ثانية حتى لو استمرت الأحداث. استخدمه حين تحتاج تحديثات منتظمة أثناء التدفق — موضع التمرير، وتتبع الفأرة، وأشرطة التقدم.

  2. 2

    اكتب دالة debounce المساعدة

    الجوهر ثمانية أسطر: دالة تأخذ fn وdelay، وتُعيد دالة مغلفة، وتستخدم معرّف مؤقت محفوظ في closure لإلغاء أي استدعاء معلق قبل جدولة استدعاء جديد.

    javascript
    function debounce(fn, delay) {
      let timer;
      return function (...args) {
        clearTimeout(timer);
        timer = setTimeout(() => {
          fn.apply(this, args);
        }, delay);
      };
    }
  3. 3

    طبّقها على حقل البحث

    غلّف استدعاء الـ API بـdebounce قبل إضافته كمستمع للحدث. سيُطلق البحث بعد 300 مللي ثانية فقط من توقف المستخدم عن الكتابة، لا عند كل ضغطة مفتاح.

    javascript
    const searchInput = document.getElementById('search');
    
    async function fetchResults(query) {
      if (!query.trim()) return;
      const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
      const data = await res.json();
      renderResults(data);
    }
    
    const debouncedSearch = debounce(fetchResults, 300);
    
    searchInput.addEventListener('input', (e) => {
      debouncedSearch(e.target.value);
    });
  4. 4

    طبّقها على معالج تغيير حجم النافذة

    تُطلق أحداث تغيير حجم النافذة باستمرار بينما يسحب المستخدم حافة المتصفح. تأجيل المعالج يعني تنفيذ حسابات التخطيط المكلفة مرة واحدة فقط، بعد اكتمال التغيير.

    javascript
    function recalculateLayout() {
      console.log('Recalculating at:', window.innerWidth, window.innerHeight);
      // ... DOM measurements, chart redraws, grid recalculations
    }
    
    window.addEventListener('resize', debounce(recalculateLayout, 200));
  5. 5

    أضف خيار الحافة الأمامية (leading edge)

    النسخة الافتراضية تُنفَّذ عند الحافة المتأخرة (بعد فترة الهدوء). لحماية أزرار الإرسال — حيث تريد تنفيذ الاستدعاء الأول فوراً وتجاهل التكرارات السريعة — استخدم نسخة الحافة الأمامية.

    javascript
    function debounce(fn, delay, { leading = false } = {}) {
      let timer;
      return function (...args) {
        const callNow = leading && !timer;
        clearTimeout(timer);
        timer = setTimeout(() => {
          timer = null;
          if (!leading) fn.apply(this, args);
        }, delay);
        if (callNow) fn.apply(this, args);
      };
    }
    
    // Fires immediately on first click, ignores rapid repeat clicks
    const handleSubmit = debounce(submitForm, 500, { leading: true });
  6. 6

    احتفظ بسياق `this` الصحيح

    تستخدم الدالة المساعدة أعلاه fn.apply(this, args)، مما يمرر قيمة this التي استُدعيت بها الدالة المغلفة. هذا مهم عند تأجيل توابع على كائنات أو نماذج من الـ class. لا تستخدم دالة سهمية كمغلف خارجي أبداً — فهي تربط this لغوياً وستتجاهل سياق المستدعي بصمت.

    javascript
    class SearchWidget {
      constructor(input) {
        this.input = input;
        this.query = '';
        // Arrow function here would lose `this` inside handleInput
        this.input.addEventListener('input', debounce(this.handleInput.bind(this), 300));
      }
    
      handleInput(e) {
        this.query = e.target.value;
        this.fetch();
      }
    }
  7. 7

    لا تحتاج lodash لهذا

    دالة _.debounce من lodash مختبرة جيداً وتتعامل مع حالات الحافة كالإفراغ وإلغاء الاستدعاءات المعلقة. لكن إذا كان Debounce هو كل ما تحتاجه، فالدالة المساعدة ذات الثمانية أسطر أعلاه كافية — بلا اعتماديات، وبلا تكلفة على حزمة البناء، وأنت تعرف تماماً ما تفعله. اعتمد على lodash فقط إن كنت تستخدمه أصلاً، أو إن احتجت إلى واجهة flush/cancel.

نصائح ومحاذير

  • 300 مللي ثانية مناسبة افتراضياً لحقول البحث؛ 150-200 مللي ثانية تبدو أسرع استجابةً لأحداث تغيير الحجم والتمرير.
  • طبّق debounce دائماً في موضع الاستدعاء — أنشئ مغلفاً جديداً في كل مرة، لا تعدّل الدالة الأصلية في مكانها، وإلا ستشترك كل المستدعين في مؤقت واحد.
  • إذا احتجت إلى إلغاء استدعاء مؤجل (مثلاً عند إزالة مكوّن)، اكشف تابع <code>cancel</code> يستدعي <code>clearTimeout(timer)</code>.
  • في React، أنشئ الدالة المؤجلة باستخدام <code>useMemo</code> أو <code>useRef</code> لتجنب إعادة إنشائها عند كل تصيير.
  • التأجيل لا يغني عن تحديد معدل الطلبات من جهة السيرفر — طبّق throttling دائماً على جانب الـ API أيضاً.

خاتمة

Debouncing من أعلى التقنيات تأثيراً للحفاظ على سرعة استجابة الواجهات الغنية بالأحداث. دالة مساعدة واحدة من ثمانية أسطر تُزيل استدعاءات API المكررة، وتمنع اضطراب التخطيط، وتجعل نيتك في الكود واضحة. اكتبها مرة واحدة، ضعها في utils.js، واستوردها أينما احتجت إليها.

#JavaScript #Performance #Events
العودة إلى جميع الأدلة

هل تحتاج مساعدة في مشروعك؟

احجز استشارة مجانية لمدة 30 دقيقة لمناقشة تحدياتك التقنية واستكشاف الحلول معًا.