اذهب إلى المحتوى

فهم عملية انتشار الأحداث في المتصفح والتحكم فيها عبر جافاسكربت


محمد أمين بوقرة

لنبتدئ بمثال. المعالج التالي مُسندٌ إلى العنصر <div>، لكنّه أيضًا يشتغل عند النقر على الوسوم الداخلة تحته مثل <em> أو <code>.

<div onclick="alert('The handler!')">
  <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>

أليس هذا غريبًا بعض الشيء؟ لماذا يشتغل المعالج المُسنَد إلى <div> إذا كان النقر في الواقع على <em>؟

انتشار اﻷحداث نحو اﻷعلى

مبدأ الانتشار نحو اﻷعلى (bubbling) بسيط.

عندما يقع حدثٌ على عنصرٍ ما، فإنّه يُشغّل المعالجات المسندة إليه، ثم المسندة إلى أبيه، وهكذا صعودًا إلى المعالجات المسندة إلى أسلافه الآخرين.

لنفترض أنّ لدينا ثلاثة عناصر متداخلة P < DIV < FORM ، مع معالجٍ لكلّ منها:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form onclick="alert('form')">FORM
  <div onclick="alert('div')">DIV
    <p onclick="alert('p')">P</p>
  </div>
</form>

يؤدّي النّقر على العنصر <p> الذي بالداخل أوّلًا إلى تشغيل onclick :

  1. المُسند إلى <p> ذاك.
  2. ثم المُسند إلى <div> الذي خارجه.
  3. ثم المُسند إلى <form> الذي خارجه.
  4. وهكذا صعودًا إلى الكائن document.

event-order-bubbling.png

فإذا نقرنا على <p> ، سنرى ثلاثة تنبيهات متتالية: form <- div <- p.

تُعرف على هذه العمليّة بالانتشار نحو اﻷعلى (bubbling)، لأنّ الأحداث تنتشر من العنصر الداخلي صعودًا عبر آبائه كالفقّاعة في الماء.


تنبيه: تنتشر مُعظم اﻷحداث نحو اﻷعلى

ينبغي التنبه في هذه الجملة إلى كلمة "مُعظم".

على سبيل المثال، لا ينتشر الحدث focus نحو اﻷعلى. وسنرى أيضا أمثلة أخرى. لكنّها تبقى استثناءً عوض القاعدة، فمعظم اﻷحداث تنتشر نحو اﻷعلى.


event.target

يمكن للمعالج المسند إلى عنصرٍ أبٍ أن يتحصّل دائمًا على تفاصيل مكان وقوع الحدث.

يُسمّى العنصر اﻷدنى الذي نشأ عنه الحدث بالعنصر "الهدف"، ويمكن الوصول إليه بواسطة event.target.

لاحظ الاختلاف الذي بينه و this (الذي هو نفس event.currentTarget):

  • event.target -- هو العنصر "الهدف" الذي أنشأ الحدث، ولا يتغيّر خلال عمليّة الانتشار نحو اﻷعلى.
  • this -- هو العنصر "الحاليّ"، أي الذي أُسنِد إليه المعالجُ الذي يشتغل حاليّا.

على سبيل المثال، إذا كان لدينا معالجٌ وحيدٌ form.onclick مُسندٌ إلى النموذج <form>، فإنّه يمكنه "التقاط" جميع النقرات داخل النموذج. أيّا كان مكان وقوعها، فإنّها تنتشر نحو اﻷعلى إلى <form> وتشغّل المعالج.

في المعالج form.onclick:

  • this (الذي هو نفس event.currentTarget) هو العنصر <form>، لأنّ المعالج المُشتغل مسندٌ إليه.
  • event.target هو العنصر الذي نُقر عليه داخل النموذج.

يمكنك رؤية ذلك من هنا ، من خلال النقر على مختلف العناصر لإظهار event.target و this في كلّ حالة.

قد يكون event.target هو نفسه this، كما لو نقرنا هنا على العنصر <form> مباشرة.

إيقاف الانتشار نحو اﻷعلى

ينطلق الحدث عند انتشاره نحو اﻷعلى من العنصر الهدف مباشرة. ويواصل الانتشار عادةً إلى أن يصل إلى <html>، ومن ثَمّ إلى الكائن document، بل إنّ بعض اﻷحداث قد تصل إلى window، ويشغّل جميع المعالجات في طريقه إلى هناك.

لكن قد يقرّر أحد المعالجات أن الحدث قد تمّت معالجته ويوقف بذلك عمليّة الانتشار.

يتوقّف الانتشار نحو اﻷعلى بواسطة التابع event.stopPropagation()‎. على سبيل المثال، لا يشتغل المعالج body.onclick هنا عند النقر على <button>:

<body onclick="alert(`the bubbling doesn't reach here`)">
  <button onclick="event.stopPropagation()">Click me</button>
</body>


ملاحظة: ()event.stopImmediatePropagation

