IlmHamroh
JavaScript Full-stack/9-qism. JavaScript brauzerda va debugging20/29-dars21 daqiqa
Mundarija (36)

Custom events JavaScript'da: CustomEvent, dispatchEvent va EventTarget

Qisqacha: Brauzer hodisalarini tinglashdan tashqari, o'z hodisangizni ham yaratib, elementga "chalib" yubora olasiz: new CustomEvent(...) va dispatchEvent(...). Ma'lumot detail ichida keladi; ota elementlar eshitishi uchun bubbles: true kerak. Barcha tinglovchilar dispatchEvent ichida, shu zahoti ishlaydi, new EventTarget() esa sahifada ko'rinmaydigan "hodisa markazi" beradi.

Bu darsda

  • new Event va new CustomEvent bilan o'z hodisangizni yaratib, dispatchEvent bilan yubora olasiz.
  • detail orqali ma'lumot uzatasiz, bubbles va cancelable nima qilishini bilasiz.
  • dispatchEvent sinxron ekanini va qaytaradigan qiymatini tushunasiz.
  • EventTarget bilan "event bus" yasaysiz va qachon kerak emasligini ham bilasiz.
  • Portfolio'dagi tema almashinuvini hodisa orqali e'lon qilasiz.

Oldin bilishingiz kerak: Event delegation, Hodisa tarqalishi: capture va bubble, Event obyekti va standart harakatlar, Fokus, scroll va resize hodisalari.

1. Nega bu kerak?

«Bahor» saytiga savat qo'shildi. Mehmon "Savatga" tugmasini bossa, uch narsa o'zgarishi kerak: sarlavhadagi savat belgisidagi son, sahifa pastidagi jami summa va ekran chetida "Osh qo'shildi" degan qisqa xabar.

Aziz birinchi bo'lib shunday yozdi:

js
function savatgaQosh(taom) {
  sonniYangila();
  jaminiHisobla();
  xabarniKorsat(taom);
}

Ishlaydi. Lekin bir hafta o'tib Nodira "savatga qo'shilganda analitikaga ham yozilsin" dedi. Yana bir hafta o'tib — "tugma bir lahza yashil bo'lsin". Har safar savatgaQosh ichiga qo'l tiqish kerak. Bu funksiya savatdan boshqa hamma narsani biladigan bo'lib qoldi.

Choyxonaning oshxonasini eslang. Oshpaz Rustam aka osh tayyor bo'lganda har ofitsiantning oldiga borib aytmaydi. U qo'ng'iroqni chaladi. Kimga kerak bo'lsa — Otabek, kassir, Jasur aka — o'zi eshitib, o'z ishini qiladi. Ertaga yangi ofitsiant kelsa, Rustam aka hech narsani o'zgartirmaydi: yangi odam shunchaki qo'ng'iroqni tinglaydi.

O'z hodisangiz — aynan shu qo'ng'iroq. Savat "savatga qo'shildi" deb e'lon qiladi, kerakli qismlar o'zlari tinglaydi.

2. Birinchi o'z hodisamiz: new Event va dispatchEvent

2.1 Yaratish va yuborish

Ikki qadam kerak:

  1. new Event("nom") — hodisa obyektini yaratish.
  2. element.dispatchEvent(hodisa) — uni shu elementga "yuborish" (chalish).

Tinglash esa odatdagidek — addEventListener("nom", ...). Brauzer hodisasidan hech qanday farqi yo'q:

html
<p id="oshxona">Oshxona</p>
<script>
  const oshxona = document.getElementById("oshxona");

  oshxona.addEventListener("osh-tayyor", (event) => {
    console.log(`2. Eshitildi: ${event.type}`);
  });

  console.log("1. Qo'ng'iroq chalinmoqda");
  oshxona.dispatchEvent(new Event("osh-tayyor"));
  console.log("3. dispatchEvent tugadi");
</script>

Konsolda:

text
1. Qo'ng'iroq chalinmoqda
2. Eshitildi: osh-tayyor
3. dispatchEvent tugadi

2.2 dispatchEvent sinxron

Tartibga qarang: 2 raqamli qator 3 dan oldin chiqdi. Demak, dispatchEvent tinglovchini darhol chaqirdi va u ishlab bo'lgandan keyingina keyingi qatorga o'tdi.

Bunday ishlash sinxron (synchronous) deyiladi — "navbat bilan, shu zahoti": keyingi qator oldingi ish tugashini kutadi. Bu so'zni Callback darsida "darhol chaqiriladigan callback" sifatida ko'rgansiz.

Bu brauzer hodisalaridan muhim farq. Mehmon tugmani bosganda brauzer hodisani "keyinroq", kodingiz bo'shaganda yuboradi. dispatchEvent esa oddiy funksiya chaqiruviga o'xshaydi: barcha tinglovchilar navbat bilan, shu zahoti bajariladi.

Bundan ikki xulosa:

  • Tinglovchi hodisadan oldin qo'yilgan bo'lishi kerak. Keyin qo'yilsa — u o'tib ketgan qo'ng'iroqni eshitmaydi.
  • Tinglovchi ichida xato bo'lsa, u konsolda qizil chiqadi. Lekin dispatchEvent ni chaqirgan kod to'xtamaydi va keyingi tinglovchilar ham ishlaydi.

