١١ أغسطس ٢٠٢٠

التدفق و الإلتقاط

لنبدأ بمثال.

تم تعيين هذا المعالج إلى <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:

  1. على ذلك ال <p>.
  2. ثم على الخارجي <div>.
  3. ثم على الخارجي <form>.
  4. وهكذا إلى أعلى حتى document كائن.

لذا، إذا نقرت فوق <p>, سنرى بعد ذلك 3 تنبيهات: pdivform.

تسمى العملية “التدفق”, لأن الأحداث “تدفق” من العنصر الداخلي إلى أعلى إلى الآباء مثل فقاعة في الماء.

تقريبا كل الأحداث تتدفق.

الكلمة الأساسية في هذه العبارة هي “تقريبا”.

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

event.target

يمكن أن يحصل المعالج الموجود على العنصر الأصل دائمًا على التفاصيل حول المكان الذي حدث فيه بالفعل.

ويسمى العنصر الأكثر تداخلا والذي تسبب في الحدث العنصر الاساسي , الذي يمكن الوصول إليه كـ event.target.

لاحظ الاختلافات من this (=event.currentTarget):

  • event.target – هو عنصر “الهدف” الذي بدأ الحدث، ولا يتغير من خلال عملية التدفق.
  • this – هو العنصر “الحالي”، العنصر الذي يحتوي على معالج قيد التشغيل حاليًا.

فعلى سبيل المثال, إذا كان لدينا معالج واحد form.onclick, ثم يمكن “الامساك” بكل النقرات داخل form. بغض النظر عن مكان حدوث النقر, سوف تدفق لأعلى <form> ويتم تشغيل المعالج.

في معالج form.onclick :

  • this (=event.currentTarget) هي العنصر <form> , لأن المعالج يعمل عليه.
  • event.target العنصر الفعلي داخل النموذج الذي تم النقر فوقه.

تحقق من ذلك:

نتيجة
script.js
example.css
index.html
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.stopImmediatePropagation

إذا كان هناك عنصر لديه العديد من معالجات الأحداث على حدث واحد، ثم حتى إذا توقف أحدهم عن تنفيذ التدفق، فإن العناصر الأخرى لا تزال تنفذ التدفق.

وبعبارة أخرى, ()event.stopPropagation توقف الحركة لأعلى, ولكن على العنصر الحالي فإن كل المعالجات الأخرى سوف تعمل.

لإيقاف تشغيل التدفق ومنع المعالجات الموجودة على العنصر الحالي من التشغيل, هناك دالة ()event.stopImmediatePropagation. بعد ذلك لن تقوم أي معالجات أخرى بالتنفيذ.

لا توقف التدفق دون الحاجة إلي ذلك!

التدفق مناسب. لا توقف ذلك دون حاجة حقيقية: فكر وقم بالادارة بوضوح .

أحيانا ()event.stopPropagation ينشئ مخاطر مخفية قد تصبح مشاكل في وقت لاحق.

على سبيل المثال:

  1. نحن ننشئ قائمة متداخلة. تعالج كل قائمة فرعية النقرات على العناصر وتنادي علي stopPropagation وبذلك لا يتم تشغيل القائمة الخارجية.
  2. وفي وقت لاحق قررنا التقاط النقرات على النافذة بالكامل, لتعقب سلوك المستخدمين (حيث يقوم الأشخاص بالنقر). والواقع أن بعض الأنظمة التحليلية تفعل ذلك. عادة الكود المستخدم هو document.addEventListener('click'…) لالتقاط كل النقرات.
  3. لن يعمل التحليل لدينا في المنطقة التي تتوقف فيها النقرات بواسطة stopPropagation. للأسف، قد وصلنا الي “منطقة ميتة”.

لا توجد حاجة حقيقية عادة لمنع حدوث التدفق .فالمهمة تبدو وكأنها تتطلب حل هذه المشكلة بوسائل أخرى. ومن بين هذه الأحداث استخدام أحداث مخصصة,سنتناولها لاحقًا. كما يمكننا كتابة بياناتنا في كائن event في معالج واحد وقراءته في معالج آخر, حتى نتمكن من تمرير معلومات إلى المدعين حول معالجة المعلومات أدناه.

الإلتقاط

هناك مرحلة أخرى من معالجة الأحداث تسمى “الإلتقاط”. ونادرًا ما يتم استخدامها في الكود, ولكن قد يكون مفيداً في بعض الأحيان.