إذا أُسنِد إلى عنصرٍ ما عدّةُ معالجات لنفس الحدث، فحتىّ لو أوقف أحدها الانتشار نحو اﻷعلى، ستشتغل المعالجات الأخرى.

بعبارة أخرى، يوقِف التابع event.stopPropagation()‎‎ الانتشار نحو اﻷعلى، لكن ستشتغل بقيّة المعالجات المسندة إلى العنصر الحاليّ.

لإيقاف الانتشار نحو اﻷعلى، ومنع اشتغال بقيّة المعالجات المُسندة إلى العنصر الحاليّ أيضا، يوجد هناك تابع لذلك event.stopImmediatePropagation()‎‎ لا يشتغل بعده معالج.


تنبيه: لا توقف الانتشار نحو اﻷعلى دون الحاجة لذلك!

الانتشار نحو اﻷعلى أمرٌ ملائم. لا توقفه دون سبب وجيه، يكون واضحًا ومُمحصًّا هندسيًّا.

قد يُحدث التابع event.stopPropagation()‎‎ أحيانًا مزالق تتسبّب لاحقًا في مشاكل. على سبيل المثال:

  1. ننشئ قائمة متداخلة. تعالج كلُّ قائمة داخليّة النقرات التي على عناصرها، وتستدعيstopPropagation لتفادي تفعيل القائمة الخارجيّة.
  2. نقرّر بعدها أن نلتقط جميع النقرات على النافذة، لتتبع سلوك المستخدمين (أين ينقر الناس). تقوم بعض أنظمة التحليل بذلك، وعادةً ما تستخدم الشيفرةُ التابعَ document.addEventListener('click'…)‎‎ لالتقاط جميع النقرات.
  3. لن يعمل نظام التحليل على المساحة التي أوقف فيها انتشار النقرات نحو اﻷعلى بواسطة stopPropagation. فيكون بذلك لدينا "منطقة ميّتة" للأسف.

لا توجد في العادة حاجة حقيقيّة لإيقاف الانتشار نحو اﻷعلى. فالمهام التي تبدو أنّها تتطلب ذلك يمكن حلّها بوسائل أخرى. من بين هذه الوسائل، استخدام اﻷحداث المخصّصة (custom events) التي سنتناولها لاحقا. يمكننا أيضا كتابة بياناتٍ على الكائن event في معالج وقراءتها في معالج آخر، ليتسنى بذلك تمرير معلومات إلى المعالجات المسندة إلى الآباء حول المعالجة التي تمت في اﻷسفل.


الانتشار نحو الأسفل

توجد هناك مرحلة أخرى لمعالجة اﻷحداث يُطلق عليها "الانتشار نحو اﻷسفل" (capturing). من النادر استخدامها في شيفرات واقعيّة، لكنّها قد تكون مفيدة أحيانا.

يصِف معيار أحداث DOM ثلاث مراحل لانتشار الأحداث:

  1. مرحلة الانتشار نحو اﻷسفل (Capturing phase) - ينزل الحدث إلى العنصر.
  2. مرحلة الهدف (Target phase) - يصل الحدث إلى العنصر الهدف.
  3. مرحلة الانتشار نحو اﻷعلى (Bubbling phase) - ينتشر الحدث صعودًا من العنصر.

هذه صورة لما يحصل عند النقر على <td> داخل جدول، مأخوذة من المواصفة:

eventflow.png

ما يعني ذلك: بالنقر على <td> ، ينتشر الحدث أوّلا عبر سلسلة الأسلاف نزولًا إلى العنصر (مرحلة الانتشار نحو اﻷسفل)، فيبلغ الهدفَ ويتفعّل هناك (مرحلة الهدف)، ثم ينتشر صعودًا (مرحلة الانتشار نحو اﻷعلى) مستدعيًا المعالجات في طريقه.

اقتصرنا في السابق على مرحلة الانتشار نحو اﻷعلى، لأنّه من النادر استخدام مرحلة الانتشار نحو اﻷسفل. لا تظهر لنا عادةً.

لا يعلم المعالجون الذين عُيّنوا على شكل خاصيّة on<event>‎‎ ، أو على شكل سمة HTML، أو باستخدام addEventListener(event, handler)‎‎ بوسيطين فقط، شيئًا عن الانتشار نحو اﻷسفل، فهم يشتغلون فقط في المرحلتين الثانية والثالثة.

لالتقاط حدثٍ في مرحلة الانتشار نحو اﻷسفل، يجب تغيير قيمة الخيار capture إلى true.

elem.addEventListener(..., {capture: true})

//  {capture: true} فهو اختصار لـ ،"true" أو فقط

elem.addEventListener(..., true)

يمكن أن يأخذ الخيار capture قيمتين:

  • إذا كانت false (افتراضيًّا)، فإنّ المعالج يوضع في مرحلة الانتشار نحو اﻷعلى.
  • إذا كانت true، فإنّ المعالج يوضع في مرحلة الانتشار نحو اﻷسفل.

