IlmHamroh
JavaScript Full-stack/9-qism. JavaScript brauzerda va debugging14/29-dars19 daqiqa
Mundarija (33)

JavaScript hodisa tarqalishi: capture, bubble va stopPropagation

Qisqacha: Elementni bossangiz, hodisa faqat o'sha elementda bo'lmaydi. U window dan pastga — bosilgan elementgacha tushadi (capture), keyin u yerdan yana yuqoriga ko'tariladi (bubble). Shuning uchun ichki tugmani bossangiz, uning ota-bobolaridagi tinglovchilar ham ishlaydi. Oddiy addEventListener bubble bosqichida tinglaydi, { capture: true } esa capture bosqichida. stopPropagation() sayohatni to'xtatadi — lekin uni kamdan-kam ishlating.

Bu darsda

  • Hodisaning uch bosqichini (capture → target → bubble) tushuntirib, tinglovchilar qaysi tartibda ishlashini oldindan ayta olasiz.
  • Capture bosqichida tinglashni bilasiz va u qachon kerakligini tushunasiz.
  • stopPropagation va stopImmediatePropagation farqini, ularning xavfini va o'rniga nima ishlatishni bilasiz.
  • preventDefault va stopPropagation ni chalkashtirmaysiz.
  • Qaysi hodisalar ko'tarilmasligini (focus, mouseenter) va ularning ko'tariladigan juftini bilasiz.

Oldin bilishingiz kerak: Event obyekti va standart harakatlar, DOM bo'ylab harakatlanish.

1. Nega bu kerak?

O'tgan darsda g'alati narsani ko'rdik. Tinglovchi tugmada edi, biz esa uning ichidagi + belgisini (span) bosdik. Tinglovchi baribir ishladi, event.target esa span ni ko'rsatdi. Demak hodisa span dan tugmaga qandaydir "yetib bordi".

Endi «Bahor» menyusidan real muammo. Taom kartasini bossangiz, taom haqida batafsil oyna ochiladi. Kartaning ichida «Savatga» tugmasi bor. Mehmon «Savatga» ni bosdi — va taom savatga qo'shildi, ustiga oyna ham ochildi. Mehmon buni so'ramagan edi.

Ikkala holatning sababi bitta: hodisa tarqalishi (event propagation). Hodisa DOM daraxti bo'ylab sayohat qiladi va yo'lida uchragan har tinglovchini ishga tushiradi. Bu sayohatni tushunsangiz — ikkala muammo ham tushunarli, keyingi darsdagi kuchli naqsh — Event delegation — ham.

2. Bubble: hodisa yuqoriga ko'tariladi

2.1 Birinchi tajriba

Kartada ham, uning ichidagi tugmada ham click tinglovchisi bor:

html
<ul id="menyu">
  <li class="taom-karta">
    Osh
    <button type="button">Savatga</button>
  </li>
</ul>
<script>
  const menyu = document.getElementById("menyu");
  const karta = document.querySelector(".taom-karta");
  const tugma = document.querySelector(".taom-karta button");

  tugma.addEventListener("click", () => console.log("tugma"));
  karta.addEventListener("click", () => console.log("karta"));
  menyu.addEventListener("click", () => console.log("menyu"));
</script>

«Savatga» ni bosing. Konsolda uch qator: tugma, karta, menyu. Endi "Osh" so'zining ustidan bosing: karta, menyu. Tugmaning tinglovchisi ishlamadi — bosish tugmaga tegmadi.

Qoida: hodisa avval bosilgan elementda ishlaydi, keyin uning otasida, keyin bobosida — va shunday qilib eng yuqorigacha (document, keyin window). Bu bubble (inglizcha "pufakcha") bosqichi.

O'xshatish: suv tubidan pufakcha chiqdi. U o'z joyida qolmaydi — yuqoriga ko'tariladi va yo'lidagi har qatlamdan o'tadi. Hodisa ham shunday: bosilgan joydan daraxtning tepasigacha ko'tariladi.

2.2 Ko'tarilish — foydali xususiyat

Boshida bu xato bo'lib tuyulishi tabiiy: "men tugmani bosdim, nega karta ham bilib qoldi?". Lekin bu juda foydali:

  • Tugma ichida ikonka bo'lsa ham, tugmaning tinglovchisi ishlaydi («span» muammosi aynan shu bilan hal bo'lgan).
  • Ro'yxatning 100 ta bandiga emas, ro'yxatning o'ziga bitta tinglovchi qo'yib, qaysi band bosilganini bilish mumkin. Bu — Event delegation, keyingi dars.

