كل تفاعل في صفحة ويب، من نقرة زر إلى تمرير الفأرة إلى
ضغطة لوحة مفاتيح، يمر عبر نظام الأحداث
في JavaScript. هو النظام الذي يحوّل صفحة جامدة إلى
تطبيق حي. ورغم أن معظم المطورين يستخدمون
addEventListener يوميًا، فإن قليلين
يفهمون كيف ينتقل الحدث عبر DOM، أو
لماذا قد لا يعمل مستمع معين، أو لماذا تبدو الصفحة
بطيئة بعد إضافة مستمعي تمرير.
هذا الدليل لا يكتفي بشرح “كيف تضيف مستمعًا”. إنه يفكك النظام من الداخل: كيف ينتقل الحدث من أعلى الشجرة إلى العنصر ثم يعود، وكيف يؤثر ذلك على الأداء والمنطق، وكيف تستخدم التفويض (Event Delegation) لتحسين الأداء، وكيف تنظف المستمعات دون تسريب ذاكرة. الهدف أن تتعامل مع الأحداث كالمهندس، لا كمستخدم للـAPI.
نظرة سريعة: خريطة أحداث JavaScript
الجدول التالي يلخص المفاهيم الأساسية التي يغطيها الدليل، مع مثال سريع لكل منها:
| المفهوم | الوظيفة | مثال |
|---|---|---|
| التدفق | مسار الحدث عبر DOM | capture → target → bubble |
| addEventListener | تسجيل مستمع حدث | el.addEventListener(...) |
| كائن الحدث | بيانات الحدث | e.target، e.type |
| preventDefault | منع السلوك الافتراضي | منع إرسال نموذج |
| stopPropagation | إيقاف انتشار الحدث | منع وصول النقر للأب |
| التفويض | مستمع واحد لعناصر كثيرة | parent.addEventListener |
| Passive | تحسين أداء التمرير | { passive: true } |
| AbortController | إزالة مستمعات متعددة | signal: controller.signal |
ما هي الأحداث؟ ولماذا هي جوهر الويب التفاعلي؟
الحدث هو إشعار يصدره المتصفح عند حدوث شيء يستحق الانتباه: نقرة، ضغطة مفتاح، اكتمال تحميل صورة، تغير حجم النافذة، استلام بيانات من الشبكة. المتصفح يطلق الإشعار، والكود الذي سجّل مستمعًا له يستجيب.
قبل الأحداث، كانت صفحات الويب جامدة. الـJavaScript
الحديثة كلها تقوم على الأحداث: أطر مثل React وVue
تستخدم نظامًا اصطناعيًا (synthetic) مبنيًا على أحداث
DOM الحقيقية. حتى لو لم تكن تكتب
addEventListener مباشرة، فأنت تستخدم
نظام الأحداث عندما تكتب onClick في React
أو v-on:click في Vue.
فهم الأحداث الحقيقية يجعلك قادرًا على:
- تشخيص مشاكل لا يفسرها الإطار (لماذا لا يعمل المستمع؟ لماذا يُستدعى مرتين؟).
- تحسين الأداء عبر التفويض والمستمعات السلبية.
- تجنب تسريب الذاكرة بتنظيف المستمعات.
- بناء مكونات قابلة لإعادة الاستخدام مع أحداث مخصصة.
تدفق الحدث: الالتقاط والهدف والفقاعات
عندما تنقر على زر داخل صفحة، لا يحدث الحدث في الزر فقط. المتصفح يمرره في ثلاث مراحل عبر شجرة DOM:
- مرحلة الالتقاط (Capturing): يبدأ الحدث من أعلى شجرة (window أو document)، وينزل نحو العنصر المستهدف.
- مرحلة الهدف (Target): يصل الحدث إلى العنصر الذي نُقر عليه.
- مرحلة الفقاعات (Bubbling): يعود الحدث صاعدًا من الهدف نحو الجذر.
هذا التدفق هو أساس تفويض الأحداث (Event Delegation)، وهو أيضًا مصدر كثير من الأخطاء عند المبتدئين. المثال التالي يوضح:
<div id="outer">
<button id="inner">Click me</button>
</div>
<script>
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
outer.addEventListener("click", () => console.log("outer capture"), true);
outer.addEventListener("click", () => console.log("outer bubble"), false);
inner.addEventListener("click", () => console.log("inner"));
// النتيجة عند النقر على الزر:
// outer capture
// inner
// outer bubble
</script>
المعامل الثالث في addEventListener يحدد
المرحلة: true (أو
{capture: true}) يعني الالتقاط،
والافتراضي false يعني الفقاعات. معظم
الأكواد تستخدم الوضع الافتراضي، لكن فهم الفرق
حاسم عند تشخيص مشاكل “لماذا يُستدعى المستمع
في العنصر الأب قبل الابن؟”.
addEventListener مقابل onclick: الفرق الذي يحدد جودة الكود
الطريقة التقليدية element.onclick = handler
تبدو أبسط، لكنها تحمل قيودًا تجعلها غير مناسبة في
الإنتاج:
| المعيار | onclick | addEventListener |
|---|---|---|
| مستمعات متعددة | لا، يستبدل السابق | نعم، يمكن إضافة عدة مستمعات |
| التحكم في المرحلة | لا | نعم، عبر capture |
| الإزالة | el.onclick = null | removeEventListener أو AbortSignal |
| المستمعات السلبية | لا | نعم، {passive: true} |
| التوافق مع الأطر | قد يتعارض مع مستمعات أخرى | مستقل |
| الشيوع في الإنتاج | نادر | المعيار الفعلي |
القاعدة العملية: استخدم
addEventListener دائمًا. الحالة الوحيدة
التي قد تستخدم فيها onclick هي كود تعليمي
بسيط، أو عند الحاجة إلى استبدال معالج حدث ديناميكيًا
دون تراكم مستمعات.
كائن الحدث: ما الذي تحمله كل نقرة؟
كل مستمع يستقبل كائن حدث يحتوي على معلومات غنية. الخصائص الأكثر استخدامًا:
-
type: نوع الحدث (مثل “click”). -
target: العنصر الذي أُطلق عليه الحدث فعليًا (قد يكون ابنًا للعنصر المسجّل عليه المستمع). -
currentTarget: العنصر الذي عليه المستمع الحالي (يختلف عن target عند الفقاعات). -
timeStamp: وقت وقوع الحدث. -
preventDefault(): منع السلوك الافتراضي. -
stopPropagation(): إيقاف انتشار الحدث. -
stopImmediatePropagation(): إيقاف الانتشار ومنع المستمعات الأخرى على نفس العنصر.
لأحداث محددة، هناك خصائص إضافية. مثلًا،
MouseEvent تحتوي على
clientX/clientY و
button، وKeyboardEvent
تحتوي على key وcode
وaltKey وغيرها.
نقطة مهمة: target هو ما نقر عليه
المستخدم، وهو ليس دائمًا العنصر الذي عليه المستمع.
هذه الحقيقة هي أساس التفويض.
preventDefault وstopPropagation: الفرق الذي يخلط كثيرين
هاتان الدالتان تبدوان متشابهتين، لكن وظيفتهما مختلفة تمامًا:
-
preventDefault(): تمنع السلوك الافتراضي للمتصفح تجاه الحدث. مثال: منع إرسال نموذج عند الضغط على زر submit، أو منع فتح رابط عند النقر عليه. -
stopPropagation(): تمنع الحدث من الانتشار إلى العناصر الأخرى في شجرة DOM. الحدث لا يكمل مرحلة الفقاعات.
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault(); // منع إعادة تحميل الصفحة
// إرسال البيانات عبر fetch
});
document.querySelector(".modal").addEventListener("click", (e) => {
e.stopPropagation(); // منع النقر من الوصول للخلفية
});
خطأ شائع: استخدام stopPropagation
عندما تريد منع السلوك الافتراضي، أو العكس. تذكر
القاعدة: preventDefault يوقف المتصفح،
stopPropagation يوقف الحدث.
تفويض الأحداث (Event Delegation): النمط الذي يغير كل شيء
بدلًا من إضافة مستمع لكل عنصر في قائمة من 1000 عنصر، أضف مستمعًا واحدًا على العنصر الأب، واستفد من الفقاعات لتحديد أي عنصر تم النقر عليه. هذا هو تفويض الأحداث.
// الطريقة السيئة: مستمع لكل عنصر
document.querySelectorAll(".item").forEach((item) => {
item.addEventListener("click", () => console.log(item.dataset.id));
});
// الطريقة الأفضل: مستمع واحد على الأب
document.querySelector(".list").addEventListener("click", (e) => {
const item = e.target.closest(".item");
if (!item) return;
console.log(item.dataset.id);
}); مزايا التفويض:
- أداء أفضل: مستمع واحد بدل آلاف المستمعات.
- يدعم العناصر المضافة حديثًا: إذا أضفت عنصرًا جديدًا إلى القائمة، سيعمل دون إعادة ربط.
- كود أنظف: منطق واحد في مكان واحد.
- ذاكرة أقل: عدد أقل من closures محتجزة.
لكن التفويض ليس سحريًا. تحدياته:
-
بعض الأحداث لا تنتشر (مثل
focusوblur، رغم أنfocusinوfocusoutينتشران). -
يجب استخدام
closest()بدلmatches()للتعامل مع العناصر المتداخلة. - قد يكون أبطأ في حالات نادرة إذا كان الشجرة عميقة جدًا.
الأحداث الأكثر استخدامًا في 2026
هذه هي الأحداث التي ستستخدمها في 90% من الحالات:
| الفئة | الأحداث | الاستخدام |
|---|---|---|
| الفأرة | click، dblclick، mousedown، mouseup، mousemove | أزرار، سحب، تلميحات |
| لوحة المفاتيح | keydown، keyup، keypress (مهمل) | اختصارات، إدخال |
| النماذج | submit، change، input، focus، blur | التحقق، الإرسال |
| اللمس | touchstart، touchmove، touchend | تفاعلات الهاتف |
| التمرير والحجم | scroll، resize | تأثيرات، تحميل كسول |
| الحياة | DOMContentLoaded، load، beforeunload | تهيئة، تحذير مغادرة |
| الشبكة | online، offline | إشعارات الاتصال |
ملاحظة: keypress مهمل، ويفضَّل استخدام
keydown أو beforeinput
بدلًا منه. كما أن scroll و
resize يعملان بكثافة عالية، ويحتاجان
إلى debounce أو
requestAnimationFrame.
الأحداث المخصصة: تواصل بين مكوناتك
ليس كل حدث يجب أن يكون من المتصفح. يمكنك إطلاق أحداث
مخصصة عبر CustomEvent، وهو نمط شائع في
Web Components أو عند تصميم مكونات مستقلة تتواصل
مع بقية الصفحة:
// إطلاق حدث مخصص من داخل مكون
const event = new CustomEvent("user:login", {
detail: { userId: 42, name: "Ali" },
bubbles: true,
});
document.dispatchEvent(event);
// الاستماع
document.addEventListener("user:login", (e) => {
console.log(e.detail.name); // "Ali"
}); الفائدة: المكون لا يحتاج إلى معرفة من يستمع، ولا يعتمد على متغيرات عامة. هذا يجعل المكونات قابلة لإعادة الاستخدام والاختبار.
المستمعات السلبية (Passive): كيف تنقذ التمرير؟
افتراضيًا، عندما تضيف مستمع scroll أو
touchmove أو wheel،
يفترض المتصفح أنك قد تستدعي
preventDefault()، فينتظر حتى ينتهي
المستمع قبل أن يستمر في التمرير. هذا يسبب
تأخرًا في التمرير حتى لو لم تفعل
شيئًا.
الحل هو passive: true:
window.addEventListener("scroll", handler, { passive: true });
window.addEventListener("touchmove", handler, { passive: true });
هذا يخبر المتصفح: “لن أمنع السلوك الافتراضي”.
النتيجة: التمرير يبقى سلسًا حتى لو كان المستمع
يعمل. القاعدة الحديثة: استخدم passive: true
لكل مستمع scroll أو
touchmove أو wheel ما لم
تكن متأكدًا أنك تحتاج preventDefault.
التنظيف: AbortController وremoveEventListener
المستمعات لا تُحذف تلقائيًا عند إزالة العنصر من DOM. وإذا كانت تشير إلى بيانات كبيرة، فإنها تسبب تسريب ذاكرة. هذا شائع في تطبيقات الصفحة الواحدة (SPA).
طريقتان لتنظيف المستمعات:
1. removeEventListener
function handler() { /* ... */ }
element.addEventListener("click", handler);
// لاحقًا
element.removeEventListener("click", handler); القيد: يجب تمرير نفس المرجع للدالة. إذا استخدمت دالة مجهولة، لا يمكنك إزالتها.
2. AbortController
الطريقة الحديثة والأنظف، خصوصًا عند إضافة مستمعات متعددة:
const controller = new AbortController();
element.addEventListener("click", handler, { signal: controller.signal });
element.addEventListener("scroll", scrollHandler, { signal: controller.signal });
// إزالة كل المستمعات المرتبطة
controller.abort();
الفائدة: متغير واحد يدير دورة حياة كل المستمعات.
عند تفكيك المكون، استدعاء
controller.abort() يزيلها جميعًا. هذا
نمط يجب أن يكون قياسيًا في أي كود حديث.
أخطاء شائعة في التعامل مع الأحداث
| الخطأ | السبب | الحل |
|---|---|---|
| استخدام دالة مجهولة ثم محاولة إزالتها | لا يوجد مرجع | استخدم دالة مسماة أو AbortController |
| الاعتماد على e.target بدل closest() | النقر قد يقع على ابن | استخدم e.target.closest(".item") |
| خلط preventDefault وstopPropagation | سلوك غير مقصود | افهم الفرق |
| إضافة مستمع داخل حلقة | آلاف المستمعات | استخدم التفويض |
| عدم استخدام passive في scroll | تمرير بطيء | {passive: true} |
| تنفيذ منطق ثقيل في scroll | إطارات مسقطة | debounce أو rAF |
| عدم إزالة المستمع عند تفكيك المكون | تسريب ذاكرة | AbortController |
الأداء: كيف تتجنب إبطاء الصفحة؟
المستمعات ليست مجانية. لها تكلفة ذاكرة، وتكلفة تنفيذ عند كل حدث. هذه المبادئ تحميك من إبطاء الصفحة:
- قلل عدد المستمعات: استخدم التفويض بدل مستمع لكل عنصر.
- قيّد الأحداث الكثيفة:
scrollوresizeوmousemoveتُطلق آلاف المرات في الثانية. استخدم debounce (تنفيذ بعد توقف الإدخال) أو throttle (تنفيذ مرة كل فترة). - استخدم passive: خصوصًا في
scrollوtouchmove. - استخدم rAF للرسوم: إذا كنت
تعدل العناصر بصريًا استجابةً لحدث كثيف، استخدم
requestAnimationFrameبدلًا من التنفيذ الفوري. - تجنب المستمعات في العناصر التي ستُزال قريبًا: أضفها فقط عند الحاجة، وأزل عند الانتهاء.
function debounce(fn, delay = 200) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
window.addEventListener("resize", debounce(() => {
// منطق مكلف يعمل بعد توقف التمرير
}, 300), { passive: true }); أفضل الممارسات في الإنتاج
- استخدم addEventListener دائمًا.
وليس
onclick. - استخدم التفويض للأحداث المتكررة. مستمع واحد على الأب أفضل من آلاف على الأبناء.
- استخدم AbortController. حتى لو كنت ستزيل مستمعًا واحدًا، لأنه يوحّد التنظيف.
- مرر passive في scroll/touchmove/wheel. هذا تحسين مجاني.
- استخدم debounce/throttle. لا تنفّذ منطقًا ثقيلًا في كل حدث.
- افهم الفرق بين preventDefault وstopPropagation. خطأ شائع، وعواقبه كبيرة.
- استخدم CustomEvent للتواصل بين المكونات. بدل المتغيرات العامة.
- نظّف عند التفكيك. في React، استخدم cleanup function في useEffect. في Vanilla، استخدم AbortController.
قائمة تحقق نهائية
-
هل تستخدم
addEventListenerبدلًا منonclick؟ ← نعم، دائمًا - هل تفهم مراحل التدفق الثلاث؟ ← ضروري لتشخيص المشاكل
- هل تستخدم التفويض في القوائم المتكررة؟ ← نعم، لمستمع واحد
-
هل تمرر
passive: trueفي scroll؟ ← تحسين ملموس - هل تستخدم debounce في الأحداث الكثيفة؟ ← أو rAF
- هل تستخدم AbortController للتنظيف؟ ← أفضل نمط حديث
- هل تعرف الفرق بين preventDefault وstopPropagation؟ ← خطأ شائع
- هل تنظف المستمعات عند إزالة العناصر؟ ← نعم، لمنع التسريبات
- هل تستخدم CustomEvent للتواصل بين المكونات؟ ← بديل أنظف من المتغيرات العامة
- هل تختبر أداء الصفحة بعد إضافة مستمعات كثيرة؟ ← Performance Panel
روابط ومصادر مفيدة
فيما يلي روابط رسمية ومراجع يمكنك الرجوع إليها:
- MDN — EventTarget.addEventListener
- MDN — Introduction to events
- MDN — Event interface
- MDN — AbortController
- MDN — CustomEvent
- Chrome DevTools — Passive event listeners
- JavaScript.info — Bubbling and capturing
- JavaScript.info — Event delegation
أسئلة شائعة
هل يجب أن أستخدم onclick أم addEventListener دائمًا؟
addEventListener دائمًا، لأنه يسمح
بمستمعات متعددة، ويدعم خيارات مثل
passive وcapture،
ويمكن إزالته بشكل نظيف. onclick
يستبدل أي مستمع سابق، وهذا سبب شائع لأخطاء يصعب
تتبعها.
ما الفرق بين event.target وevent.currentTarget؟
target هو العنصر الذي أطلق الحدث
فعليًا (قد يكون ابنًا للعنصر الذي عليه المستمع).
currentTarget هو العنصر الذي عليه
المستمع الحالي. أثناء الفقاعات،
target يبقى ثابتًا، لكن
currentTarget يتغير مع كل عنصر يمر
عليه الحدث.
متى أستخدم preventDefault ومتى stopPropagation؟
استخدم preventDefault عندما تريد
منع السلوك الافتراضي للمتصفح (إرسال نموذج، فتح
رابط، تحديد نص). استخدم stopPropagation
عندما تريد منع الحدث من الوصول إلى العناصر
الأخرى (مثل منع نقر داخل مودال من الوصول إلى
الخلفية).
هل التفويض يعمل مع كل الأحداث؟
يعمل مع أي حدث ينتشر (bubbles).
معظم أحداث الواجهة تنتشر، لكن بعضها لا (مثل
focus وblur). لهذه،
استخدم focusin وfocusout،
وهما ينتشران.
لماذا يجب استخدام AbortController بدل removeEventListener؟
removeEventListener يتطلب تمرير
نفس مرجع الدالة، وهذا صعب عندما يكون لديك عدة
مستمعات. AbortController يسمح لك
بإدارة جميع المستمعات المرتبطة بمتغير واحد،
وإزالتها دفعة واحدة عند الحاجة. هذا يقلل الأخطاء
ويسهل التنظيف.