لاحظ رغم أنّه يوجد رسميًّا ثلاث مراحل، إلّا أن المرحلة الثانية (مرحلة الهدف: عندما يبلغ الحدثُ الهدف) لا تُعالَج بشكل مستقل، بل تشتغل كلٌّ من المعالجات الموضوعة في مرحلتي الانتشار نحو اﻷسفل واﻷعلى في هذه المرحلة أيضا.

لنرى كلًّا من الانتشار نحو اﻷسفل والأعلى حال عملهما:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
  }
</script>

تُسند الشيفرة معالجاتٍ لحدث النّقر إلى جميع العناصر التي في المستند لرؤية أيّها تعمل.

عند النقر على <p>، فإنّ التسلسل يكون كالتالي:

  1. DIV ‹- FORM ‹- BODY ‹- HTML (مرحلة الانتشار نحو اﻷسفل، أوّل المنصتين).
  2. P (مرحلة الهدف، تُفعّل مرّتين لأنّنا وضعنا مُنصتَين اثنين: الانتشار نحو اﻷسفل واﻷعلى).
  3. HTML ‹- BODY ‹- FORM ‹- DIV (مرحلة الانتشار نحو اﻷسفل، ثاني المنصتين).

توجد هناك الخاصيّة event.eventPhase التي تخبرنا برقم المرحلة التي تمّ فيها التقاط الحدث. لكن يندر استخدامها لأنّنا نعلم ذلك من خلال المعالج عادةً.


ملاحظة: لحذف المعالج، يستلزم التابع removeEventListener إعطاء نفس المرحلة

عند إضافة معالجٍ بهذا الشكل addEventListener(..., true)‎‎، فيجب ذكر نفس المرحلة أيضًا في removeEventListener(..., true)‎‎ لحذف المعالج بشكل صحيح.


ملاحظة: تشتغل المعالجات التي أُسندت إلى نفس العنصر وفي نفس المرحلة حسب الترتيب الذي أُنشئت به

إذا كانت لدينا عدّة معالجات للأحداث مُسنَدة إلى نفس العنصر، وفي نفس المرحلة فإنها تشتغل بحسب الترتيب الذي أُنشئت به.

elem.addEventListener("click", e => alert(1)); // من المؤكّد أن يشتغل أوّلا
elem.addEventListener("click", e => alert(2));

الملخص

عندما يقع الحدث، فإن أدنى العناصر الذي وقع فيه الحدث يُعلَّم بالعنصر"الهدف" (event.target).

  • ثم ينتشر الحدث نزولًا من جذر المستند إلى event.target مستدعيًا في طريقه المعالجين المعيّنين بواسطة addEventListener(..., true)‎‎ (القيمة ‎‎ true هي اختصار لـ {capture: true})

  • ثم نُستدعى المعالجات المُسندة إلى العنصر الهدف نفسه.

  • ثم ينتشر الهدف صعودًا من event.target إلى الجذر، مناديًا المعالجات المعيّنة بواسطة on<event>‎‎ و addEventListener دون الوسيط الثالث false/{capture:false}‎‎.

يمكن لأيّ معالجٍ أن يستخدم خاصيّات الكائن event التالية:

  • event.target -- أدنى العناصر الذي نشأ عنه الحدث.
  • event.currentTarget (هو نفس this) -- العنصر الذي يعالج الحدث حاليًّا (الذي أُسند إليه المعالج).
  • event.eventPhase -- المرحلة الحاليّة (1=الانتشار نحو اﻷسفل، 2=الهدف، 3=الانتشار نحو اﻷعلى).

يمكن لأيّ معالجٍ أن يوقف انتشار الحدث من خلال استدعاء event.stopPropagation()‎‎، لكن لا يُنصح بذلك، لأنّه لا يمكن التأكّد حقًّا من عدم الحاجة إليه في الأعلى، ربّما في أمورٍ مختلفة تماما.

يندر جدًّا استخدام مرحلة الانتشار نحو اﻷسفل، إذ تُعالَج الأحداث عادةً عند انتشارها نحو اﻷعلى. هناك سبب منطقي وراء ذلك.

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

ينطبق الأمر على معالجات الأحداث. تكون الشيفرة المسؤولة عن إسناد المعالج إلى عنصرٍ ما أعلم بالتفاصيل المتعلّقة بذلك العنصر ومالذي يفعله. فيكون المعالج المسند إلى العنصر<td> خصيصا أنسب بتولي أمر ذلك العنصر بالذات، إذ يعلم كلّ شيء بخصوصه، وينبغي أن تُمنح له الفرصة أوّلا. ثم يأتي أبوه المباشر، الذي يكون له اطّلاع على السياق لكنّه أقلّ معرفةً به. وهكذا إلى أعلى العناصر، الذي يعالج الأمور العامّة ويكون آخرهم اشتغالا.

يرسي مفهوم الانتشار نحو اﻷسفل واﻷعلى اﻷساس لموضوع "تفويض الأحداث"، الذي يُعدّ نمط معالجةٍ للأحداث قويًّا للغاية. سندرسه في المقال التالي.

ترجمة -وبتصرف- للمقال Bubbling and capturing من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...