Seriya: JavaScript Basics Dars 89

Web Components

Web Components nima, uchta ustuni (Custom Elements, Shadow DOM, HTML Templates), lifecycle callback'lar (constructor, connectedCallback, disconnectedCallback, attributeChangedCallback), observedAttributes, <template> va named slot'lar, custom event'lar orqali composed: true, customElements API metodlari va React/Vue bilan taqqoslash.

89-dars

Web Components

Oldingi mavzuda Shadow DOM'ni ko'rdik — bu izolyatsiya vositasi. Lekin bitta savol qoladi: qanday qilib butunlay yangi, qayta ishlatiladigan HTML teg yarataman — masalan, <my-button> yoki <user-card>, xuddi <video> yoki <select> kabi o'zining ichki mantig'i, holati va uslubi bilan?

React/Vue buni framework darajasida hal qiladi (<Button /> komponenti) — lekin bu faqat shu framework ichida ishlaydi. Web Components — bu brauzerning o'zida, native shu imkoniyatni beruvchi standart, hech qanday framework/build tool kerak emas.

Uchta ustun — birga ishlaydigan API'lar

1. Custom Elements  → yangi HTML teg RO'YXATDAN o'tkazish
2. Shadow DOM       → ichki tuzilmani IZOLYATSIYA qilish (o'tgan mavzu)
3. HTML Templates   → qayta ishlatiladigan HTML SHABLON (<template>, <slot>)

1. Custom Elements — yangi teg yaratish

class UserCard extends HTMLElement {
  // konstruktor — element YARATILGANDA chaqiriladi, lekin hali DOM'ga ULANMAGAN
  constructor() {
    super(); // HTMLElement konstruktorini chaqirish MAJBURIY
    console.log('constructor: hali DOM\'da emas');
  }

  // element HAQIQATAN sahifaga qo'shilganda chaqiriladi
  connectedCallback() {
    console.log('connectedCallback: endi DOM\'da');
    this.innerHTML = `<div>Salom, foydalanuvchi!</div>`;
  }

  // element sahifadan olib tashlanganda
  disconnectedCallback() {
    console.log('disconnectedCallback: DOM\'dan chiqarildi');
  }
}

customElements.define('user-card', UserCard);
<user-card></user-card>

Bu yerda customElements.define('user-card', UserCard) — brauzerga aytadi: "har qachon HTML'da <user-card> tegini ko'rsang, uni oddiy HTMLUnknownElement emas, balki UserCard klassining instansiyasi sifatida ishla".

Nega constructorda emas, connectedCallbackda ishlash kerak

Bu — eng ko'p xato qilinadigan joy. constructor chaqirilganda, element hali DOM daraxtiga ulanmagan bo'lishi mumkin:

const card = document.createElement('user-card'); // constructor SHU YERDA ishlaydi
// lekin card hali sahifada YO'Q!
document.body.appendChild(card); // FAQAT SHU YERDA connectedCallback ishga tushadi

Agar siz constructorda this.innerHTML = ... yozsangiz, ba'zi holatlarda (masalan, atributlar hali o'rnatilmagan bo'lsa) noto'g'ri natija olishingiz mumkin. Shuning uchun qat'iy qoida: DOM bilan ishlash — faqat connectedCallbackda.

Lifecycle callback'lar — to'liq jadval

Callback Qachon chaqiriladi
constructor() Element yaratilganda (hali DOM'da emas)
connectedCallback() Element DOM'ga qo'shilganda (har safar, hatto qayta qo'shilsa ham)
disconnectedCallback() Element DOM'dan olib tashlanganda
attributeChangedCallback(name, oldVal, newVal) Kuzatilayotgan atribut o'zgarganda
adoptedCallback() Element boshqa document'ga ko'chirilganda (kamdan-kam, masalan iframe orasida)

Atributlarni kuzatish — observedAttributes

class RatingStars extends HTMLElement {
  static get observedAttributes() {
    return ['value']; // faqat shu atributlar KUZATILADI
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'value') {
      this.render();
    }
  }

  connectedCallback() {
    this.render();
  }

  render() {
    const value = Number(this.getAttribute('value')) || 0;
    this.innerHTML = '★'.repeat(value) + '☆'.repeat(5 - value);
  }
}

customElements.define('rating-stars', RatingStars);
<rating-stars value="3"></rating-stars>

Muhim: attributeChangedCallback faqat static get observedAttributes() ro'yxatida ko'rsatilgan atributlar uchun ishlaydi — bu performance uchun ataylab shunday: agar brauzer har bir atribut o'zgarishini kuzatsa, bu keraksiz overhead bo'lardi. Siz aniq qaysi atributlar muhimligini ro'yxat qilib berasiz.


2. HTML Template — samarali shablon yaratish

<template id="card-template">
  <style>
    .card { border: 1px solid #ccc; padding: 16px; border-radius: 8px; }
  </style>
  <div class="card">
    <slot name="title">Sarlavha yo'q</slot>
    <slot name="body"></slot>
  </div>
</template>

<template> tegining muhim xususiyati: uning ichidagi HTML parse qilinadi (brauzer uni tushunadi, DOM tuzilmasiga aylantiradi), lekin hech qachon render qilinmaydi va bajarilmaydi — ichidagi <img> yuklanmaydi, <script> ishga tushmaydi, u shunchaki "inert" (harakatsiz) DOM bo'lagi sifatida xotirada yotadi, faqat siz JS orqali klonlaganingizda jonlanadi:

class MyCard extends HTMLElement {
  connectedCallback() {
    const template = document.getElementById('card-template');
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.appendChild(template.content.cloneNode(true)); // true = CHUQUR klonlash
  }
}

customElements.define('my-card', MyCard);

template.content — bu maxsus DocumentFragment obyekti. cloneNode(true) — butun ichki tuzilmani nusxalab oladi, shu bilan bir vaqtda bir nechta <my-card> elementi bitta shablondan, bir-biridan mustaqil nusxalar yaratishi mumkin.

Named Slots — bir nechta "teshik"

<my-card>
  <span slot="title">Mening kartam</span>
  <p slot="body">Bu — karta matni.</p>
</my-card>

slot="title" atributi orqali, tashqi element aniq qaysi <slot name="..."> joyiga tushishini belgilaydi — bu shablon ichida bir nechta joylashtiriladigan joy borligini bildiradi (oldingi mavzudagi oddiy <slot></slot> esa faqat standart, nomsiz joy edi).


3. To'liq amaliy misol — hammasi birga

class CounterButton extends HTMLElement {
  #count = 0; // private field (haqiqiy JS private, Shadow DOM'dan mustaqil)

  static get observedAttributes() {
    return ['start'];
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'start') this.#count = Number(newVal) || 0;
  }

  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        button { padding: 8px 16px; font-size: 16px; }
      </style>
      <button>Bosildi: ${this.#count}</button>
    `;

    shadow.querySelector('button').addEventListener('click', () => {
      this.#count++;
      shadow.querySelector('button').textContent = `Bosildi: ${this.#count}`;

      // tashqariga CUSTOM EVENT chiqarish
      this.dispatchEvent(new CustomEvent('count-changed', {
        detail: { count: this.#count },
        bubbles: true,
        composed: true // Shadow chegarasidan o'tishi uchun MAJBURIY
      }));
    });
  }
}

customElements.define('counter-button', CounterButton);
<counter-button start="5"></counter-button>

<script>
  document.querySelector('counter-button')
    .addEventListener('count-changed', (e) => {
      console.log('Yangi qiymat:', e.detail.count);
    });
</script>

Nega composed: true bu yerda majburiy

CustomEvent yaratilganda, standart holatda u Shadow chegarasidan tashqariga chiqmaydi (composed: false — default). Agar siz tashqi kod (light DOM'dagi) bu eventni ushlashini xohlasangiz, aniq composed: true berishingiz kerak — bu Shadow DOM'ning izolyatsiya printsipining tabiiy davomi: hatto event'lar ham, ataylab ruxsat berilmaguncha, chegaradan o'tmaydi.


4. customElements obyektining boshqa metodlari

Metod Vazifasi
customElements.define(name, class, options?) Yangi elementni ro'yxatdan o'tkazadi
customElements.get(name) Berilgan nom bo'yicha klassni qaytaradi (agar ro'yxatdan o'tgan bo'lsa)
customElements.whenDefined(name) Element ro'yxatdan o'tguncha kutadigan Promise qaytaradi
customElements.upgrade(node) Mavjud DOM elementini majburan "yangilaydi"

whenDefined — foydali holat: agar <user-card> HTML'da allaqachon yozilgan, lekin uning JS klassi hali yuklanmagan bo'lsa (masalan, script tag pastda), brauzer uni vaqtincha HTMLElementning oddiy, "jonlanmagan" versiyasi sifatida ko'rsatadi — :not(:defined) CSS selektori orqali buni hatto stilize qilish mumkin (masalan, "yuklanmoqda" effekti):

user-card:not(:defined) {
  opacity: 0;
}

5. Nega bu React/Vue'dan farq qiladi — muhim tushuncha

React komponenti Web Component
Ishlash muhiti Faqat React ilovasi ichida Har qanday HTML sahifada (Vue, Angular, oddiy HTML — hammasida)
Qanday render qiladi Virtual DOM orqali, qayta-qayta taqqoslab (diffing) To'g'ridan-to'g'ri, native DOM API orqali
Holat (state) boshqaruvi useState, virtual re-render Oddiy JS class field, siz o'zingiz DOM'ni yangilaysiz
Framework kerakmi Ha, React runtime kerak Yo'q — bu browser'ning o'z standarti

Amalda, katta kompaniyalar (masalan, YouTube'ning ba'zi qismlari, GitHub'ning <relative-time>, <clipboard-copy> kabi elementlari) Web Components'dan framework'dan mustaqil, uzoq muddat barqaror komponentlar yaratish uchun foydalanadi — chunki bu brauzer standarti, React versiyasi o'zgarib ketishidan qat'i nazar, u doim ishlayveradi.


6. V8 va Blink darajasida — qisqa xulosa

customElements.define() chaqirilganda, Blink (rendering engine) ichida maxsus registryga yozuv qo'shiladi: "teg nomi → JS klassi" moslashuvi. Har safar HTML parser (yoki innerHTML, yoki createElement) shu teg nomiga duch kelganda, u oddiy elementni emas, balki ro'yxatdan o'tgan JS klassning yangi instansiyasini yaratadi, va lifecycle callback'larni aynan to'g'ri tartibda (constructor → connectedCallback) V8'ga signal beradi.

Bu — Shadow DOM'da ko'rgan C++ binding orqali JS'ga signal berish mexanizmining xuddi shu davomi: haqiqiy DOM daraxti boshqaruvi Blink'da, JS mantiq esa V8'da, ular doimiy, tez chaqiruvlar orqali sinxron ishlaydi.