Har tinglovchida event.target bir xil — bosilgan eng ichki element. event.currentTarget esa har qadamda o'zgaradi: tugmada — tugma, kartada — karta. O'tgan darsdagi farq endi to'liq tushunarli: target — sayohat boshlangan joy, currentTarget — hozir hodisa turgan bekat.

Tekshirib ko'ring: Yuqoridagi misolda document ga ham click tinglovchisi qo'yildi. «Savatga» bosilganda u nechanchi bo'lib ishlaydi?

Javob

To'rtinchi: tugma, karta, menyu, document. Daraxtning ildizi — document, ul dan yuqorida body va html ham bor (ularda tinglovchi yo'q, hodisa ulardan jim o'tadi). document dan keyin hodisa window ga ham yetadi.

3. Uch bosqich: capture → target → bubble

3.1 Hodisa avval pastga tushadi

Bubble — sayohatning faqat ikkinchi yarmi. Brauzer hodisani bosilgan elementga to'g'ridan-to'g'ri yubormaydi. Avval u eng yuqoridan — window dan — pastga tushadi, elementgacha. Keyin yuqoriga ko'tariladi:

flowchart TD
  W["window"] -->|"1. capture ↓"| D["document"]
  D --> B["body"]
  B --> U["ul#menyu"]
  U --> L["li.taom-karta"]
  L --> T["button — 2. target"]
  T -.->|"3. bubble ↑"| L2["li"]
  L2 -.-> U2["ul"]
  U2 -.-> B2["body → document → window"]