Oxirgi gapni sinab ko'ramiz. Birinchi tinglovchi mavjud bo'lmagan stolni qidiradi va tanish xatoga uchraydi:

html
<p id="oshxona">Oshxona</p>
<script>
  const oshxona = document.getElementById("oshxona");
  oshxona.addEventListener("osh-tayyor", () => {
    const stol = document.querySelector("#stol-5");
    console.log(stol.textContent);
  });
  oshxona.addEventListener("osh-tayyor", () => {
    console.log("2. Kassa eshitdi");
  });

  console.log("1. Yuborilmoqda");
  const natija = oshxona.dispatchEvent(new Event("osh-tayyor"));
  console.log("3. Davom etdi:", natija);
</script>

Konsolda:

text
1. Yuborilmoqda
Uncaught TypeError: Cannot read properties of null (reading 'textContent')
2. Kassa eshitdi
3. Davom etdi: true

Tarjimasi: "null ning xususiyatini o'qib bo'lmaydi ('textContent' ni o'qiyotganda)" — #stol-5 topilmadi. Xato birinchi tinglovchini to'xtatdi, xolos. Ikkinchi tinglovchi ham, dispatchEvent dan keyingi qator ham ishladi. Oddiy funksiya chaqiruvida bunday emas: chaqirilgan funksiya ichida xato bo'lsa, uni chaqirgan kod ham to'xtaydi.

Bu xulq yaxshi ham, xavfli ham. Yaxshi — bitta buzuq tinglovchi butun saytni yiqitmaydi. Xavfli — xatoni sezmay qolish oson: sahifa "ishlayotgandek" ko'rinadi. Shuning uchun o'z hodisalaringiz bilan ishlaganda konsoldagi qizil qatorlarga e'tibor bering.

2.3 isTrusted: hodisani kim yaratdi?

Har hodisada event.isTrusted bor (Event obyekti darsida ko'rgansiz). Mehmon harakatidan kelgan hodisada — true, kod yaratganida — false. Shu sababli dispatchEvent bilan "soxta bosish" yaratsangiz ham, brauzer uni haqiqiy deb hisoblamaydi. Masalan, faqat haqiqiy bosishdan keyin ruxsat beriladigan narsalar (yangi oyna ochish, to'liq ekran) soxta hodisada ishlamaydi.

new Event dan tashqari brauzer hodisalarining o'z "konstruktorlari" ham bor. Konstruktor — new bilan chaqirilib, yangi obyekt yasaydigan funksiya: new MouseEvent("click"), new KeyboardEvent("keydown", { key: "a" }). Ular bilan brauzer hodisasini taqlid qilish mumkin — lekin faqat tinglovchilar uchun. Soxta klaviatura hodisasi maydonga harf yozmaydi:

html
<label for="ism">Ism</label>
<input id="ism">
<script>
  const ism = document.getElementById("ism");
  ism.addEventListener("keydown", (event) => {
    console.log("keydown:", event.key, event.isTrusted);
  });

  ism.dispatchEvent(new KeyboardEvent("keydown", { key: "a" }));
  console.log("Qiymat:", JSON.stringify(ism.value));
</script>

Konsolda:

text
keydown: a false
Qiymat: ""

Tinglovchi hodisani oldi (isTrusted — false), maydon esa bo'sh qoldi. Brauzer harf yozishni faqat haqiqiy tugma bosilganda qiladi. Bu xavfsizlik uchun muhim: begona skript sizning nomingizdan parol maydoniga yoza olmaydi. Qoida: o'z hodisangiz uchun Event va CustomEvent yetarli. Brauzer hodisalarini taqlid qilish asosan avtomatik testlarda kerak bo'ladi — ularni kursda alohida o'rganamiz.

Tekshirib ko'ring: Quyidagi kodda konsolga nima chiqadi?

oshxona.dispatchEvent(new Event("choy-tayyor")); — keyin oshxona.addEventListener("choy-tayyor", () => console.log("Choy!"));

Javob

Hech narsa. dispatchEvent sinxron: hodisa yuborilgan paytda hali tinglovchi yo'q edi. Tinglovchi keyin qo'yildi, lekin qo'ng'iroq allaqachon chalinib bo'lgan. Brauzer o'tgan hodisalarni eslab qolmaydi.

3. CustomEvent va detail: hodisa bilan ma'lumot

3.1 Ma'lumot uzatish

"Savatga qo'shildi" degan xabar yetarli emas — nima qo'shildi? Ma'lumot uzatish uchun CustomEvent bor. U Event ning o'zi, faqat qo'shimcha detail xususiyati bilan:

html
<button type="button" id="osh">Osh — 35 000</button>
<script>
  const osh = document.getElementById("osh");

  osh.addEventListener("savat:qoshildi", (event) => {
    const { nom, narx } = event.detail;
    console.log(`Savatga: ${nom}, ${narx} so'm`);
    console.log(event.detail);
    console.log(event, event.isTrusted);
  });

  const hodisa = new CustomEvent("savat:qoshildi", {
    detail: { nom: "Osh", narx: 35000 },
  });
  osh.dispatchEvent(hodisa);
</script>

Konsolda:

text
Savatga: Osh, 35000 so'm
{nom: 'Osh', narx: 35000}
CustomEvent {type: 'savat:qoshildi'} false

