لنبدأ بمثال.
تم تعيين هذا المعالج إلى <div>
, ولكن يتم تشغيله أيضاً إذا نقرت فوق أي علامة متداخلة مثل <em>
أو <code>
:
<div onclick="alert('المعالج!')">
<em> <code>DIV</code> فانه يتم تشغيل المعالج علي. <code>EM</code>اذا نقرت علي,</em>
</div>
أليس هذا غريباً بعض الشيء؟ لماذا يقوم المعالج بالتشغيل علي <div>
إذا كانت النقرت الفعلية علي <em>
?
التدفق
إن مبدأ التدفق بسيط.
عندما يحدث حدث على عنصر، فإنه يقوم أولاً بتشغيل المعالجات عليه، ثم على والده، ثم على طول الطريق إلى أعلى على أسلافه الآخرين.
لنفترض أن لدينا 3 عناصر متداخلة FORM > DIV > P
مع معالج على كل منها:
<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
:
- على ذلك ال
<p>
. - ثم على الخارجي
<div>
. - ثم على الخارجي
<form>
. - وهكذا إلى أعلى حتى
document
كائن.
لذا، إذا نقرت فوق <p>
, سنرى بعد ذلك 3 تنبيهات: p
→ div
→ form
.
تسمى العملية “التدفق”, لأن الأحداث “تدفق” من العنصر الداخلي إلى أعلى إلى الآباء مثل فقاعة في الماء.
الكلمة الأساسية في هذه العبارة هي “تقريبا”.
فعلى سبيل المثال, الحدث focus
لا يتدفق. وهناك أمثلة أخرى أيضاً، سوف نلتقي بها. ولكن ما زال هذا يشكل استثناءً وليس قاعدة، حيث أن أغلب الأحداث لا تزال تتدفق.
event.target
يمكن أن يحصل المعالج الموجود على العنصر الأصل دائمًا على التفاصيل حول المكان الذي حدث فيه بالفعل.
ويسمى العنصر الأكثر تداخلا والذي تسبب في الحدث العنصر الاساسي , الذي يمكن الوصول إليه كـ event.target
.
لاحظ الاختلافات من this
(=event.currentTarget
):
event.target
– هو عنصر “الهدف” الذي بدأ الحدث، ولا يتغير من خلال عملية التدفق.this
– هو العنصر “الحالي”، العنصر الذي يحتوي على معالج قيد التشغيل حاليًا.
فعلى سبيل المثال, إذا كان لدينا معالج واحد form.onclick
, ثم يمكن “الامساك” بكل النقرات داخل form. بغض النظر عن مكان حدوث النقر, سوف تدفق لأعلى <form>
ويتم تشغيل المعالج.
في معالج form.onclick
:
this
(=event.currentTarget
) هي العنصر<form>
, لأن المعالج يعمل عليه.event.target
العنصر الفعلي داخل النموذج الذي تم النقر فوقه.
تحقق من ذلك:
form.onclick = function(event) {
event.target.style.backgroundColor = 'yellow';
// chrome needs some time to paint yellow
setTimeout(() => {
alert("target = " + event.target.tagName + ", this=" + this.tagName);
event.target.style.backgroundColor = ''
}, 0);
};
form {
background-color: green;
position: relative;
width: 150px;
height: 150px;
text-align: center;
cursor: pointer;
}
div {
background-color: blue;
position: absolute;
top: 25px;
left: 25px;
width: 100px;
height: 100px;
}
p {
background-color: red;
position: absolute;
top: 25px;
left: 25px;
width: 50px;
height: 50px;
line-height: 50px;
margin: 0;
}
body {
line-height: 25px;
font-size: 16px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="example.css">
</head>
<body>
<code>event.target</code> و <code>this</code> للمقارنة بالنقر يوضح ان كلا من:
<form id="form">FORM
<div>DIV
<p>P</p>
</div>
</form>
<script src="script.js"></script>
</body>
</html>
من المحتمل أن 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()">انقر علي</button>
</body>
إذا كان هناك عنصر لديه العديد من معالجات الأحداث على حدث واحد، ثم حتى إذا توقف أحدهم عن تنفيذ التدفق، فإن العناصر الأخرى لا تزال تنفذ التدفق.
وبعبارة أخرى, ()event.stopPropagation
توقف الحركة لأعلى, ولكن على العنصر الحالي فإن كل المعالجات الأخرى سوف تعمل.
لإيقاف تشغيل التدفق ومنع المعالجات الموجودة على العنصر الحالي من التشغيل, هناك دالة ()event.stopImmediatePropagation
. بعد ذلك لن تقوم أي معالجات أخرى بالتنفيذ.
التدفق مناسب. لا توقف ذلك دون حاجة حقيقية: فكر وقم بالادارة بوضوح .
أحيانا ()event.stopPropagation
ينشئ مخاطر مخفية قد تصبح مشاكل في وقت لاحق.
على سبيل المثال:
- نحن ننشئ قائمة متداخلة. تعالج كل قائمة فرعية النقرات على العناصر وتنادي علي
stopPropagation
وبذلك لا يتم تشغيل القائمة الخارجية. - وفي وقت لاحق قررنا التقاط النقرات على النافذة بالكامل, لتعقب سلوك المستخدمين (حيث يقوم الأشخاص بالنقر). والواقع أن بعض الأنظمة التحليلية تفعل ذلك. عادة الكود المستخدم هو
document.addEventListener('click'…)
لالتقاط كل النقرات. - لن يعمل التحليل لدينا في المنطقة التي تتوقف فيها النقرات بواسطة
stopPropagation
. للأسف، قد وصلنا الي “منطقة ميتة”.
لا توجد حاجة حقيقية عادة لمنع حدوث التدفق .فالمهمة تبدو وكأنها تتطلب حل هذه المشكلة بوسائل أخرى. ومن بين هذه الأحداث استخدام أحداث مخصصة,سنتناولها لاحقًا. كما يمكننا كتابة بياناتنا في كائن event
في معالج واحد وقراءته في معالج آخر, حتى نتمكن من تمرير معلومات إلى المدعين حول معالجة المعلومات أدناه.
الإلتقاط
هناك مرحلة أخرى من معالجة الأحداث تسمى “الإلتقاط”. ونادرًا ما يتم استخدامها في الكود, ولكن قد يكون مفيداً في بعض الأحيان.
معيارDOM Events يصف ثلاث مراحل من نشر الحدث:
- مرحلة الالتقاط-- ينتقل الحدث إلى اسفل ليصل الي العنصر.
- مرحلة الهدف – يصل الحدث إلى العنصر المستهدف.
- مرحلة التدفق – يتدفق الحدث لأعلي من العنصر.
إليك صورة النقر فوق <td>
داخل جدول، مأخوذ من المواصفات:
وهذا هو: بالنقر فوق <td>
يمر الحدث أولاً عبر سلسلة الأجداد نزولاً إلى العنصر (مرحلة الالتقاط), ثم تصل إلى الهدف وتتسبب في تشغيل ذلك الهدف (مرحلة الهدف), ثم يرتفع لأعلي (مرحلة التدفق), مناديا للمعالجين في طريقه.
قبل أن نتحدث عن التدفق فقط، لأن مرحلة الالتقاط نادراً ما تستخدم. عادة ما تكون غير مرئية بالنسبة لنا.
تمت إضافة معالجات باستخدام خاصية on<event>
-او باستخدام خواص HTML او باستخدام two-argument addEventListener(event, handler)
لا تعرف أي شيء عن الالتقاط, وهي تعمل فقط على المرحلتين الثانية والثالثة.
لالتقاط حدث في مرحلة الالتقاط, يجب أن نضبط اختيار المعالج capture
الي true
:
elem.addEventListener(..., {capture: true})
// {capture: true} هو اسم مستعار لـ "true" أو فقط
elem.addEventListener(..., true)
توجد قيمتان محتملتان لـلاختيار capture
:
- اذا كانت
false
(default),يتم ضبط المعالج على مرحلة التدفق. - اذا كانت
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>
, ثم يكون التسلسل:
HTML
→BODY
→FORM
→DIV
(مرحلة الالتقاط, المستمع الأول):P
(مرحلة الهدف, يتم تشغيلها مرتين, كما وضعنا مستمعين: الالتقاط والتدفق)DIV
→FORM
→BODY
→HTML
(مرحلة التدفق, المستمع الثاني).
توجد خاصيةevent.eventPhase
وهي تخبرنا بعدد المرحلة التي يتم فيها وقوع الحدث. ولكنها نادرًا ما يتم استخدامها، لأننا نعرفه عادةً في المعالج.
removeEventListener
يحتاج إلى المرحلة نفسهاإذا كان لدينا العديد من معالجات الأحداث في نفس المرحلة, تم تعيينها للعنصر نفسه مع addEventListener
, يتم تشغيلها بنفس الترتيب الذي تم إنشاؤها به:
elem.addEventListener("click", e => alert(1)); // guaranteed to trigger first
elem.addEventListener("click", e => alert(2));
## الخلاصة
عندما يحدث حدث ما -- يكون العنصر الأكثر تداخل حيث يحدث يسمي "العنصر المستهدف" (`event.target`).
- Then the event moves down from the document root to `event.target`, calling handlers assigned with `addEventListener(..., true)` on the way (`true` is a shorthand for `{capture: true}`).
- Then handlers are called on the target element itself.
- Then the event bubbles up from `event.target` to the root, calling handlers assigned using `on<event>`, HTML attributes and `addEventListener` without the 3rd argument or with the 3rd argument `false/{capture:false}`.
يمكن لكل معالج الوصول إلى خصائص كائن "الحدث":
- `event.target` -- العنصر الأعمق الذي نشأ عن الحدث.
- `event.currentTarget` (=`this`) -- العنصر الحالي الذي يعالج الحدث (لذي يكون المعالج عليه)
- `event.eventPhase` -- المرحلة الحالية (الالتقاط=1, الهدف=2, التدفق=3).
يمكن أن يوقف معالج الأحداث الحدث باستخدام `event.stopPropagation()`, ولكن هذا غير موصى به, لأننا لا نستطيع أن نتأكد من أننا لن نحتاج إليها أعلاه، ربما لأشياء مختلفة تماماً.
تُستخدم مرحلة الالتقاط نادرًا جدًا, وعادة ما نتعامل مع الأحداث الجارية في مرحلة التدفق. وهناك منطق وراء ذلك.
في العالم الحقيقي، حين يقع حادث ما, فالسلطات المحلية ترد أولاً. فهم يعرفون المنطقة التي حدث فيا جيدا. ثم سلطات أعلى مستوى إذا لزم الأمر.
The same for event handlers. The code that set the handler on a particular element knows maximum details about the element and what it does. A handler on a particular `<td>` may be suited for that exactly `<td>`, it knows everything about it, so it should get the chance first. Then its immediate parent also knows about the context, but a little bit less, and so on till the very top element that handles general concepts and runs the last one.
وضع التدفق والالتقاط الأساس لـ "تفويض الحدث" -- نمط قوي للغاية للتعامل مع الأحداث ندرسه الفصل التالي.