Uch bosqich:

  1. Capture (ushlash) — window dan bosilgan elementning otasigacha pastga.
  2. Target (nishon) — elementning o'zida.
  3. Bubble (ko'tarilish) — otasidan window gacha yuqoriga.

Diagrammadagi asosiy fikr: hodisa yo'lidagi har elementdan ikki marta o'tadi — tushayotganda va ko'tarilayotganda. Oddiy addEventListener faqat ko'tarilishni eshitadi. Shuning uchun biz hozirgacha capture'ni sezmadik.

3.2 Capture bosqichida tinglash

Tushish paytida tinglash uchun addEventListener ga uchinchi argument beriladi: true yoki { capture: true } (ikkalasi bir xil; ikkinchisi o'qishga aniqroq). Endi bitta tajribada ikkala bosqichni ko'ramiz — har elementga ikkitadan tinglovchi:

html
<ul id="menyu">
  <li class="taom-karta">
    <button type="button">Savatga</button>
  </li>
</ul>
<script>
  const qatlamlar = [
    [document, "document"],
    [document.body, "body"],
    [document.getElementById("menyu"), "ul"],
    [document.querySelector(".taom-karta"), "li"],
    [document.querySelector("button"), "button"],
  ];

  for (const [el, nom] of qatlamlar) {
    el.addEventListener("click", () => {
      console.log(`capture ↓ ${nom}`);
    }, { capture: true });
    el.addEventListener("click", () => {
      console.log(`bubble ↑ ${nom}`);
    });
  }
</script>

«Savatga» ni bosing. Konsolda o'nta qator chiqadi — biz buni Chrome'da tekshirdik:

text
capture ↓ document
capture ↓ body
capture ↓ ul
capture ↓ li
capture ↓ button
bubble ↑ button
bubble ↑ li
bubble ↑ ul
bubble ↑ body
bubble ↑ document

for (const [el, nom] of ...) — sikl ichida massiv destructuring (Massiv destructuring): har juftdan element va nom olinadi.

Ikki narsaga qarang:

  • Capture tinglovchilari tashqaridan ichkariga, bubble — ichkaridan tashqariga.
  • Tugmaning o'zida ikkala tinglovchi ham ishladi: avval capture, keyin bubble. Hodisa nishonga yetganda zamonaviy brauzerlar avval capture tinglovchilarni, keyin oddiylarini chaqiradi.

Endi kartaning bo'sh joyini (tugmadan tashqarisini) bosing. Sakkiz qator: capture ↓ document dan capture ↓ li gacha, keyin bubble ↑ li dan yuqoriga. Tugma yo'ldan chetda qoldi — sayohat har doim bosilgan elementgacha boradi, undan chuqurroq emas.

3.3 eventPhase — hodisa qaysi bosqichda?

Hodisa obyektida eventPhase xususiyati bor. U hodisa hozir qaysi bosqichda ekanini raqam bilan aytadi: 1 — capture, 2 — target, 3 — bubble. Bitta funksiyani uch joyga qo'yib ko'ramiz:

html
<div id="karta">
  <button type="button">Savatga</button>
</div>
<script>
  const karta = document.getElementById("karta");
  const tugma = karta.querySelector("button");

  function bosqich(event) {
    const nom = event.currentTarget.nodeName;
    console.log(`${nom}: eventPhase ${event.eventPhase}`);
  }

  karta.addEventListener("click", bosqich, { capture: true });
  tugma.addEventListener("click", bosqich);
  karta.addEventListener("click", bosqich);
</script>

«Savatga» ni bosing. Konsolda uch qator chiqadi — uchalasini ham bitta funksiya yozdi:

text
DIV: eventPhase 1
BUTTON: eventPhase 2
DIV: eventPhase 3

Kartaga bir xil funksiya ikki marta qo'shildi: biri capture bilan, biri capture'siz. Brauzer uchun bular ikki xil tinglovchi. Shuning uchun karta ikki marta ishladi — hodisa tushayotganda (1) va ko'tarilayotganda (3). Kundalik kodda eventPhase kam kerak bo'ladi. Lekin tarqalishni o'rganayotganda bitta shunday console.log ko'p narsani ko'rsatadi.

3.4 Capture nimaga kerak?

Kundalik koddagi tinglovchilarning deyarli hammasi oddiy — bubble. Capture kamdan-kam, lekin aniq holatlarda kerak:

  • Ichkaridagilardan oldin ushlash. Masalan, sahifadagi hamma bosishlarni hisoblaydigan analitika document ga capture tinglovchisi qo'yadi. Ichkaridagi biror kod stopPropagation qilsa ham (pastda), capture tinglovchisi baribir oldinroq ishlab ulguradi.
  • Ko'tarilmaydigan hodisalarni tepada eshitish — «Ko'tarilmaydigan hodisalar» bo'limida.

Tarixiy izoh: 1990-yillarda Netscape hodisani tepadan pastga, Internet Explorer esa pastdan tepaga yuborgan. Standart ikkalasini birlashtirdi — shuning uchun ikki bosqich bor.

Tekshirib ko'ring: body da capture tinglovchisi, button da oddiy tinglovchi bor. Tugma bosildi. Qaysi biri birinchi ishlaydi?

Javob

body dagi capture tinglovchisi. Capture bosqichi butunlay bubble'dan oldin keladi: hodisa avval tepadan tushadi (body ni capture'da uchratadi), keyin tugmada ishlaydi.

4. stopPropagation — sayohatni to'xtatish

4.1 Karta ichidagi tugma

«Nega bu kerak?» bo'limidagi muammoga qaytamiz: «Savatga» bosilganda karta ham ochilmasin. Eng to'g'ridan-to'g'ri yo'l — tugmaning handlerida hodisaning yuqoriga ko'tarilishini to'xtatish:

html
<li class="taom-karta">
  Osh — 35 000 so'm
  <button type="button">Savatga</button>
</li>
<script>
  const karta = document.querySelector(".taom-karta");
  const tugma = karta.querySelector("button");

  karta.addEventListener("click", () => {
    console.log("Karta: batafsil oyna ochildi");
  });

  tugma.addEventListener("click", (event) => {
    event.stopPropagation();
    console.log("Tugma: savatga qo'shildi");
  });
</script>

«Savatga» ni bosing — faqat "Tugma: savatga qo'shildi". Kartaning matnini bosing — "Karta: batafsil oyna ochildi". stopPropagation() (tarqalishni to'xtat) hodisaga "bundan nariga borma" deydi: joriy element tinglovchilari ishlab bo'ladi, keyingi qatlamlarga hodisa yetmaydi.

4.2 Nega uni kamdan-kam ishlatish kerak?

Muammo hal bo'lgandek. Lekin stopPropagation — bolta: u hodisani hamma yuqoridagilardan yashiradi, nafaqat kartadan. Yuqorida esa siz bilmagan tinglovchilar bo'lishi mumkin. Masalan, «Bahor» sahifasida "Ulashish" ochiluvchi menyusi bor. U tashqariga bosilganda yopilishi kerak — buni document dagi tinglovchi qiladi:

html
<button id="ulashish" type="button">Ulashish ▾</button>
<p id="panel" hidden>Telegram · Nusxa olish</p>
<li class="taom-karta">
  Osh <button id="savatga" type="button">Savatga</button>
</li>
<script>
  const panel = document.getElementById("panel");
  const ulashish = document.getElementById("ulashish");
  const savatga = document.getElementById("savatga");

  ulashish.addEventListener("click", () => {
    panel.hidden = false;
  });

  document.addEventListener("click", (event) => {
    if (event.target.id !== "ulashish") {
      panel.hidden = true;
      console.log("Tashqariga bosildi — panel yopildi");
    }
  });

  savatga.addEventListener("click", (event) => {
    event.stopPropagation();
    console.log("Savatga qo'shildi");
  });
</script>

Sinab ko'ring:

  1. «Ulashish ▾» ni bosing — panel ochiladi.
  2. Sahifaning bo'sh joyini bosing — panel yopiladi, konsolda "Tashqariga bosildi — panel yopildi".
  3. Panelni yana oching va endi «Savatga» ni bosing. Konsolda faqat "Savatga qo'shildi" — panel ochiq qoldi.

Tugmadagi stopPropagation hodisani document gacha yetkazmadi va panel "tashqariga bosildi" ekanini bilmadi. Real loyihada bu ikki kodni ikki xil odam yozgan bo'ladi — xatoni topish juda qiyin. Xuddi shu sabab bilan analitika "Savatga" bosishlarini hisoblamay qoladi.

4.3 Yaxshiroq yo'l: kim bosilganini tekshirish

Hodisani to'xtatish o'rniga kartaning tinglovchisi o'zi so'rasin: "bosish tugmaga tushdimi?". Bunga closest aynan mos:

html
<li class="taom-karta">
  Osh — 35 000 so'm
  <button type="button">
    <span class="ikonka">+</span> Savatga
  </button>
</li>
<script>
  const karta = document.querySelector(".taom-karta");

  karta.addEventListener("click", (event) => {
    if (event.target.closest("button")) {
      return;
    }
    console.log("Karta: batafsil oyna ochildi");
  });

  karta.querySelector("button").addEventListener("click", () => {
    console.log("Tugma: savatga qo'shildi");
  });
</script>

«Savatga» ni (hatto + ni) bosing — faqat "Tugma: savatga qo'shildi". Kartaning matnini bosing — "Karta: batafsil oyna ochildi". Hodisa to'xtatilmadi: document dagi har qanday tinglovchi uni baribir eshitadi. Karta shunchaki "bu menga emas" deb qaror qildi.

Nega event.target === tugma emas, closest? + ni bossangiz, target — span. closest("button") span dan yuqoriga qarab tugmani topadi. Bu qolip keyingi darsning yuragi.

Qoida: stopPropagation ni faqat boshqa yo'l qolmaganda ishlating va nega qo'yganingizni izohda yozing. Ko'p holda "kim bosilgan?" tekshiruvi yetadi.

Tekshirib ko'ring: Kartaning tinglovchisida if (event.target.closest("button")) return; yozildi. Mehmon kartaning ichidagi havolani (<a>) bosdi. Karta ochiladimi? Uni ham istisno qilish uchun selektorni qanday o'zgartirasiz?

Javob

Ochiladi — havola button emas. Selektorga havolani qo'shing: closest("button, a"). Vergul bilan bir nechta selektor — CSS'dagidek (Selektorlar). Kartadagi barcha interaktiv elementlarni istisno qilish kerak bo'lsa, ro'yxatni kengaytirasiz: "button, a, input, select".

5. stopImmediatePropagation — shu yerning o'zida

stopPropagation keyingi elementlarni to'xtatadi, lekin shu elementdagi qolgan tinglovchilar baribir ishlaydi. stopImmediatePropagation() esa darhol to'xtatadi: shu elementdagi keyingi tinglovchilar ham, yuqoridagilar ham ishlamaydi.

Qachon kerak? addEventListener asoslari darsidagi aria-disabled qorovulini eslang. Faraz qiling, tugmada bir nechta tinglovchi bor (bittasi buyurtmani yuboradi, bittasi analitikaga yozadi). Tugma "o'chirilgan" bo'lsa, ularning hech biri ishlamasligi kerak. Qorovulni birinchi qo'shib, stopImmediatePropagation qilsak, qolganlari to'xtaydi:

html
<button id="yubor" type="button" aria-disabled="true">
  Buyurtma berish
</button>
<script>
  const tugma = document.getElementById("yubor");

  tugma.addEventListener("click", (event) => {
    if (tugma.getAttribute("aria-disabled") === "true") {
      event.stopImmediatePropagation();
      console.log("Qorovul: tugma o'chirilgan");
    }
  });

  tugma.addEventListener("click", () => {
    console.log("Buyurtma yuborildi");
  });

  document.addEventListener("click", () => {
    console.log("Analitika: bosish yozildi");
  });
</script>

Bosing — faqat "Qorovul: tugma o'chirilgan". "Tahrirlab ko'rish" bilan stopImmediatePropagation ni stopPropagation ga almashtiring: endi "Buyurtma yuborildi" ham chiqadi (shu tugmaning ikkinchi tinglovchisi), "Analitika" esa yo'q. aria-disabled ni "false" qilsangiz — uchala qator.

Bu qolip tinglovchilar qo'shilish tartibiga tayanadi — qorovul birinchi bo'lishi shart. Mo'rt joy, shuning uchun u ham kamdan-kam ishlatiladi.

6. preventDefault va stopPropagation — ikki xil narsa

Ikkalasi ham "to'xtatadi", shuning uchun boshlovchilar ko'p chalkashtiradi. Ular mutlaqo mustaqil:

Metod Nimani to'xtatadi Nimani to'xtatmaydi
preventDefault() brauzerning standart harakatini hodisaning tarqalishini
stopPropagation() boshqa elementlarga tarqalishni standart harakatni

Tekshirib ko'ramiz. Checkbox kartaning ichida, checkbox'da faqat stopPropagation:

html
<li class="taom-karta">
  Osh
  <label><input id="achchiq" type="checkbox"> achchiq qilib</label>
</li>
<script>
  const karta = document.querySelector(".taom-karta");
  const belgi = document.getElementById("achchiq");

  karta.addEventListener("click", () => console.log("karta"));

  belgi.addEventListener("click", (event) => {
    event.stopPropagation();
    console.log("belgi:", belgi.checked);
  });
</script>

Checkbox'ning o'zini bosing. Konsolda faqat belgi: true — karta jim, lekin belgi qo'yildi: standart harakat ishladi. Endi "achchiq qilib" yozuvini bosing. Konsolda karta, keyin belgi: false — va belgi olib tashlandi.

Ikkinchi holatni tushuntiraylik — u ham foydali. Siz label ni bosdingiz. label ning standart harakati — bog'langan maydonni "bosish" (o'tgan darsdagi jadval). Birinchi click label da boshlandi va kartaga ko'tarildi: karta. Keyin brauzer checkbox'ga ikkinchi click yubordi: belgi: false. Bitta bosish — ikki hodisa. Bu label li formalarda tez-tez uchraydigan "nega handler ikki marta ishladi?" degan savolning javobi.

preventDefault ham xuddi shunday mustaqil: havolada preventDefault chaqirilsa, sahifa almashmaydi, lekin click hodisasi otalariga baribir ko'tariladi.

Tekshirib ko'ring: Kartaning ichida havola bor. Havolaning tinglovchisi faqat event.stopPropagation() ni chaqiradi. Havola bosilganda nima bo'ladi: karta tinglovchisi ishlaydimi, sahifa almashadimi?

Javob

Karta tinglovchisi ishlamaydi — tarqalish to'xtadi. Lekin sahifa almashadi: standart harakatni hech kim bekor qilmadi. Ikkalasini ham to'xtatish kerak bo'lsa, ikkala metod alohida chaqiriladi. Odatda esa faqat bittasi kerak — qaysi biri ekanini aniq biling.

7. Ko'tarilmaydigan hodisalar

Hamma hodisa ham bubble qilmaydi. Hodisa obyektidagi bubbles xususiyati buni aytadi. Eng ko'p uchraydiganlari va ularning ko'tariladigan "egizaklari":

Ko'tarilmaydi Ko'tariladigan juft Qachon
focus, blur focusin, focusout fokus keldi / ketdi
mouseenter, mouseleave mouseover, mouseout sichqoncha kirdi / chiqdi
load, error (rasmda) — rasm yuklandi / yuklanmadi
scroll (elementda) — element ichi surildi

Formaning istalgan maydoniga fokus kelganini bilmoqchimiz. Tinglovchi formada:

html
<form id="bron" action="https://example.com/bron" method="post">
  <label for="ism">Ism</label>
  <input id="ism" name="ism">
</form>
<script>
  const forma = document.getElementById("bron");

  forma.addEventListener("focus", () => {
    console.log("focus (oddiy)");
  });
  forma.addEventListener("focusin", (event) => {
    console.log("focusin, bubbles:", event.bubbles);
  });
  forma.addEventListener("focus", (event) => {
    console.log("focus (capture), bubbles:", event.bubbles);
  }, { capture: true });
</script>

Maydonni bosing. Konsolda ikki qator: focus (capture), bubbles: false va focusin, bubbles: true. "focus (oddiy)" — yo'q.

Uch xulosa:

  • focus maydondan formaga ko'tarilmadi — oddiy tinglovchi uni eshitmadi.
  • focusin — xuddi o'sha voqea, lekin ko'tariladi.
  • Capture tinglovchisi focus ni ham eshitdi! Ko'tarilmaydigan hodisa ham pastga tushadi — faqat qaytib chiqmaydi. «Capture nimaga kerak?» bo'limidagi ikkinchi sabab shu.

Tekshirib ko'ring: Taom kartalari ro'yxati bor. Sichqoncha istalgan kartaga kirganda ro'yxatning o'zida bitta tinglovchi buni bilishi kerak. mouseenter yoki mouseover — qaysi birini tanlaysiz?

Javob

mouseover: u ko'tariladi, shuning uchun ro'yxatdagi bitta tinglovchi uni har kartadan eshitadi. mouseenter ko'tarilmaydi — ro'yxatdagi oddiy tinglovchi faqat sichqoncha ro'yxatning o'ziga kirganini biladi. Bir tafsilot: mouseover karta ichidagi har bola elementga o'tganda ham qayta keladi — buni Pointer hodisalari darsida hal qilamiz.

Fokus hodisalarining o'zini Fokus, scroll va resize darsida, mouseenter/mouseover farqini Pointer hodisalari darsida batafsil ko'ramiz.

8. Hodisaning yo'lini ko'rish: composedPath()

event.composedPath() hodisa o'tadigan yo'lni massiv qilib beradi — bosilgan elementdan window gacha:

html
<ul id="menyu">
  <li><button type="button">Osh</button></li>
</ul>
<script>
  const tugma = document.querySelector("button");

  tugma.addEventListener("click", (event) => {
    for (const bekat of event.composedPath()) {
      console.log(bekat.nodeName ?? "window");
    }
  });
</script>

Bosing. Konsolda yetti qator: BUTTON, LI, UL, BODY, HTML, #document, window. window da nodeName yo'q (u DOM tuguni emas), shuning uchun ?? (Nullish coalescing) o'rniga "window" yozdi.

Bubble shu ro'yxat bo'yicha boshidan oxirigacha yuradi, capture esa oxiridan boshigacha. Nomdagi "composed" so'zi Shadow DOM bilan bog'liq — komponentning ichki yashirin daraxti. Uni Web Components mavzusida ko'rasiz, hozir bilish shart emas.

9. DevTools'da tarqalishni ko'rish

Elements → Event Listeners tabida (o'tgan darslardan tanish) ikki foydali belgi bor:

  • Ancestors — yoqilsa, tanlangan elementning ota-bobolaridagi tinglovchilar ham ko'rinadi. "Bosganimda kutilmagan narsa bo'ldi" degan savolga javob shu ro'yxatda: tugmaning tepasidagi qaysi element ham click ni tinglayotganini darhol ko'rasiz.
  • Har tinglovchi yonida useCapture: true/false — u qaysi bosqichda ishlashini bildiradi.

10. Ko'p uchraydigan xatolar

10.1 "Har ehtimolga qarshi" stopPropagation

Har handlerga stopPropagation qo'yish — keyin delegation, analitika va "tashqariga bosganda yopish" ishlamay qoladi. Belgisi: "bu tugmani bossam, menyu yopilmaydi" kabi g'alati xatolar. Tuzatish: stopPropagation ni olib tashlab, yuqoridagi tinglovchida closest bilan "kim bosildi?" ni tekshiring.

10.2 preventDefault bilan tarqalishni to'xtatmoqchi bo'lish

preventDefault faqat standart harakatni bekor qiladi. Otasidagi tinglovchi baribir ishlaydi. Va aksincha: stopPropagation havola ochilishini to'xtatmaydi.

10.3 return false tarqalishni to'xtatadi deb o'ylash

Eski jQuery kutubxonasida return false ikkalasini birdan qilardi. Oddiy JavaScript'da addEventListener qaytgan qiymatga umuman qaramaydi, onclick xususiyatida esa faqat standart harakatni bekor qiladi (Event obyekti). Tarqalish ikkala holda ham davom etadi.

10.4 focus ni otada tinglash

forma.addEventListener("focus", ...) hech qachon ishlamaydi — focus ko'tarilmaydi. Tuzatish: focusin yoki { capture: true }.

10.5 Capture tinglovchisini oddiy usulda olib tashlash

addEventListener("click", f, { capture: true }) bilan qo'shilgan tinglovchini removeEventListener("click", f) olib tashlamaydi: brauzer uchun bu ikki xil yozuv. Olib tashlashda ham { capture: true } kerak. Chrome'da sinadik — capture'siz chaqiruvdan keyin tinglovchi ishlashda davom etdi. Batafsil — Listener opsiyalari darsida.

11. Mashqlar

1-mashq (oson): Tartibni ayting

Kodni o'qing va «Osh» tugmasi bosilganda konsolda nima chiqishini avval qog'ozga yozing. Keyin bosib tekshiring.

html
<div id="tashqi">
  <p id="ichki"><button id="osh" type="button">Osh</button></p>
</div>
<script>
  const tashqi = document.getElementById("tashqi");
  const ichki = document.getElementById("ichki");
  const osh = document.getElementById("osh");

  tashqi.addEventListener("click", () => console.log("A"));
  osh.addEventListener("click", () => console.log("B"));
  tashqi.addEventListener("click", () => console.log("C"), true);
  ichki.addEventListener("click", () => console.log("D"));
</script>
Yechim

C, B, D, A. Avval capture: hodisa pastga tushayotib tashqi ning capture tinglovchisini uchratadi (C). Nishonda — B. Keyin ko'tarilish: ichki (D), tashqi ning oddiy tinglovchisi (A). Kodda A birinchi yozilgani ahamiyatsiz — bosqich tartibni hal qiladi. Biz buni Chrome'da tekshirdik.

2-mashq (o'rta): Karta va havola

Taom kartasi bosilganda konsolga "Batafsil: Manti" chiqsin. Lekin kartaning ichidagi «Savatga» tugmasi va «Retsept» havolasi bosilganda karta handleri ishlamasin. stopPropagation ishlatmang. Havolaning standart harakatini to'xtating (bu namoyish), uning tinglovchisida "Retsept ochildi" chiqsin.

html
<article class="taom-karta">
  <h2>Manti</h2>
  <button type="button">Savatga</button>
  <a href="https://example.com/retsept">Retsept</a>
</article>
<script>
  // shu yerga yozing
</script>
Yechim
html
<article class="taom-karta">
  <h2>Manti</h2>
  <button type="button">Savatga</button>
  <a href="https://example.com/retsept">Retsept</a>
</article>
<script>
  const karta = document.querySelector(".taom-karta");
  const nomi = karta.querySelector("h2").textContent;

  karta.addEventListener("click", (event) => {
    if (event.target.closest("button, a")) {
      return;
    }
    console.log(`Batafsil: ${nomi}`);
  });

  karta.querySelector("button").addEventListener("click", () => {
    console.log("Savatga qo'shildi");
  });

  karta.querySelector("a").addEventListener("click", (event) => {
    event.preventDefault();
    console.log("Retsept ochildi");
  });
</script>

Sarlavhani bosing — "Batafsil: Manti". Tugmani — "Savatga qo'shildi", havolani — "Retsept ochildi". Ikkala holda ham hodisa kartaga yetib bordi, lekin karta closest bilan "bu menga emas" deb qaytdi. Agar document ga tinglovchi qo'shsangiz, u uchala bosishni ham eshitadi — stopPropagation bilan bunday bo'lmasdi.

3-mashq (qiyin): Tashqariga bosganda yopilsin

«Ish vaqti ▾» tugmasi panelni ochadi va yopadi. Panel tashqarisiga bosilganda ham yopilsin. Panelning ichiga bosilganda yopilmasin. Holatni hidden xususiyati bilan boshqaring. stopPropagation ishlatmang.

Ishora: document ga bitta tinglovchi qo'ying. Bosilgan joy panel ichidami? Buni panel.contains(event.target) aytadi (DOM bo'ylab harakatlanish darsida va'da qilingan aynan shu holat). Tugmaning o'zi ham "tashqari" emas — u o'z tinglovchisida ishni bajaradi.

html
<button id="vaqt" type="button" aria-expanded="false">
  Ish vaqti ▾
</button>
<div id="panel" hidden>
  <p>Har kuni 7:00–23:00</p>
  <p>Bron: 10:00–22:00</p>
</div>
<p>Sahifaning boshqa qismi</p>
<script>
  // shu yerga yozing
</script>
Yechim
html
<button id="vaqt" type="button" aria-expanded="false">
  Ish vaqti ▾
</button>
<div id="panel" hidden>
  <p>Har kuni 7:00–23:00</p>
  <p>Bron: 10:00–22:00</p>
</div>
<p>Sahifaning boshqa qismi</p>
<script>
  const tugma = document.getElementById("vaqt");
  const panel = document.getElementById("panel");

  function panelniKorsat(ochiq) {
    panel.hidden = !ochiq;
    tugma.setAttribute("aria-expanded", String(ochiq));
    console.log(ochiq ? "ochildi" : "yopildi");
  }

  tugma.addEventListener("click", () => {
    panelniKorsat(panel.hidden);
  });

  document.addEventListener("click", (event) => {
    const ichida = panel.contains(event.target);
    const tugmada = tugma.contains(event.target);
    if (!panel.hidden && !ichida && !tugmada) {
      panelniKorsat(false);
    }
  });
</script>

Tugmani bosing — "ochildi". Paneldagi matnni bosing — hech narsa. "Sahifaning boshqa qismi" ni bosing — "yopildi". Tugmani ikki marta bosing — ochildi, yopildi.

Nega tugmani alohida tekshirdik? Tugma bosilganda hodisa avval tugmada ishlaydi (panelni ochadi), keyin document ga ko'tariladi. Tekshiruv bo'lmasa, document tinglovchisi "tashqariga bosildi" deb panelni darhol yopib qo'yardi — tugma ishlamaydigandek ko'rinardi. Tarqalishni tushunish aynan shunday xatolardan saqlaydi.

aria-expanded — Malika uchun: NVDA "Ish vaqti, tugma, yoyilgan" deydi (ARIA asoslari). Shu ishni HTML'ning o'zi — popover bilan — skriptsiz qiladi (Popover API); bu mashq tarqalishni o'rganish uchun.

4-mashq: Portfolio qadami — menyu hodisasining yo'li

Bu safar portfolio'ga yangi kod qo'shmaymiz — keyingi darsga tayyorlanamiz. O'tgan darslarda menyudagi har havolaga alohida tinglovchi qo'ydik. Keyingi darsda ularni bitta tinglovchiga aylantiramiz. Buning uchun havola bosilganda hodisa qayerlardan o'tishini ko'rish kerak.

  1. Portfolio'ni Live Server bilan oching, DevTools → Console.
  2. Konsolga quyidagini yozing (bu vaqtinchalik tajriba, faylga emas):
js
document.getElementById("asosiy-menyu").addEventListener(
  "click",
  (event) => console.log(event.target, event.currentTarget),
);
  1. Kompyuter o'lchamida menyudagi istalgan havolani Ctrl bilan bosing (yangi tabda ochiladi, joriy sahifa qoladi). Konsolda nima chiqdi?
  2. Elements → Event Listeners'da biror havolani tanlang va Ancestors ni yoqing. Havolaning o'z tinglovchisi va #asosiy-menyu dagi yangi tinglovchi ko'rinadimi?
Yechim

3-qadamda konsolda ikki element chiqadi: target — siz bosgan <a href="…">…</a>, currentTarget — <nav id="asosiy-menyu" …>…</nav>. Demak havola bosilishi menyuning o'ziga ko'tariladi va bitta tinglovchi istalgan havolani taniy oladi. Keyingi darsda aynan shuni asosiy.js ga yozamiz.

4-qadamda ro'yxatda ikkalasi ko'rinadi: havolaning click tinglovchisi (asosiy.js dan) va nav dagi tinglovchi (konsoldan qo'shilgan, manbasi "VM…" kabi nom bilan). Sahifani yangilasangiz, konsol tinglovchisi yo'qoladi — u faylda emas edi. Commit kerak emas.

12. Real ishda

  • Event delegation — deyarli har ro'yxat, jadval va menyu shu bilan ishlaydi. React kabi freymvorklar (kursda alohida o'rganamiz) ham ichida hodisalarni ildizdagi bitta tinglovchi orqali tarqatadi — bubble'ga tayanib.
  • "Tashqariga bosganda yopish" — dropdown, kontekst menyusi, qidiruv takliflari: hammasi document dagi tinglovchi + contains/closest. Shuning uchun begona stopPropagation eng ko'p ana shu komponentlarni buzadi.
  • Analitika va xatolarni yig'ish vositalari document yoki window da (ko'pincha capture bilan) tinglaydi.
  • Intervyuda: "Event bubbling nima?", "capture va bubble farqi?", "stopPropagation va preventDefault farqi?", "qaysi hodisalar ko'tarilmaydi?" — to'rttasi ham shu darsda.

Xulosa

  • Hodisa uch bosqichdan o'tadi: capture (window → element), target, bubble (element → window).
  • Oddiy addEventListener bubble'da tinglaydi; { capture: true } — capture'da.
  • stopPropagation keyingi elementlarni, stopImmediatePropagation shu elementdagi qolgan tinglovchilarni ham to'xtatadi. Ikkalasi ham xavfli — o'rniga closest bilan "kim bosildi?" ni tekshiring.
  • preventDefault va stopPropagation mustaqil: biri standart harakatni, ikkinchisi tarqalishni to'xtatadi.
  • focus, blur, mouseenter, mouseleave ko'tarilmaydi — focusin, focusout, mouseover, mouseout yoki capture ishlating.

Keyingi dars: Event delegation — bubble'dan foydalanib, yuzlab element uchun bitta tinglovchi yozamiz va keyin qo'shilgan elementlar ham avtomatik ishlaydi.

Manbalar

  • MDN: "Event bubbling", "Event: stopPropagation() method", "Event: stopImmediatePropagation() method" — developer.mozilla.org
  • MDN: "Event: eventPhase property", "Event: composedPath() method", "Event: bubbles property" — developer.mozilla.org
  • DOM Living Standard: "Dispatching events" — dom.spec.whatwg.org
  • Chrome DevTools: "View and debug event listeners" (Ancestors) — developer.chrome.com
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript hodisa tarqalishi: capture, bubble va stopPropagation — IlmHamroh