detail ga istalgan qiymat sig'adi: son, satr, massiv, obyekt. Odatda — obyekt: keyinchalik yangi maydon qo'shish oson bo'ladi. Tinglovchida uni destructuring bilan ochish qulay.

3.2 Ikki tuzoq

detail faqat detail kaliti orqali uzatiladi. Ma'lumotni to'g'ridan-to'g'ri ikkinchi argumentga yozsangiz, u yo'qoladi:

html
<p id="savat">Savat</p>
<script>
  const savat = document.getElementById("savat");
  savat.addEventListener("savat:qoshildi", (event) => {
    console.log(event.detail, event.nom);
  });
  savat.dispatchEvent(
    new CustomEvent("savat:qoshildi", { nom: "Osh" }),
  );
</script>

Konsolda:

text
null undefined

Xato xabari yo'q — ma'lumot shunchaki jimgina tashlab yuborildi. detail berilmasa, uning qiymati null. Tuzatish: { detail: { nom: "Osh" } }.

Ikkinchi tuzoq — havola semantikasi. detail dagi obyekt nusxalanmaydi: barcha tinglovchilar bitta obyektni oladi. Bitta tinglovchi event.detail.narx = 0 qilsa, keyingisi ham 0 ni ko'radi. Qoida: tinglovchida detail ni faqat o'qing, o'zgartirmang.

4. bubbles va cancelable

4.1 O'z hodisangiz standart holatda bubble qilmaydi

Brauzerning click hodisasi bubble qiladi. new Event va new CustomEvent bilan yaratilgani esa yo'q — buni alohida so'rash kerak. Ko'p boshlovchilar shu yerda qoqiladi:

html
<ul id="menyu">
  <li id="osh">Osh</li>
</ul>
<script>
  const menyu = document.getElementById("menyu");
  const osh = document.getElementById("osh");
  menyu.addEventListener("tanlandi", (event) => {
    console.log(`ul eshitdi: ${event.target.id}`);
  });

  osh.dispatchEvent(new CustomEvent("tanlandi"));
  console.log("— birinchisi (bubbles yo'q) yuborildi");

  osh.dispatchEvent(new CustomEvent("tanlandi", { bubbles: true }));
  console.log("— ikkinchisi (bubbles: true) yuborildi");
</script>

Konsolda:

