۲۹ سپتامبر ۲۰۲۲

بالارفتن و گرفتن

با یک مثال شروع می‌کنیم.

این کنترل‌کننده به <div> اختصاص داده شده، اما در صورتی که هر تگ داخل آنرا مانند <em> یا <code> کلیک کنید، باز هم اجرا می‌شود:

<div onclick="alert('کنترل‌کننده!')">
  <em>اگر روی <code>EM</code> کلیک کنید، کنترل‌کننده روی <code>DIV</code> اجرا می‌شود.</em>
</div>

این رفتار کمی عجیب نیست؟ چرا کنترل‌کننده‌ی روی <div> باید زمانی اجرا شود که کلیک در اصل روی <em> بوده است؟

بالا رفتن حبابی

رفتار بالارفتن حبابی ساده است.

وقتی که رویدادی روی یک عنصر اتفاق می‌افتد، اول کنترل‌کننده‌ها را روی خودش اجرا می‌کند، بعد روی عنصر پدرش اجرا می‌کند، و همینطور تا بالاترین اجداد آن.

فرض کنیم که سه عنصر تو در تو به صورت ‍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> اول باعث فراخوانی onlick به صورت زیر می‌شود:

  1. روی <p>.
  2. روی <div> بیرونی.
  3. روی <form> بیرونی.
  4. همینطور بالا می‌رود تا روی شئ document هم اجرا شود.

پس اگر روی <p> کلیک کنیم، سه پیام به صورت روبرو مشاهده می‌کنیم: pdivform.

این روند اصطلاحاً “بالارفتن حبابی” یا “bubbling” است، چون رویدادها از داخلی‌ترن عنصر تا عنصرهای والد مانند یک حباب در آب، بالا می‌روند.

تقریبا همه رویدادها بالا می‌روند.

کلمه کلیدی در این جمله “تقریبا” است.

برای مثال، یک رویداد focus بالا نمی‌روند. مثال‌های دیگری نیز وجود دارد که با آنها آشنا خواهیم شد. با این حال این یک استثنا است تا یک قانون. بیشتر رویدادها بالا می‌روند.

ویژگی event.target

یک کنترل‌کننده روی یک عنصر والد همیشه می‌تواند جزئیاتی درباره اینکه رویداد درواقعیت کجا اتفاق افتاده است را بگیرد.

عمیق‌ترین عنصری که باعث فراخوانی یک رویداد شده را عنصر هدف می‌نامند، که می‌توانیم با event.target به آن دسترسی یابیم.

به تفاوت آن با this دقت کنید (=event.currentTarget):

  • event.target – عنصر “هدف” است که رویداد را برای اولین بار فراخوانی کرده، در طول روند بالارفتن تغییر نمی‌کند.
  • this – عنصر “فعلی” است، که رویداد در حال حاضر روی آن در حال اجرا است.

برای مثال، اگر فقط یک کنترل‌کننده روی form.onclick داشته باشیم، سپس می‌تواند همه کلیک‌های داخل فرم را “بگیرد”. بدون توجه به اینکه کلیک کجا اتفاق افتاده، همه راه را تا <form> بالا می‌رود و کنترل‌کننده را اجرا می‌کند.

درون کنترل‌کننده form.onclick:

  • this (=event.currentTarget) همان عنصر <form>‌ است، چون کنترل‌کننده روی آن اجرا شده. ‍‍- event.target عنصری درون فرم که در اصل کلیک روی آن اتفاق افتاده.

ببینید:

