لو فتحت Network tab في Chrome وكتبت 8 حروف في search box، ولقيت 8 طلبات راحوا للسيرفر، يبقى الـ frontend بتاعك بيكلّف الشركة فلوس مش لازمة. سطر واحد اسمه Debounce بيخلي الطلب يطلع مرة واحدة بس لما المستخدم يخلّص كتابة.
المشكلة باختصار
عندك input بسيط في صفحة بحث. كل ما المستخدم يضغط زر، الـ onChange بيتنفّذ، والـ function اللي جواه بترسل fetch للسيرفر. لو المستخدم بيكتب "javascript"، ده 10 حروف، يعني 10 طلبات. 9 منهم بيتلغوا قبل ما يرجعوا، وبس آخر واحد هو اللي ليه قيمة فعلية للمستخدم.
المشكلة هنا مش في الكود، المشكلة في إنك بتعامل كل ضغطة زر زي ما هي حدث مهم لازم يتبعت لورا. فيه طريقتين بنحلهم بيهم: Debounce و Throttle. الناس بتلخبط بينهم، فا خليني أوضّح بمثال قبل أي كود.
مثال المصعد عشان تفهم الفرق على طول
تخيل معاك مصعد في مبنى. الراجل اللي بيشغّله بيطبّق سياسة من اتنين:
- سياسة Debounce: أي حد يدخل المصعد، الباب بيستنى 5 ثواني. لو في أي حد دخل تاني خلال الـ 5 ثواني، بيبتدي يعد من الأول. المصعد ميتحرّكش غير لما يعدي 5 ثواني كاملة من غير ما حد يدخل.
- سياسة Throttle: المصعد بيتحرّك كل 5 ثواني بالظبط، مهما حصل. لو دخل 10 ناس في ثانية، يطلعوا كلهم. لو دخل واحد بعد 6 ثواني، بيستنى 4 ثواني تانيين.
الفرق هنا واضح: Debounce بيأجّل التنفيذ لحد ما النشاط يهدأ. Throttle بيحدّ من معدل التنفيذ بغض النظر عن النشاط. الاتنين بيقللوا عدد المرات اللي function معينة بتتنفّذ فيها، بس بطريقة مختلفة.
التعريف العلمي لـ Debounce
Debounce هي higher-order function بتاخد دالة fn وفترة delay بالمللي ثانية، وبترجع دالة جديدة. الدالة الجديدة دي لما تتنده، بتفضّل تأجّل تنفيذ fn لحد ما يعدي وقت delay بدون أي نداء جديد. لو نُديت تاني خلال الـ delay، الـ timer بيبدأ من الأول.
الافتراض هنا: fn ميهمّكش تتنفّذ كل مرة، اللي يهمّك بس آخر مرة بعد ما النشاط يهدأ.
الكود (شغّال على Node 22 و Chrome 120+)
function debounce(fn, delay) {
let timerId = null;
return function (...args) {
if (timerId !== null) {
clearTimeout(timerId);
}
timerId = setTimeout(() => {
fn.apply(this, args);
timerId = null;
}, delay);
};
}
// الاستخدام في search input
const search = debounce((query) => {
console.log("Calling API with:", query);
fetch(`/api/search?q=${encodeURIComponent(query)}`);
}, 300);
document.getElementById("search-input")
.addEventListener("input", (e) => search(e.target.value));