text
— birinchisi (bubbles yo'q) yuborildi
ul eshitdi: osh
— ikkinchisi (bubbles: true) yuborildi

Birinchi hodisa li da qoldi — ul uni eshitmadi. Ikkinchisi bubbles: true bilan yuborildi va bubble qilib ul ga yetdi.

Qachon bubbles: true kerak? Hodisa yuqoridagi elementlarda tinglanishi kerak bo'lsa — deyarli doim. Ayniqsa delegation bilan: kartalar 200 ta bo'lsa ham, document dagi bitta tinglovchi hammasini eshitadi.

4.2 cancelable va preventDefault: "ruxsat so'rash"

dispatchEvent qiymat qaytaradi: true yoki false. Agar hodisa cancelable: true bilan yaratilgan bo'lsa va tinglovchilardan biri event.preventDefault() chaqirsa — false qaytadi.

Bu "ruxsat so'rash" imkonini beradi. Savat "Manti'ni o'chiraman" deb e'lon qiladi. Oshxona tinglovchisi "manti allaqachon qozonda — o'chirib bo'lmaydi" deb preventDefault qiladi. Savat qaytgan false ni ko'rib, o'chirmaydi:

html
<ul id="savat">
  <li data-nom="Osh">Osh
    <button type="button">O'chirish</button></li>
  <li data-nom="Manti" data-holat="pishmoqda">Manti (pishmoqda)
    <button type="button">O'chirish</button></li>
</ul>
<script>
  const savat = document.getElementById("savat");

  // Savat: o'chirishdan oldin "ruxsat so'raydi"
  savat.addEventListener("click", (event) => {
    const qator = event.target.closest("li");
    if (!event.target.matches("button") || qator === null) {
      return;
    }
    const hodisa = new CustomEvent("savat:ochirish", {
      detail: { nom: qator.dataset.nom },
      bubbles: true,
      cancelable: true,
    });
    const ruxsat = qator.dispatchEvent(hodisa);
    if (ruxsat) {
      qator.remove();
      console.log(`O'chirildi: ${qator.dataset.nom}`);
    }
  });

  // Oshxona: pishayotgan taomni o'chirishga yo'l qo'ymaydi
  document.addEventListener("savat:ochirish", (event) => {
    if (event.target.dataset.holat === "pishmoqda") {
      event.preventDefault();
      console.log(`Mumkin emas: ${event.detail.nom} pishmoqda`);
    }
  });
</script>

Manti yonidagi "O'chirish" ni bosing: konsolda Mumkin emas: Manti pishmoqda, qator joyida qoladi. Osh yonidagisini bosing: O'chirildi: Osh, qator yo'qoladi (tekshirdik).

Savat oshxona haqida hech narsa bilmaydi. U faqat "kimdir qarshi emasmi?" deb so'raydi. Ertaga "chegirmali taomni o'chirib bo'lmaydi" degan qoida kerak bo'lsa — yangi tinglovchi qo'shiladi, savat kodi o'zgarmaydi.

Diqqat: cancelable siz preventDefault hech narsa qilmaydi va dispatchEvent baribir true qaytaradi. "Ruxsat so'rash" uchun ikkalasi ham kerak.

4.3 composed — kelajak uchun bir gap

CustomEvent ning yana bir opsiyasi bor: composed: true. U hodisani Shadow DOM chegarasidan o'tkazadi. Shadow DOM — komponentning ichki, tashqaridan yashirin DOM'i; u Web Components darsida keladi. Hozir bilish shart emas: oddiy sahifada composed hech narsani o'zgartirmaydi.

4.4 Brauzer hodisasi va o'z hodisangiz: farqlar jadvali

Belgi Brauzer hodisasi (click) O'z hodisangiz
Kim yaratadi Brauzer, mehmon harakatidan Sizning kodingiz
isTrusted true false
Qachon tinglovchiga keladi Keyinroq, kod bo'shaganda Darhol, dispatchEvent ichida
bubbles Hodisaga qarab (click — ha, focus — yo'q) Standart — yo'q
Ma'lumot Tayyor xususiyatlar (key, clientX) detail

Qolgan hamma narsa bir xil: addEventListener, removeEventListener, once, signal, capture va bubble, stopPropagation, target va currentTarget. O'z hodisangiz — DOM hodisalar tizimining to'la huquqli a'zosi.

O'zingiz to'ldiring (opsiya nomini yozing):

  • Taom nomi va narxini hodisa bilan uzatish uchun kalit —
  • Hodisa li dan ul va document ga ko'tarilishi uchun —
  • Tinglovchidagi preventDefault ishlashi uchun —

Tekshirib ko'ring: Aziz new CustomEvent("savat:qoshildi", { detail }) ni tugmada yubordi va document da tingladi. Hech narsa chiqmadi. Nima yetishmaydi?

Javob

bubbles: true. O'z hodisasi standart holatda bubble qilmaydi va tugmadan document gacha ko'tarilmaydi. Yoki hodisani to'g'ridan-to'g'ri document ga yuborish kerak: document.dispatchEvent(...).

5. Nomlash qoidalari

Brauzer hodisa nomiga hech qanday cheklov qo'ymaydi — istalgan satr ishlaydi. Shuning uchun tartibni o'zimiz saqlaymiz:

  • Kichik harf. Brauzer hodisalari shunday: click, keydown. SavatQoshildi emas.
  • Prefiks bilan: savat:qoshildi, savat:ochirish, menyu:yopildi. Ikki nuqtadan oldingi qism — kim e'lon qilyapti. Bir qarashda tushunarli va brauzer hodisalari bilan hech qachon to'qnashmaydi.
  • Brauzer nomlarini olmang. O'z click yoki change hodisangizni yaratsangiz, haqiqiy va soxta hodisalar aralashib ketadi.
  • Nomni bir joyda saqlang. Nomdagi xato jim o'tadi: savat:qoshildi ni yuborib, savat:qoshlidi ni tinglasangiz — xato yo'q, natija ham yo'q. Katta loyihada nomlar bitta o'zgarmasga yoziladi: const SAVAT_QOSHILDI = "savat:qoshildi";.

Yana bir farq: brauzer hodisalarining onclick kabi xususiyatlari bor (addEventListener asoslari). O'z hodisangizda bunday xususiyat yo'q — faqat addEventListener.

6. EventTarget: elementsiz hodisa markazi

6.1 Har kim hodisa "chala" oladi

DOM daraxti darsidagi diagrammani eslang: tugunlar oilasining eng tepasida EventTarget turardi. addEventListener, removeEventListener va dispatchEvent aynan shu oiladan keladi. Shuning uchun ular document, har bir element va window da bor.

EventTarget ni o'zingiz ham yaratishingiz mumkin — new EventTarget(). Bu DOM elementi emas, sahifada ko'rinmaydi. U faqat hodisa yuborish va tinglash uchun "markaz":

html
<p>Buyurtmalar markazi (konsolga qarang)</p>
<script>
  const markaz = new EventTarget();

  markaz.addEventListener("buyurtma", (event) => {
    console.log(`Oshxona: ${event.detail.taom} tayyorlansin`);
  });
  markaz.addEventListener("buyurtma", (event) => {
    console.log(`Kassa: +${event.detail.narx} so'm`);
  });

  markaz.dispatchEvent(new CustomEvent("buyurtma", {
    detail: { taom: "Lag'mon", narx: 28000 },
  }));
  console.log(markaz);
</script>

Konsolda:

text
Oshxona: Lag'mon tayyorlansin
Kassa: +28000 so'm
EventTarget {}

Bitta e'lon — ikki tinglovchi. Ular bir-birini bilmaydi va qo'shilish tartibida chaqiriladi.

6.2 Node'da ham ishlaydi

EventTarget, Event va CustomEvent — brauzer asboblari, lekin Brauzer muhiti darsida aytganimizdek, foydali asboblarni Node ham o'ziga qo'shib olgan. Shu kodni terminalda ham ishga tushirsa bo'ladi — "Ishga tushir" tugmasini bosing:

js
const markaz = new EventTarget();

markaz.addEventListener("buyurtma", (event) => {
  console.log(`Oshxona: ${event.detail.taom}`);
});

markaz.dispatchEvent(new CustomEvent("buyurtma", {
  detail: { taom: "Manti" },
}));
console.log("Yuborildi");

Konsolda:

text
Oshxona: Manti
Yuborildi

Node'da bu sinxronlik yana bir bor ko'rinadi: Oshxona qatori Yuborildi dan oldin.

6.3 Event bus: qachon kerak, qachon yo'q

Hamma qismlar bitta markaz orqali gaplashadigan tuzilmani event bus ("hodisalar shinasi") deyishadi.

flowchart LR
  T["Savatga tugmasi"] -->|savat:qoshildi| M(("markaz"))
  M --> S["Savat belgisi: son"]
  M --> J["Jami summa"]
  M --> X["Qisqa xabar"]
  M --> A["Analitika"]

Diagrammada chapdagi tugma o'ngdagi to'rt qismning birortasini ham bilmaydi. Beshinchi qism kerak bo'lsa — yangi strelka qo'shiladi, tugma kodi o'zgarmaydi. Bu bog'liqliksiz aloqa deyiladi: qismlar faqat hodisa nomi orqali tanish.

Lekin har qulaylikning narxi bor:

  • Kuzatish qiyin. "Bu son nega o'zgardi?" degan savolga javob izlaganda, to'g'ridan-to'g'ri chaqiruvni bir bosishda topasiz. Hodisada esa "kim tinglayapti?" deb butun loyihani qidirasiz.
  • Unutilgan tinglovchi. Sahifadan olib tashlangan qism markazda tinglashda davom etsa, xotira bekorga band bo'ladi. Listener opsiyalari darsidagi signal bilan tozalashni unutmang.
  • Ortiqcha murakkablik. Ikki funksiya bir-birini chaqirishi yetarli bo'lgan joyda event bus — keraksiz qavat.

Amaliy qoida: bitta joy bitta ishni chaqirsa — oddiy funksiya chaqiruvi. Bitta voqeaga ko'p va har xil qismlar javob bersa — hodisa.

Tekshirib ko'ring: Bron formasi yuborilganda faqat bitta ish bor: tasdiq matnini ko'rsatish. Aziz buning uchun bron:yuborildi hodisasi va event bus yasamoqchi. To'g'ri qarormi?

Javob

Yo'q. Bitta voqea — bitta javob: oddiy funksiya chaqiruvi (tasdiqniKorsat()) soddaroq, o'qish va xatoni topish oson. Hodisa uchinchi-to'rtinchi "tinglovchi" paydo bo'lganda yoki forma kodi tinglovchilar haqida bilmasligi kerak bo'lganda (masalan, alohida analitika skripti) foydali bo'ladi.

6.4 Bir martalik va to'xtatiladigan tinglovchilar

Listener opsiyalari darsidagi once va signal o'z hodisalaringizda ham xuddi shunday ishlaydi. «Bahor» aksiyasi: birinchi buyurtmaga ko'k choy bepul. Xabar faqat birinchi savat:qoshildi da chiqishi kerak:

html
<p id="aksiya" hidden>Birinchi buyurtma: ko'k choy bepul!</p>
<script>
  const aksiya = document.getElementById("aksiya");

  document.addEventListener("savat:qoshildi", (event) => {
    aksiya.hidden = false;
    console.log(`Aksiya: ${event.detail.nom} bilan choy bepul`);
  }, { once: true });

  document.addEventListener("savat:qoshildi", (event) => {
    console.log(`Savatga: ${event.detail.nom}`);
  });

  for (const nom of ["Osh", "Manti"]) {
    document.dispatchEvent(
      new CustomEvent("savat:qoshildi", { detail: { nom } }),
    );
  }
</script>

Konsolda:

text
Aksiya: Osh bilan choy bepul
Savatga: Osh
Savatga: Manti

Ikkinchi savat:qoshildi da aksiya tinglovchisi yo'q edi — once: true uni birinchi chaqiruvdan keyin o'zi olib tashladi. Oddiy tinglovchi esa ikkala hodisani ham eshitdi.

Event bus'da bu ayniqsa muhim. Markaz sahifa yopilguncha yashaydi. Unga tinglovchi qo'ygan qism (masalan, yopilgan oyna) o'chirilgan bo'lsa ham, tinglovchi qolib ketaveradi va har hodisada behuda ishlaydi. Qism yo'qolganda tinglovchilarini ham olib tashlang: removeEventListener yoki bitta AbortController ning abort() i bilan.

6.5 Klass bilan — oldindan qisqa ko'rinish

Real kodda EventTarget ko'pincha klass bilan kengaytiriladi: obyekt o'z ma'lumotini saqlaydi va o'zgarganda hodisa e'lon qiladi. Klasslarni 10-qismda o'rganamiz (class sintaksisi) — hozir bu kodni tushunish shart emas, faqat shaklini tanib qoling:

js
class Savat extends EventTarget {
  taomlar = [];

  qosh(taom) {
    this.taomlar.push(taom);
    this.dispatchEvent(
      new CustomEvent("ozgardi", { detail: this.taomlar }),
    );
  }
}

extends EventTarget — "Savat ham hodisa yubora oladigan obyekt bo'lsin". Bu naqsh keyinroq Web Components da va freymvorklarda tez-tez uchraydi.

7. DevTools'da o'z hodisalaringiz

O'z hodisangiz ishlamasa, ikkita yordamchi bor:

  • Event Listeners yorlig'i (Elements paneli) brauzer hodisalari kabi o'zinikilarini ham ko'rsatadi: savat:ochirish bandi va qaysi fayl, qaysi qatorda tinglanayotgani.
  • Console'da getEventListeners(document) — document dagi barcha tinglovchilar ro'yxati, hodisa nomlari bo'yicha guruhlangan. Nomdagi xatoni (qoshlidi) shunda darhol ko'rasiz.
js
getEventListeners(document);
// {click: Array(1), savat:ochirish: Array(1)}
monitorEvents(document, "savat:ochirish");
// har savat:ochirish hodisasi konsolga chiqadi

getEventListeners va monitorEvents faqat DevTools konsolida ishlaydi (Klaviatura hodisalari darsida monitorEvents ni ko'rgansiz).

8. Ko'p uchraydigan xatolar

8.1 bubbles: true ni unutish

Hodisa elementda yuborildi, document da tinglanyapti — jimlik. Tuzatish: { bubbles: true } yoki hodisani tinglanayotgan joyning o'ziga yuborish.

8.2 Ma'lumot detail dan tashqarida

new CustomEvent("x", { nom: "Osh" }) → event.detail — null. Tuzatish: { detail: { nom: "Osh" } }.

8.3 Tinglovchi hodisadan keyin qo'yildi

dispatchEvent sinxron — kech qo'yilgan tinglovchi o'tib ketgan hodisani eshitmaydi. Tuzatish: tinglovchilarni avval qo'ying (odatda fayl boshida), keyin hodisa yuboring.

8.4 Nomdagi xato

"savat:qoshildi" va "savat:qoshlidi" — xato xabari yo'q, natija ham yo'q. Tuzatish: nomni bitta o'zgarmasda saqlang va getEventListeners bilan tekshiring.

8.5 Mantiqni soxta click ga bog'lash

Boshqa joydan "tugmani bosgandek" qilish uchun tugma.dispatchEvent(new MouseEvent("click")) yozish. Bu chalkash: tinglovchi haqiqiy bosish bilan kodni farqlay olmaydi, isTrusted esa false. Tuzatish: ishni funksiyaga chiqaring va ikkala joydan ham shu funksiyani chaqiring. Tugmani dasturdan bosish rostdan kerak bo'lsa — tugma.click() (u ham isTrusted: false, lekin brauzerning odatiy bosish xulqini to'liq bajaradi).

9. Mashqlar

1-mashq (oson): Yangi bron

bron:yangi nomli hodisa yarating va document ga yuboring. detail da mehmon ismi va odam soni bo'lsin: Ali, 4. Tinglovchi konsolga Yangi bron: Ali, 4 kishi chiqarsin.

html
<p>Bronlar paneli</p>
<script>
  // shu yerga yozing
</script>
Yechim
html
<p>Bronlar paneli</p>
<script>
  document.addEventListener("bron:yangi", (event) => {
    const { ism, odam } = event.detail;
    console.log(`Yangi bron: ${ism}, ${odam} kishi`);
  });

  document.dispatchEvent(new CustomEvent("bron:yangi", {
    detail: { ism: "Ali", odam: 4 },
  }));
</script>

Konsolda:

text
Yangi bron: Ali, 4 kishi

Tinglovchi yuborishdan oldin yozilgan — aks holda hodisa o'tib ketardi. bubbles kerak emas: hodisa to'g'ridan-to'g'ri document ga yuborildi.

2-mashq (o'rta): Savat hisoblagichi

Har taom kartasida "Savatga" tugmasi bor. Tugma bosilganda u savat:qoshildi hodisasini (detail — nom va narx) o'zidan yuborsin. Ikki alohida tinglovchi document da: biri savatdagi taomlar sonini, ikkinchisi jami summani yangilasin. Har qo'shilishda konsolga Qo'shildi: Osh chiqsin.

Ishora: taom ma'lumoti — kartaning data-nom va data-narx atributlarida (dataset), son uchun Number() (Matn ↔ son). Bosishni ham delegation bilan oling.

html
<p>Savatda: <output id="soni">0</output> ta,
   jami <output id="jami">0</output> so'm</p>
<ul id="menyu">
  <li data-nom="Osh" data-narx="35000">Osh
    <button type="button">Savatga</button></li>
  <li data-nom="Ko'k choy" data-narx="5000">Ko'k choy
    <button type="button">Savatga</button></li>
</ul>
<script>
  // shu yerga yozing
</script>
Yechim
html
<p>Savatda: <output id="soni">0</output> ta,
   jami <output id="jami">0</output> so'm</p>
<ul id="menyu">
  <li data-nom="Osh" data-narx="35000">Osh
    <button type="button">Savatga</button></li>
  <li data-nom="Ko'k choy" data-narx="5000">Ko'k choy
    <button type="button">Savatga</button></li>
</ul>
<script>
  // 1. Tugmalar: faqat e'lon qiladi
  const menyu = document.getElementById("menyu");
  menyu.addEventListener("click", (event) => {
    const tugma = event.target.closest("button");
    if (tugma === null) {
      return;
    }
    const { nom, narx } = tugma.closest("li").dataset;
    tugma.dispatchEvent(new CustomEvent("savat:qoshildi", {
      detail: { nom, narx: Number(narx) },
      bubbles: true,
    }));
  });

  // 2. Hisoblagich
  let soni = 0;
  document.addEventListener("savat:qoshildi", (event) => {
    soni++;
    document.getElementById("soni").textContent = soni;
    console.log(`Qo'shildi: ${event.detail.nom}`);
  });

  // 3. Jami summa
  let jami = 0;
  document.addEventListener("savat:qoshildi", (event) => {
    jami += event.detail.narx;
    document.getElementById("jami").textContent = jami;
  });
</script>

Osh va ko'k choyni bir martadan bossangiz: "Savatda: 2 ta, jami 40000 so'm", konsolda Qo'shildi: Osh va Qo'shildi: Ko'k choy. Uchala qism bir-birini bilmaydi. Hisoblagichni o'chirsangiz, summa ishlashda davom etadi.

3-mashq (qiyin): Bronni bekor qilish — ruxsat bilan

Bronlar ro'yxatida har qatorda "Bekor qilish" tugmasi bor. Tugma bosilganda bron:bekor hodisasi cancelable bo'lib yuborilsin. Alohida "qoida" tinglovchisi data-tolangan="ha" li bronni bekor qilishga yo'l qo'ymasin va konsolga Oldindan to'langan: Malika — kassaga murojaat qiling yozsin. Ruxsat bo'lsa — qator o'chsin va Bekor qilindi: Ali chiqsin.

Yechim
html
<ul id="bronlar">
  <li data-ism="Ali">Ali, 4 kishi, 19:00
    <button type="button">Bekor qilish</button></li>
  <li data-ism="Malika" data-tolangan="ha">Malika, 2 kishi, 20:00
    <button type="button">Bekor qilish</button></li>
</ul>
<script>
  const bronlar = document.getElementById("bronlar");

  bronlar.addEventListener("click", (event) => {
    const qator = event.target.closest("li");
    if (!event.target.matches("button") || qator === null) {
      return;
    }
    const ruxsat = qator.dispatchEvent(new CustomEvent("bron:bekor", {
      detail: { ism: qator.dataset.ism },
      bubbles: true,
      cancelable: true,
    }));
    if (ruxsat) {
      qator.remove();
      console.log(`Bekor qilindi: ${qator.dataset.ism}`);
    }
  });

  document.addEventListener("bron:bekor", (event) => {
    if (event.target.dataset.tolangan === "ha") {
      event.preventDefault();
      const ism = event.detail.ism;
      console.log(
        `Oldindan to'langan: ${ism} — kassaga murojaat qiling`,
      );
    }
  });
</script>

Malika qatoridagi tugma: konsolda Oldindan to'langan: Malika — ..., qator joyida. Ali qatoridagi: Bekor qilindi: Ali, qator yo'qoladi. Ro'yxat kodi "to'lov" haqida hech narsa bilmaydi — qoida alohida tinglovchida. cancelable: true ni olib tashlab ko'ring: preventDefault ishlamay qoladi va Malikaning broni ham o'chadi.

4-mashq: Portfolio qadami — tema o'zgarganini e'lon qilish

addEventListener asoslari darsida mavzu tugmasi ikki ishni qiladi: temaniQoy ni chaqiradi va o'zining aria-pressed ini almashtiradi. Muammo: temaniQoy ni boshqa joydan chaqirsangiz, tugma buni bilmaydi. DevTools konsolida temaniQoy("qorongi") deb sinang — sayt qorong'i, tugma esa "bosilmagan" (aria-pressed="false") holida qoladi. Malika NVDA bilan noto'g'ri holatni eshitadi. Keyinroq tema boshqa joylardan ham o'zgaradi: tizim sozlamasidan (matchMedia) va saqlangan tanlovdan (11-qism).

Yechim — "kim o'zgartirgan bo'lsa ham, tema o'zgardi" degan hodisa:

  1. temaniQoy oxirida document ga tema:ozgardi hodisasini yuboring. detail da — haqiqiy tema (html.dataset.tema, bo'lmasa "tizim") va hisoblangan color-scheme.
  2. document da bitta tinglovchi: tugmaning aria-pressed ini detail.tema ga qarab qo'ysin.
  3. Tugmaning click tinglovchisidan aria-pressed ni almashtiradigan qatorni olib tashlang — endi buni tinglovchi qiladi.
Yechim

asosiy.js dagi tema qismi:

js
function temaniQoy(tema) {
  const html = document.documentElement;
  if (tema === "yorug" || tema === "qorongi") {
    html.dataset.tema = tema;
  } else {
    delete html.dataset.tema;
  }
  const sxema = getComputedStyle(html).colorScheme;
  document.dispatchEvent(new CustomEvent("tema:ozgardi", {
    detail: { tema: html.dataset.tema ?? "tizim", sxema },
  }));
  return sxema;
}

const temaTugma = document.getElementById("tema-tugma");

document.addEventListener("tema:ozgardi", (event) => {
  const qorongi = event.detail.tema === "qorongi";
  temaTugma?.setAttribute("aria-pressed", String(qorongi));
});

temaTugma?.addEventListener("click", () => {
  const yoqilgan = temaTugma.getAttribute("aria-pressed") === "true";
  temaniQoy(yoqilgan ? "yorug" : "qorongi");
});

Kichik nusxada tekshiramiz: temaniQoy ni tugmasiz, to'g'ridan-to'g'ri chaqiramiz — xuddi DevTools konsolidan chaqirgandek:

html
<style>
  :root { color-scheme: light dark; }
  [data-tema="qorongi"] { color-scheme: dark; }
</style>
<button type="button" id="tema-tugma" aria-pressed="false">
  Qorong'i rejim</button>
<script>
  function temaniQoy(tema) {
    const html = document.documentElement;
    if (tema === "yorug" || tema === "qorongi") {
      html.dataset.tema = tema;
    } else {
      delete html.dataset.tema;
    }
    const sxema = getComputedStyle(html).colorScheme;
    document.dispatchEvent(new CustomEvent("tema:ozgardi", {
      detail: { tema: html.dataset.tema ?? "tizim", sxema },
    }));
    return sxema;
  }

  const temaTugma = document.getElementById("tema-tugma");
  document.addEventListener("tema:ozgardi", (event) => {
    const qorongi = event.detail.tema === "qorongi";
    temaTugma?.setAttribute("aria-pressed", String(qorongi));
    console.log(`tema:ozgardi — ${event.detail.tema}`);
  });
  temaTugma?.addEventListener("click", () => {
    const bosilgan = temaTugma.getAttribute("aria-pressed");
    temaniQoy(bosilgan === "true" ? "yorug" : "qorongi");
  });

  console.log(temaTugma.getAttribute("aria-pressed"));
  temaniQoy("qorongi");
  console.log(temaTugma.getAttribute("aria-pressed"));
  temaniQoy("kok");
  console.log(temaTugma.getAttribute("aria-pressed"));
</script>

Konsolda:

text
false
tema:ozgardi — qorongi
true
tema:ozgardi — tizim
false

Tugma bosilmagan bo'lsa ham, temaniQoy("qorongi") dan keyin aria-pressed — "true". Noto'g'ri qiymat ("kok") tizim rejimiga qaytardi va hodisa haqiqiy holatni — tizim ni — e'lon qildi. Tugmani bosib ham ko'ring: har bosishda bitta tema:ozgardi qatori chiqadi.

Nega detail ga tema argumentini emas, html.dataset.tema ni yozdik? Hodisa "nima so'raldi" ni emas, "nima bo'ldi" ni aytishi kerak. Tinglovchilar haqiqiy holatga ishonsin.

Endi temaniQoy tugma haqida hech narsa bilmaydi, tugma esa kim temani o'zgartirganini bilmaydi. 11-qismda saqlangan temani tiklaganingizda tugma o'zi to'g'ri holatga keladi.

Commit: JS: tema o'zgarishini tema:ozgardi hodisasi bilan e'lon qil.

10. Real ishda

  • Web Components — o'z HTML teglaringiz (<bahor-savat>) tashqariga faqat CustomEvent orqali "gapiradi". Bu kursning Web Components darsida.
  • Freymvorklar — Vue'dagi emit, Svelte'dagi dispatch va Angular'dagi EventEmitter — aynan shu g'oyaning o'z versiyalari. React'da esa ko'pincha funksiya (onChange) uzatiladi. Kursda alohida o'rganamiz.
  • Node.js — server tomonda EventEmitter bor: oqimlar (stream), server ulanishlari hodisa bilan ishlaydi. EventTarget ham Node'da bor — buni "Ishga tushir" bilan ko'rdingiz.
  • Analitika va kengaytmalar — sahifa kodi savat:qoshildi ni e'lon qiladi, analitika skripti esa uni tinglaydi. Savat kodi analitika haqida hech narsa bilmaydi.
  • Intervyuda: "CustomEvent va Event farqi?", "dispatchEvent sinxronmi?", "Event bus'ning kamchiliklari?" — tez-tez so'raladi.

Xulosa

  • new Event("nom") yoki new CustomEvent("nom", { detail }) — o'z hodisangiz; dispatchEvent — yuborish, addEventListener — tinglash.
  • dispatchEvent sinxron: barcha tinglovchilar shu zahoti ishlaydi; kech qo'yilgan tinglovchi hodisani eshitmaydi.
  • Ma'lumot faqat detail da; bubbles va cancelable standart holatda false.
  • cancelable + preventDefault → dispatchEvent false qaytaradi — "ruxsat so'rash" naqshi.
  • new EventTarget() — elementsiz hodisa markazi (event bus); Node'da ham bor. Ko'p qism bitta voqeaga javob bersa — hodisa, bittasi bo'lsa — oddiy chaqiruv.
  • Nom: kichik harf, prefiks bilan (savat:qoshildi), brauzer nomlarisiz.

Keyingi dars: Sahifa hayot sikli va Page Visibility — sahifa yuklanishdan yopilishgacha qanday bosqichlardan o'tadi va "orqaga" tugmasi bosilganda nima bo'ladi.

Manbalar

  • MDN: "Creating and dispatching events", "CustomEvent: CustomEvent() constructor" — developer.mozilla.org
  • MDN: "EventTarget: dispatchEvent() method", "EventTarget: EventTarget() constructor" — developer.mozilla.org
  • DOM Standard: "Dispatching events", "Interface CustomEvent" — dom.spec.whatwg.org
  • Chrome DevTools: "Console Utilities API reference" (getEventListeners, monitorEvents) — developer.chrome.com
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Custom events JavaScript'da: CustomEvent, dispatchEvent va EventTarget — IlmHamroh