معيارDOM Events يصف ثلاث مراحل من نشر الحدث:

  1. مرحلة الالتقاط-- ينتقل الحدث إلى اسفل ليصل الي العنصر.
  2. مرحلة الهدف – يصل الحدث إلى العنصر المستهدف.
  3. مرحلة التدفق – يتدفق الحدث لأعلي من العنصر.

إليك صورة النقر فوق <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>, ثم يكون التسلسل:

  1. HTMLBODYFORMDIV (مرحلة الالتقاط, المستمع الأول):
  2. P (مرحلة الهدف, يتم تشغيلها مرتين, كما وضعنا مستمعين: الالتقاط والتدفق)
  3. DIVFORMBODYHTML (مرحلة التدفق, المستمع الثاني).

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

لإزالة المعالج, removeEventListener يحتاج إلى المرحلة نفسها

اذا وضعنا addEventListener(..., true), يتعين علينا أن نذكر نفس المرحلة في removeEventListener(..., true) لإزالة المعالج بشكل صحيح.

المستمعون على نفس العنصر ونفس المرحلة يتم تشغيلهم بالنسبة لترتيبهم

إذا كان لدينا العديد من معالجات الأحداث في نفس المرحلة, تم تعيينها للعنصر نفسه مع addEventListener, يتم تشغيلها بنفس الترتيب الذي تم إنشاؤها به:

elem.addEventListener("click", e => alert(1)); // guaranteed to trigger first
elem.addEventListener("click", e => alert(2));

الخلاصة

عندما يحدث حدث ما – يكون العنصر الأكثر تداخل حيث يحدث يسمي “العنصر المستهدف” (event.target).

  • ثم ينتقل الحدث لأسفل من جذر المستند إلى event.target, مناديا علي المعالجات التي تم تعيينها مع addEventListener(..., true) بطريقة ما (true اختصار لـ {capture: true}).
  • ثم يتم استدعاء المعالجات على العنصر الهدف نفسه.
  • ثم يتم قذف الحدث لأعلي منevent.target الي الجذر, مناديا علي المعالجات التي تم تعيينه باستخدام on<event> وaddEventListener مع او بدون القيمة الثالثة الممرة false/{capture:false}.

يمكن لكل معالج الوصول إلى خصائص كائن “الحدث”:

  • event.target – العنصر الأعمق الذي نشأ عن الحدث.
  • event.currentTarget (=this) – العنصر الحالي الذي يعالج الحدث (لذي يكون المعالج عليه)
  • event.eventPhase – المرحلة الحالية (الالتقاط=1, الهدف=2, التدفق=3).

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

تُستخدم مرحلة الالتقاط نادرًا جدًا, وعادة ما نتعامل مع الأحداث الجارية في مرحلة التدفق. وهناك منطق وراء ذلك.

في العالم الحقيقي، حين يقع حادث ما, فالسلطات المحلية ترد أولاً. فهم يعرفون المنطقة التي حدث فيا جيدا. ثم سلطات أعلى مستوى إذا لزم الأمر.

نفس الشيء بالنسبة لمعالجات الأحداث. الكود الذي يقوم بتعيين المعالج على عنصر معين يعرف الحد الأقصى من التفاصيل حول العنصر وما يفعله. قد يكون معالج على <td> معين مناسبا بالضبط ل <td>,فهو يعرف كل شيء عنه, لذا فلابد وأن تحظى بالفرصة أولاً. ثم يعرف الوالد المباشر أيضاً السياق, ولكن أقل قليلاً, وهكذا حتى العنصر العلوي الذي يعالج المفاهيم العامة ويدير العنصر الأخير.

وضع التدفق والالتقاط الأساس لـ “تفويض الحدث” – نمط قوي للغاية للتعامل مع الأحداث ندرسه الفصل التالي.

خريطة الدورة التعليمية

التعليقات

إقرأ هذا قبل أن تضع تعليقًا…
  • إذا كان لديك اقتراحات أو تريد تحسينًا - من فضلك من فضلك إفتح موضوعًا فى جيتهاب أو شارك بنفسك بدلًا من التعليقات.
  • إذا لم تستطع أن تفهم شيئّا فى المقال - وضّح ماهو.
  • إذا كنت تريد عرض كود استخدم عنصر <code> ، وللكثير من السطور استخدم <pre>، ولأكثر من 10 سطور استخدم (plnkr, JSBin, codepen…)