نتیجه
script.js
example.css
index.html
form.onclick = function(event) {
  event.target.style.backgroundColor = '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(`رویداد به اینجا نمی‌رسد`)">
  <button onclick="event.stopPropagation()">کلیک کنید</button>
</body>
event.stopImmediatePropagation()

اگر که یک عنصر چند کنترل‌کننده برای یک رویداد داشته باشد، در صورتی که حتی یکی از آنها از بالارفتن رویداد جلوگیری کند، بقیه باز هم اجرا می‌شوند.

به عبارت دیگر، event.stopPropagation() حرکت رو به بالا را متوقف می‌کند، اما روی همین عنصر فعلی، بقیه کنترل‌کننده‌ها اجرا می‌شوند.

برای توقف بالارفتن و جلوگیری از اجرای بقیه کنترل‌کننده‌ها عنصر فعلی، یک متد به نام event.stopImmediatePropagation() وجود دارد. بعد از آن هیچ کنترل‌کننده دیگری اجرا نمی‌شود.

اگر نیاز نیست، بالارفتن رویداد را متوقف نکنید‍‍‍!

رفتار بالارفتن ساده است. بدون یک نیاز واقعی آنرا متوقف نکنید: بدیهی و از نظر معماری خوب تدبیر شده باشد.

بعضی اوقات event.stopPropagation() یک مشکلی ایجاد می‌کند که ممکن است بعدا باعث وقوع دردسرهایی شود.

برای مثال:

  1. ما یک منوی تو در تو ایجاد می‌کنیم. هر زیرمنو کلیک‌ها را روی عناصر کنترل می‌کند و stopPropagation را صدا می‌زند پس منوی بیرونی فعال نمی‌شود.
  2. بعدا تصمیم می‌گیریم برای پی‌گیری رفتار کاربر (جایی که کاربر کلیک می‌کند) کلیک‌ها را روی کل پنجره دریافت کنیم. بعضی از سیستم‌های تحلیل این کار را انجام می‌دهند. معمولا کد از document.addEventListener('click'…) برای گرفتن همه کلیک ها استفاده می‌کند.
  3. سیستم تحلیل ما جایی که کلیک‌ها توسط stopPropagation متوقف می‌شوند کار نمی‌کند. متاسفانه ما یک “محدوده مرده” داریم.

معمولا نیاز واقعی به جلوگیری از رفتار بالارفتن رویدادها نیست. کاری که ظاهرا نیاز به آن دارد، ممکن است با روش‌های دیگر قابل حل باشد. یکی از آنها استفاده از رویدادهای دستی است که بعدا به آنها می‌پردازیم. همچنین می‌توانیم در یک کنترل‌کننده اطلاعاتی را داخل شئ event بنویسیم و در کنترل‌کننده دیگری آن اطلاعات را بخوانیم. سپس آنرا به به کنترل‌کننده‌هایی که روی عناصر والد هستند بفرستیم. با اینکار.

گرفتن

یک فاز دیگر در پردازش رویدادها وجود دارد به نام “گرفتن” یا “capturing”. به ندرت در کد واقعی استفاده می‌شود، اما گاهی اوقات کارآمد است.

رویداد‌های DOM استاندارد سه فاز از انتشار رویداد معرفی می‌کنند:

  1. فاز گرفتن – رویداد پایین می‌رود تا به عنصر برسد.
  2. فاز هدف – رویداد به عنصر هدف می‌رسد.
  3. فاز بالارفتن – رویداد از عنصر بالا می‌رود.

این یک عکس از یک کلیک روی <td> داخل یک جدول است، که شامل مشخصات زیر است

اینگونه که: برای یک کلیک روی <td> رویداد اول از زنجیره اجداد پایین می‌آید تا به عنصر برسد‌(گرفتن). بعد به عنصر هدف می‌رسد و بعد بالا می‌رود (بالارفتن) و کنترل‌کننده‌ها را در مسیر صدا می‌زند.

قبلا تنها درباره بالارفتن رویداد صحبت کردیم، چون که فاز گرفتن به ندرت استفاده می‌شود. معمولا برای ما نمایان نیست.

کنترل‌کننده‌هایی که با استفاده از خاصیت on<event> یا صفت‌های HTML یا با متد با دو ورودی addEventListener(event, handler) اضافه می‌شوند، چیزی درباره گرفتن نمی‌دانند. فقط در فازهای دوم و سوم اجرا می‌شوند.

برای گرفتن یک رویداد در فاز گرفتن، باید که ورودی سوم addEventListener را true قرار دهیم.

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

// or, just "true" is an alias to {capture: true}
elem.addEventListener(..., true)

برای آخرین ورودی که اختیاری دو مقدار می‌تواند وجود داشته باشد:

  • اگر 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(`گرفتن: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`بالارفتن: ${elem.tagName}`));
  }
</script>

این کد روی هر عنصر یک کنترل‌کننده کلیک تنظیم می‌کند تا ببینیم کدام یک کار می‌کنند.

اگر روی <p> کلیک کنیم، بعد ترتیب به این صورت است:

  1. HTMLBODYFORMDIV -> P (فاز گرفتن, اولین شنونده):
  2. PDIVFORMBODYHTML (فاز بالارفتن, شنونده دوم).

توجه کنید که P دوبار نمایش داده می‌شود: در پایان گرفتن و در زمان شروع بالارفتن.

خاصیت event.eventPhase به ما شماره فازی که در آن رویداد فراخوانی شده را می‌گوید. اما به ندرت استفاده می‌شود چون معمولا در کنترل‌کننده آنرا می‌دانیم.

برای حذف کنترل‌کننده، removeEventListener به فاز یکسان نیاز دارد

اگر ما addEventListener(..., true) را فراخوانی کنیم، سپس باید فاز یکسان را در removeEventListener(..., true) ذکر کنیم تا به درستی کنترل‌کننده را حذف کنیم.

کنترل‌کننده‌های یک المان و یک فاز به ترتیب تنظیم‌شدن‌شان اجرا می‌شوند

اگر ما چند کنترل‌کننده رویداد روی یک فاز داشته باشیم، که با addEventListener به یک المان داده شده‌اند، آن را دقیقا با ترتیب ایجاد شدن اجرا می‌شوند:

elem.addEventListener("click", e => alert(1)); // تضمین می‌شود که اول فعال شود
elem.addEventListener("click", e => alert(2));
event.stopPropagation() در حین گرفتن از بالارفتن حبابی جلوگیری می‌کند

متد event.stopPropagation() و همزاد خود event.stopImmediatePropagation() می‌توانند در فاز گرفتن هم فراخوانی شوند. سپس نه تنها گرفتن بیشتر متوقف می‌شود بلکه بالارفتن حبابی هم همینطور.

به عبارتی دیگر، معمولا رویداد ابتدا پایین می‌رود («گرفتن») و سپس بالا («بالارفتن حبابی»). اما اگر event.stopPropagation() در حین فاز گرفتن فراخوانی شود، سپس سفر رویداد متوقف می‌شود و هیچ بالارفتن حبابی اتفاق نمی‌افتد.

خلاصه

روند کنترل رویداد:

  • پس رویداد از ریشه که شئ document است به پایین حرکت می‌کند تا به event.target برسد، در حین حرکت کنترل‌کننده‌هایی که با addEventListener(..., true) تنظیم شده‌اند را صدا می‌زند (true مخفف ‍{capture: true}).
  • سپس کنترل‌کننده‌های روی خود هدف صدا زده می‌شوند.
  • سپس رویداد بالا می‌رود و از event.target به ریشه می‌رود، کنترل‌کننده‌هایی که با استفاده از on<event>، صفت‌های HTML و ‍addEventListener بدون ورودی سوم یا با ورودی سوم به صورت false/{capture:false} در مسیر خود فرا می‌خواند.

هر کنترل‌کننده به خصوصیات یک شئ event دسترسی دارد:

  • event.target – عمیق‌ترین عنصری که رویداد را ایجاد کرده.
  • event.currentTarget (=this) – عنصر فعلی که رویداد را کنترل می‌کند (عنصری که کنترل‌کننده روی آن است)
  • event.eventPhase – فاز کنونی‌ (گرفتن=1، بالارفتن=3)

هر کنترل‌کننده‌ای می‌تواند رویداد را با event.stopPropagation() متوقف کند، اما این روش پیشنهاد نمی‌شود چون که همیشه مطمئن نیستیم که رویداد را در مرحله‌های بالاتر نیاز نخواهیم داشت، شاید برای کارهایی کاملا متفاوت.

فاز گرفتن به ندرت استفاده می‌شود، معمولا رویدادها را در فاز بالارفتن کنترل می‌کنیم. و یک منطق پشت آن است.

در دنیای واقعی، زمانی که تصادفی رخ‌ میدهد، مقامات محلی زودتر عکس العمل نشان می‌دهند. آنها بهتر از هر کس دیگری درباره محلی که این حادثه رخ داده اطلاعات دارند. بعد از آنها در صورت نیاز مقامات بالاتر.

برای کنترل‌کننده‌های رویدادها نیز به همین شکل است. کدی که یک کنترل‌کننده برای یک عنصر به خصوص اختصاص می‌دهد، بیشترین جزئیات را درباره آن عنصر و کاری که انجام می‌دهد می‌داند. کنترل‌کننده روی یک <td> به خصوص ممکن است فقط برای دقیقا همان <td> مناسب باشد، همه چیز را درباره آن می‌داند. پس اولین موقعیت را باید داشته باشد. بعد اولین والد او همچنین درباره مفاهیم اطلاعات دارد، اما مقداری کمتر و همینطور تا بالاترین عنصری که مفاهیم کلی را می‌داند و آخرین کنترل‌کننده را اجرا می‌کند.

بالارفتن و گرفتن پایه‌گذاری برای “واگذاری رویداد” هستند. – یک الگوی بسیار قدرتمند برای کنترل رویداد‌ها که در بخش‌های بعدی آموزش خواهیم دید.

نقشه آموزش