IlmHamroh
JavaScript Full-stack/13-qism. Brauzer API'lari, performans va xavfsizlik4/32-dars20 daqiqa
Mundarija (36)

JavaScript ResizeObserver va MutationObserver: o'lcham va DOM o'zgarishini kuzatish

Qisqacha: ResizeObserver — element o'lchami o'zgarganda xabar beradi (window ning resize hodisasidan farqli, istalgan element uchun). MutationObserver — DOM o'zgarganda xabar beradi: bola qo'shildi yoki o'chirildi, atribut yoki matn o'zgardi. Ikkalasi IntersectionObserver kabi ishlaydi: new …Observer(callback), observe(element, sozlamalar), ish tugagach disconnect(). MutationObserver o'zgarishlarni yig'ib, bitta microtask'da massiv qilib beradi.

Bu darsda

  • ResizeObserver bilan istalgan elementning o'lchamini kuzatib, unga qarab JavaScript'dan moslasha olasiz.
  • contentRect, contentBoxSize va borderBoxSize farqini bilasiz.
  • MutationObserver bilan bola elementlar, atributlar va matn o'zgarishini kuzata olasiz (childList, attributes, characterData, subtree).
  • Ikkala kuzatuvchining "cheksiz sikl" tuzoqlarini taniysiz va ulardan qochasiz.

Oldin bilishingiz kerak: IntersectionObserver, Fokus, scroll va resize hodisalari, Element o'lchamlari va scroll, Event loop.

1. Nega bu kerak?

O'tgan darsda kuzatuvchilar oilasining birinchi a'zosini ko'rdik: IntersectionObserver — "element ko'rindimi?". Bugun qolgan ikkitasi. Ular boshqa savollarga javob beradi:

  • "Element kattalashdimi yoki kichraydimi?" «Bahor» menyu kartasi yon panelda tor, asosiy qismda keng. Kartaning ichidagi grafik yoki rasm uning kengligiga qarab qayta chizilishi kerak. Fokus, scroll va resize darsida aytgan edik: resize faqat window da keladi. Yon panel ochilib, karta torayganda oyna o'lchami o'zgarmaydi — resize jim. Kerakli vosita — ResizeObserver.
  • "DOM'da nimadir o'zgardimi?" Sahifaga boshqa kompaniyaning chat vidjeti ulangan. U o'z tugmasini qachon qo'shishini siz bilmaysiz, uning kodiga tegolmaysiz. Lekin tugma paydo bo'lishi bilan uni o'zingiz sozlamoqchisiz. Yoki DOM'ni kim o'zgartirayotganini tekshirmoqchisiz. Kerakli vosita — MutationObserver.

Ikkalasi bir xil qolipda yozilgani yaxshi: o'tgan darsdagi hamma narsa — callback, observe, unobserve, disconnect — bu yerda ham ishlaydi.

2. ResizeObserver: element o'lchami

2.1 Birinchi misol

js
const card = document.querySelector(".menu-card");

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const width = entry.contentBoxSize[0].inlineSize;
    console.log(`Karta kengligi: ${Math.round(width)} px`);
  }
});

observer.observe(card);

Tanish qolip: callback'ga yozuvlar massivi keladi, har yozuvda target (qaysi element) va o'lcham. observe dan keyin callback bir marta darhol keladi — boshlang'ich o'lcham bilan, xuddi IntersectionObserver dagi kabi.

Nima o'lchamni o'zgartiradi? Hamma narsa: oyna kengligi, ota element, CSS klass, ichidagi matn ko'paygani, foydalanuvchi sudrab kattalashtirgani. Sabab muhim emas — kuzatuvchi natijani ko'radi.

2.2 Qaysi o'lcham? Uch xil quti

Box model dan eslang: element kontent, padding, border qatlamlaridan iborat. Yozuvda shular uchun alohida maydonlar bor:

Maydon Nima Qachon kerak
contentBoxSize[0] kontent qutisi (padding'siz) ichiga nima sig'ishini hisoblash
borderBoxSize[0] border bilan birga element ekranda qancha joy egallaydi
contentRect kontent qutisi, eski shakl eski kodda uchratasiz

contentBoxSize va borderBoxSize — massiv: har birining [0] elementida inlineSize (kenglik, matn yo'nalishi bo'yicha) va blockSize (balandlik) bor. Massiv bo'lishining sababi — bir nechta ustunga bo'lingan element; hozircha har doim bitta element. inlineSize deyilgani Mantiqiy xususiyatlar darsidagi g'oya: o'ngdan chapga yoziladigan tillarda ham to'g'ri ishlaydi.

Chrome 154 da sinab ko'rdik: width: 200px; padding: 10px; border: 2px li blok uchun contentBoxSize[0].inlineSize — 200, borderBoxSize[0].inlineSize — 224 (200 + 2 × 10 + 2 × 2).

contentRect — API'ning birinchi versiyasidan qolgan DOMRectReadOnly obyekti (width, height). U ham ishlaydi va ko'p eski kodda bor. Yangi kodda contentBoxSize ni tanlang.

2.3 «Bahor»: karta o'z kengligiga moslashadi

Karta o'ng pastki burchagidan sudrab kengaytiriladi (resize: horizontal). Kengligi 320 px dan oshsa — rasm chapga, matn o'ngga o'tadi:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  .card { resize: horizontal; overflow: auto; width: 280px;
    min-width: 180px; max-width: 100%; box-sizing: border-box;
    padding: 12px; border: 1px solid #ccd; border-radius: 0.5rem;
    height: 200px; }
  .photo { height: 80px; border-radius: 0.5rem;
    background: linear-gradient(135deg, #e8a33d, #c0392b); }
  .card h3 { margin: 0.5rem 0 0; }
  .card.wide { display: flex; gap: 1rem; align-items: center; }
  .card.wide .photo { flex: 0 0 120px; }
  .card.wide h3 { margin-top: 0; }
</style>
<p>Kartani o'ng pastki burchagidan sudrab kengaytiring:</p>
<div class="card" id="card">
  <div class="photo"></div>
  <div><h3>Osh</h3><p>35 000 so'm</p></div>
</div>
<p>Kontent kengligi: <output id="size">…</output></p>
<script>
  const card = document.getElementById("card");
  const sizeOut = document.getElementById("size");
  let mode = null;

  const observer = new ResizeObserver((entries) => {
    const width = entries[0].contentBoxSize[0].inlineSize;
    sizeOut.textContent = `${Math.round(width)} px`;
    const newMode = width >= 320 ? "keng" : "tor";
    if (newMode !== mode) {
      mode = newMode;
      card.classList.toggle("wide", mode === "keng");
      console.log(`Rejim: ${mode}`);
    }
  });
  observer.observe(card);
</script>

Konsolda:

text
Rejim: tor

Boshida karta 280 px, kontenti 254 px (280 − 2 × 12 padding − 2 × 1 border) — "tor". Burchakdan sudrab kengaytiring: kontent 320 px dan oshganda konsolga Rejim: keng chiqadi va rasm chapga o'tadi. Qaytarib toraytirsangiz — Rejim: tor. Biz Chrome 154 da kartaning width ini 400 px qilib tekshirdik: Rejim: keng, kontent 374 px.

mode o'zgaruvchisi — sudrash paytida callback ko'p marta keladi, lekin konsolga faqat rejim almashganda yozamiz. Kartaga height: 200px ham bejiz berilmagan — sababi "Tuzoq" bo'limida.

2.4 CSS'ning o'zi yetadimi?

Bu misolni bilib turib tanladik: aynan shu ishni CSS'ning o'zi ham qila oladi — Container queries darsidagi @container (min-width: 320px). Va CSS yaxshiroq: JavaScript yuklanmasa ham ishlaydi, birinchi chizishdayoq to'g'ri.

Qoida: ko'rinish o'zgarishi bo'lsa — CSS (container queries). ResizeObserver — o'lchamni JavaScript bilishi kerak bo'lganda:

  • <canvas> dagi grafikni yangi o'lchamda qayta chizish (canvas o'zi "cho'zilmaydi" — Canvas 2D asoslari).
  • "Nechta tugma sig'adi?" hisobi — qolganini «Yana» menyusiga yig'ish (2-mashq).
  • Matn qisqartirilganini aniqlash va to'liq matnni title ga qo'yish.
  • Iframe yoki vidjetning balandligini kontentga moslash. Masalan, shu darsdagi jonli oynalar ham o'z balandligini ResizeObserver bilan o'lchab, dars sahifasiga xabar qiladi.

Tekshirib ko'ring: Oyna o'lchami o'zgarmadi, lekin yon panel yopilib, asosiy ustun kengaydi. window dagi resize tinglovchisi va ResizeObserver dan qaysi biri xabar beradi?

Javob

Faqat ResizeObserver. resize hodisasi faqat brauzer oynasi (viewport) o'lchami o'zgarganda keladi. Kuzatuvchi esa elementning o'zini kuzatadi — o'lcham nima sababdan o'zgarganidan qat'i nazar.

3. ResizeObserver qachon va qanday chaqiriladi

3.1 Layout'dan keyin, chizishdan oldin

requestAnimationFrame darsidagi kadr sxemasini eslang: task → microtask'lar → rAF → style va layout → paint. ResizeObserver callback'lari layout'dan keyin, paint'dan oldin chaqiriladi. Shuning uchun:

  • O'lcham allaqachon hisoblangan — callback'da uni o'qish layout'ni majburlamaydi.
  • Callback'da DOM'ni o'zgartirsangiz, natija shu kadrda chiziladi. Foydalanuvchi "noto'g'ri" oraliq holatni ko'rmaydi.
  • Bitta kadrda o'lcham o'n marta o'zgarsa ham, callback bir marta — yakuniy o'lcham bilan. Debounce shart emas.

3.2 Tuzoq: o'lchamni o'zgartiradigan callback

Callback'da kuzatilayotgan elementning o'lchamini o'zgartirsangiz nima bo'ladi? Brauzer shu kadr ichida yana o'lchaydi va yana callback chaqiradi. Bu cheksiz davom etmasligi uchun chegarasi bor: brauzer sikl tugamaganini sezsa, qolgan xabarlarni keyingi kadrga qoldiradi va xato chiqaradi:

js
const box = document.getElementById("box");
let n = 0;

// ❌ o'zi kuzatayotgan elementning kengligini o'zgartiradi
new ResizeObserver(() => {
  n++;
  box.style.width = `${100 + n}px`;
}).observe(box);

Chrome 154 konsolida (har kadrda yana bittadan):

text
Uncaught ResizeObserver loop completed with undelivered notifications.

Tarjimasi: "ResizeObserver sikli yetkazilmagan xabarlar bilan tugadi". Ya'ni: "o'lchamlar shu kadr ichida tinchimadi, ba'zi xabarlarni keyingi kadrga qoldirdim". Bu xato window dagi error hodisasiga keladi, event.error esa null — stack trace yo'q. Shuning uchun xatoni topish qiyin: u qaysi kodning aybi ekanini aytmaydi.

Xato faqat kuzatilayotgan elementning o'zidan emas. Brauzer callback'lar orasida o'lchamlarni qayta tekshiradi: callback DOM'ni o'zgartirib, boshqa kuzatuvchi kuzatayotgan ota element (masalan, butun sahifa) o'lchami o'zgarsa ham shu xato chiqadi. Biz buni shu darsning o'zida uchratdik. Jonli oynalar o'z balandligini dars sahifasiga aytish uchun <html> ni ResizeObserver bilan kuzatadi. Kartadagi rejim almashganda karta pastroq bo'lib, sahifa balandligi o'zgarardi — Chrome 154 konsoliga shu xato chiqdi. Kartaga qat'iy height berib tuzatdik.

Tuzatish:

  • Callback'da kuzatilayotgan elementning o'lchamiga ta'sir qiladigan narsani o'zgartirmang. Ichidagi rangni, klassni (o'lchamni o'zgartirmasa) yoki boshqa elementni o'zgartirish xavfsiz.
  • O'zgartirish majburiy bo'lsa — faqat qiymat haqiqatan farq qilganda (if (newMode !== mode), yuqoridagi misoldagi kabi). Shunda ikkinchi chaqiruvda hech narsa o'zgarmaydi va sikl to'xtaydi.

Diqqat: Bu xatoni ba'zan kutubxonalar ham chiqaradi va u foydalanuvchiga zarar qilmaydi. Ko'p jamoalar uni xato monitoringida filtrlab qo'yishadi. Xato monitoringi (masalan, Sentry) — foydalanuvchilar brauzerida chiqqan xatolarni yig'ib, dasturchiga ko'rsatadigan xizmat; uni keyingi qismlarda ko'ramiz. Lekin o'z kodingizda uni "yashirish" emas, sababini tuzatish kerak.

Tekshirib ko'ring: Sardor kuzatuvchi callback'ida kartaning fon rangini kengligiga qarab o'zgartirdi: tor bo'lsa — kulrang, keng bo'lsa — yashil. "loop completed" xatosi chiqadimi?

Javob

Yo'q. Fon rangi kartaning o'lchamiga ta'sir qilmaydi: brauzer qayta o'lchaganda kenglik o'sha-o'sha, yangi callback kerak emas. Xato faqat callback o'lchamni (kenglik, balandlik, padding, ichidagi matn miqdori) o'zgartirsa chiqadi.

4. MutationObserver: DOM o'zgarishlari

4.1 Nima kuzatiladi

Mutatsiya (mutation) — DOM'dagi o'zgarish. MutationObserver uch turdagi o'zgarishni kuzatadi:

Sozlama Nima kuzatiladi Yozuv type i
childList: true bola tugunlar qo'shildi/o'chirildi "childList"
attributes: true atribut o'zgardi "attributes"
characterData: true matn tugunining matni o'zgardi "characterData"

Va bitta kengaytma: subtree: true — faqat elementning o'zi emas, uning barcha avlodlari ham kuzatiladi.

observe(element, sozlamalar) da uchta turdan kamida bittasi true bo'lishi shart. Aks holda Chrome xato beradi: TypeError: Failed to execute 'observe' on 'MutationObserver': The options object must set at least one of 'attributes', 'characterData', or 'childList' to true. — "sozlamalarda kamida bittasi true bo'lishi kerak".

4.2 Yozuv (MutationRecord)

Callback'ga MutationRecord lar massivi va kuzatuvchining o'zi keladi. Yozuvning asosiy maydonlari:

Maydon Nima
type "childList", "attributes" yoki "characterData"
target o'zgargan tugun (bolalar o'zgarganda — ota)
addedNodes, removedNodes qo'shilgan va o'chirilgan tugunlar (NodeList)
attributeName, oldValue qaysi atribut; eski qiymat (so'ralgan bo'lsa)

oldValue faqat attributeOldValue: true yoki characterDataOldValue: true berilganda to'ldiriladi. Hamma atributlar emas, faqat ba'zilari kerak bo'lsa — attributeFilter: ["hidden", "data-tema"].

4.3 «Bahor»: buyurtmalar taxtasi

Oshxona ekranida buyurtmalar ro'yxati. Kuzatuvchi ro'yxatni kuzatadi va sarlavhadagi sonni o'zi yangilaydi — ro'yxatni kim o'zgartirganidan qat'i nazar:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  ul { padding-left: 1.25rem; }
  li button { font: inherit; margin-left: 0.5rem; }
  .actions button { font: inherit; padding: 0.3rem 0.7rem; }
</style>
<h3>Buyurtmalar: <span id="count">0</span></h3>
<ul id="orders"></ul>
<p class="actions">
  <button id="one" type="button">Yangi buyurtma</button>
  <button id="three" type="button">Uchtasi birdan</button>
</p>
<script>
  const list = document.getElementById("orders");
  const count = document.getElementById("count");
  const dishes = ["Osh", "Lag'mon", "Manti", "Ko'k choy"];
  let number = 0;

  const observer = new MutationObserver((records) => {
    let added = 0;
    let removed = 0;
    for (const record of records) {
      added += record.addedNodes.length;
      removed += record.removedNodes.length;
    }
    count.textContent = list.children.length;
    console.log(`${records.length} ta yozuv: +${added}, -${removed}`);
  });
  observer.observe(list, { childList: true });

  function addOrder() {
    number++;
    const li = document.createElement("li");
    li.textContent = `#${number} ${dishes[number % dishes.length]}`;
    const done = document.createElement("button");
    done.type = "button";
    done.textContent = "Tayyor";
    li.append(done);
    list.append(li);
  }

  list.addEventListener("click", (event) => {
    if (event.target.matches("button")) {
      event.target.parentElement.remove();
    }
  });
  document.getElementById("one").addEventListener("click", addOrder);
  document.getElementById("three").addEventListener("click", () => {
    addOrder();
    addOrder();
    addOrder();
  });

  addOrder();
  addOrder();
</script>

Konsolda:

text
2 ta yozuv: +2, -0

Skript oxirida ikki buyurtma qo'shildi — ikki alohida append. Lekin konsolda bitta qator: callback bir marta chaqirildi va unga ikki yozuv keldi. Endi tugmalarni sinang:

  • «Yangi buyurtma» → 1 ta yozuv: +1, -0, sarlavhada 3.
  • «Uchtasi birdan» → 3 ta yozuv: +3, -0 — bitta qator.
  • Biror buyurtmadagi «Tayyor» → 1 ta yozuv: +0, -1.

Biz har uch tugmani bosib tekshirdik — konsol va sarlavhadagi son aynan shunday.

5. MutationObserver qachon chaqiriladi

5.1 Yig'ib, microtask'da

Nega uchta append — bitta chaqiruv? MutationObserver o'zgarishlarni darhol emas, yig'ib beradi. Birinchi o'zgarishda u callback'ini microtask navbatiga qo'yadi. Joriy kod tugaguncha keyingi o'zgarishlar shu "paket"ga qo'shiladi. Kod tugagach, microtask'lar navbati keladi va callback hamma yozuvlarni birga oladi:

sequenceDiagram
  participant K as Sizning kod
  participant D as DOM
  participant M as Microtask navbati
  participant C as Kuzatuvchi callback
  K->>D: append (1)
  D->>M: callback navbatga
  K->>D: append (2)
  K->>D: append (3)
  Note over K: kod tugadi, stek bo'sh
  M->>C: callback([3 ta yozuv])

Chrome 154 da tekshirdik: o'zgarishlardan keyin queueMicrotask va setTimeout(..., 0) qo'yilgan skriptda tartib shunday chiqdi — avval sinxron kod, keyin kuzatuvchi callback'i (birinchi o'zgarishda navbatga tushgan), keyin keyingi microtask, oxirida taymer. Event loop qoidasi aynan shuni aytadi.

Bu ikki narsani anglatadi:

  • Tejamkor. Sikl ichida 1000 ta element qo'shilsa, callback bir marta, 1000 ta yozuv bilan.
  • Sinxron emas. append dan keyingi qatorda callback hali ishlamagan. "Element qo'shildi — darhol bilaman" deb kutmang.

5.2 Kutayotgan yozuvlarni darhol olish

observer.takeRecords() — navbatda turgan, hali callback'ga yetmagan yozuvlarni hozir qaytaradi va navbatdan olib tashlaydi. U kam kerak bo'ladi. Odatiy holat — disconnect dan oldin: kuzatuvni to'xtatayotganda oxirgi o'zgarishlar yo'qolmasin.

js
const pending = observer.takeRecords(); // hali ishlanmagan yozuvlar
observer.disconnect();
handleRecords(pending); // ularni ham o'zimiz ishlaymiz

Tekshirib ko'ring: Kod list.append(li) dan keyingi qatorda console.log("qo'shildi") yozadi. Kuzatuvchi callback'i ham konsolga yozadi. Qaysi biri birinchi chiqadi?

Javob

"qo'shildi" — birinchi. Kuzatuvchi callback'i microtask: u joriy sinxron kod tugagandan keyingina ishlaydi. Xuddi queueMicrotask kabi.

6. Atributlar va matnni kuzatish

6.1 Tema o'zgarishini kuzatish

«Bahor» sahifasida tema <html data-tema="yorug"> atributida (06-qism). Tema tugmasi uni o'zgartiradi. Sahifadagi grafik esa rangini temaga moslashi kerak — lekin u tema kodini bilmaydi. Kuzatuvchi atributni kuzatadi:

js
const root = document.documentElement; // <html>

const themeObserver = new MutationObserver((records) => {
  for (const record of records) {
    const now = root.dataset.tema;
    console.log(`Tema: ${record.oldValue} → ${now}`);
  }
});

themeObserver.observe(root, {
  attributes: true,
  attributeFilter: ["data-tema"], // faqat shu atribut
  attributeOldValue: true, // eski qiymat ham kerak
});

root.dataset.tema = "qorongi"; // Tema: yorug → qorongi

attributeFilter berilgani uchun <html> dagi boshqa atributlar (lang, class) o'zgarsa, kuzatuvchi jim. attributeFilter berilganda attributes: true ni yozmasa ham bo'ladi — u o'zi nazarda tutiladi. Lekin aniq yozish o'qish uchun qulay.

Shuni ham aytish kerak: o'z kodingizda tema o'zgarishini bildirish uchun hodisa yaxshiroq — Custom events va EventTarget darsidagi tema:ozgardi kabi. Kuzatuvchi — boshqa kod (kutubxona, vidjet) o'zgartirgan va u hech narsa e'lon qilmaydigan holatlar uchun.

6.2 Matn tugunlari

characterData: true — matn tugunining (text node) o'zi o'zgarganda. Bu yerda tuzoq bor. element.textContent = "yangi" matn tugunini o'zgartirmaydi — eskisini o'chirib, yangisini qo'shadi. Ya'ni bu characterData emas, childList yozuvi. characterData asosan contenteditable maydonlarda keladi — foydalanuvchi matnni to'g'ridan-to'g'ri tahrirlaganda, yoki textNode.data = "..." yozilganda.

Amaliy qoida: elementning matni o'zgarganini bilish uchun { childList: true, characterData: true, subtree: true } — ikkala yo'lni ham ushlaydi.

Tekshirib ko'ring: observe(list, { attributes: true }) berildi. Ro'yxatga yangi <li class="yangi"> qo'shildi. Kuzatuvchi nima haqida xabar beradi?

Javob

Hech narsa. childList yoqilmagan — bola qo'shilishi kuzatilmaydi. attributes esa faqat list ning o'z atributlarini kuzatadi; yangi li ning class i uning avlodiga tegishli, buning uchun subtree: true kerak edi.

7. Eski usul: mutation events

Eski kodda DOMNodeInserted, DOMSubtreeModified kabi hodisalarni uchratishingiz mumkin. Bular mutation events — MutationObserver dan oldingi usul. Ular har bir o'zgarishda sinxron ishlardi va sahifani juda sekinlashtirardi. Brauzerlar ularni olib tashladi: Chrome'da 127-versiyadan (2024) beri ishlamaydi.

Chrome 154 da sinadik: DOMNodeInserted tinglovchisi qo'yilgan ro'yxatga ikki element qo'shdik — tinglovchi bir marta ham chaqirilmadi, xato ham chiqmadi. Eski kodni yangilaganda bunday tinglovchilarni MutationObserver ga almashtiring — aks holda kod jimgina ishlamay qoladi.

8. Node'da yo'q

Ikkala kuzatuvchi ham brauzer API'si — DOM bilan ishlaydi, Node'da DOM yo'q:

js
const observer = new MutationObserver(() => {});

Node 24 da:

text
ReferenceError: MutationObserver is not defined

ResizeObserver uchun ham xuddi shunday xato. Testlarda DOM kerak bo'lsa, jsdom kabi "soxta DOM" kutubxonalari ishlatiladi — testlash qismida ko'ramiz.

9. Ko'p uchraydigan xatolar

9.1 O'zini kuzatayotgan callback

js
// ❌ callback ro'yxatga element qo'shadi — bu yangi mutatsiya
const observer = new MutationObserver(() => {
  list.append(document.createElement("hr"));
});
observer.observe(list, { childList: true });

Har callback yangi o'zgarish qiladi, u yana callback'ni chaqiradi — abadiy. Bu microtask'lar zanjiri: Event loop darsidagi "cheksiz microtask" kabi sahifa butunlay qotadi. Tuzatish: callback'da kuzatilayotgan joyni o'zgartirmang. O'zgartirish shart bo'lsa — avval disconnect(), o'zgartiring, keyin qayta observe(). Yoki shart bilan: "kerakli holat allaqachon bormi?".

9.2 Butun body ni subtree bilan kuzatish

observer.observe(document.body, { subtree: true, childList: true, attributes: true, characterData: true }) — sahifadagi har o'zgarish yozuvga aylanadi. Animatsiyali, tez yangilanadigan sahifada bu soniyasiga minglab yozuv. Tuzatish: imkon qadar kichik elementni kuzating va faqat kerakli turni yoqing (attributeFilter ham).

9.3 Kuzatishni to'xtatmaslik

Komponent sahifadan o'chirildi, kuzatuvchi esa ishlayveradi va elementlarni xotirada ushlab turadi. Tuzatish: element yo'q bo'lganda disconnect(). Har ikkala kuzatuvchida ham bu metod bor.

9.4 ResizeObserver callback'ida o'lchamni o'zgartirish

"ResizeObserver loop completed with undelivered notifications" — "ResizeObserver qachon chaqiriladi" bo'limidagi tuzoq. Tuzatish: o'lchamga ta'sir qiladigan o'zgarishni faqat qiymat farq qilganda qiling.

9.5 O'z kodingiz uchun MutationObserver

O'z funksiyangiz ro'yxatga element qo'shadi, keyin kuzatuvchi "qo'shildi" deb sonni yangilaydi. Ishlaydi, lekin aylanma yo'l: ma'lumot qayerda o'zgarishini siz allaqachon bilasiz. Holatdan UI chizish naqshida son render ichida yangilanadi. Qoida: MutationObserver — siz boshqarmaydigan DOM uchun (begona vidjet, kutubxona, contenteditable, brauzer kengaytmalari) va debugging uchun.

10. Mashqlar

1-mashq (oson): Izoh maydoni o'lchami

<textarea> ni foydalanuvchi burchagidan sudrab kattalashtira oladi. Uning ostida joriy o'lcham ko'rinsin: Izoh maydoni: 300 × 80. ResizeObserver va contentBoxSize[0] ning inlineSize, blockSize qiymatlarini ishlating (yaxlitlab).

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  textarea { width: 300px; height: 80px; font: inherit; }
</style>
<label for="note">Bron uchun izoh:</label><br>
<textarea id="note"></textarea>
<p id="size">Izoh maydoni: …</p>
<script>
  const note = document.getElementById("note");
  const sizeText = document.getElementById("size");

  new ResizeObserver((entries) => {
    const box = entries[0].contentBoxSize[0];
    const w = Math.round(box.inlineSize);
    const h = Math.round(box.blockSize);
    sizeText.textContent = `Izoh maydoni: ${w} × ${h}`;
  }).observe(note);
</script>

Ochilganda Izoh maydoni: 300 × 80 chiqadi: box-sizing sukut bo'yicha content-box, demak CSS'dagi width va height aynan kontent qutisi. Burchakdan sudrang — son har kadrda yangilanadi. <p> da boshidanoq … matni turgani muhim: bo'sh paragrafga matn yozilsa, sahifa balandligi o'zgarardi ("Tuzoq" bo'limi).

2-mashq (o'rta): «Yana» menyusi

«Bahor» turkum tugmalari bir qatorda: Taomlar, Salatlar, Ichimliklar, Shirinliklar, Nonlar. Konteyner tor bo'lsa, sig'maganlari yashirilsin (hidden), oxirida esa Yana (N) yozuvi chiqsin. Konteyner kengligi resize: horizontal bilan o'zgaradi.

Ishora: callback'da avval hamma tugmalarni ko'rsating, keyin chapdan o'ngga kengliklarini (offsetWidth, Element o'lchamlari) yig'ing. Yig'indi konteyner kengligidan oshgan tugmalarni yashiring. Konteynerni emas, uning ichidagi qatorni o'zgartirasiz — konteyner o'lchami o'zgarmaydi, sikl yo'q.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  #bar { resize: horizontal; overflow: hidden; width: 330px;
    min-width: 120px; max-width: 100%; border: 1px solid #ccd;
    padding: 0.5rem; white-space: nowrap; }
  #bar button { font: inherit; margin-right: 4px; }
  #more { color: #555; }
</style>
<div id="bar">
  <button type="button">Taomlar</button>
  <button type="button">Salatlar</button>
  <button type="button">Ichimliklar</button>
  <button type="button">Shirinliklar</button>
  <button type="button">Nonlar</button>
  <span id="more"></span>
</div>
<script>
  const bar = document.getElementById("bar");
  const more = document.getElementById("more");
  const buttons = [...bar.querySelectorAll("button")];
  const MORE_WIDTH = 70; // «Yana (N)» uchun joy

  new ResizeObserver((entries) => {
    const width = entries[0].contentBoxSize[0].inlineSize;
    buttons.forEach((b) => (b.hidden = false));
    let used = 0;
    let hiddenCount = 0;
    for (const button of buttons) {
      used += button.offsetWidth + 4;
      if (used > width - MORE_WIDTH) {
        button.hidden = true;
        hiddenCount++;
      }
    }
    more.textContent = hiddenCount ? `Yana (${hiddenCount})` : "";
    console.log(`Yashirin tugmalar: ${hiddenCount}`);
  }).observe(bar);
</script>

Konsolda:

text
Yashirin tugmalar: 2

Sudrab kengaytiring — yashirin tugmalar kamayadi, toraytiring — ko'payadi. Callback ichida offsetWidth o'qish layout'ni majburlaydi, lekin bu kadrda layout baribir hozirgina hisoblangan va biz faqat tugmalarni yozgandan keyin o'qiymiz — bitta qayta hisob. Haqiqiy loyihada Yana tugmasi yashirin turkumlarni popover menyuda ko'rsatadi (Popover API JS'dan).

3-mashq (qiyin): Element paydo bo'lishini kutish

Begona bron vidjeti sahifaga #bron-oyna elementini qachondir qo'shadi. waitForElement(selector, timeoutMs) funksiyasini yozing: u Promise qaytaradi. Element allaqachon bor bo'lsa — darhol bajariladi. Bo'lmasa, MutationObserver bilan document.body ni (childList, subtree) kuzatadi va element paydo bo'lganda bajariladi. timeoutMs ichida chiqmasa — Error("Topilmadi: " + selector) bilan rad etiladi. Ikkala holatda ham kuzatuvchi to'xtatilsin.

Sinov: vidjetni 300 ms dan keyin setTimeout bilan qo'shing va await waitForElement("#bron-oyna", 2000) natijasining id sini chiqaring.

Yechim
html
<p>Vidjet yuklanmoqda…</p>
<script type="module">
  function waitForElement(selector, timeoutMs) {
    return new Promise((resolve, reject) => {
      const found = document.querySelector(selector);
      if (found) return resolve(found);

      const observer = new MutationObserver(() => {
        const el = document.querySelector(selector);
        if (el) {
          observer.disconnect();
          clearTimeout(timerId);
          resolve(el);
        }
      });
      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });

      const timerId = setTimeout(() => {
        observer.disconnect();
        reject(new Error(`Topilmadi: ${selector}`));
      }, timeoutMs);
    });
  }

  // "begona vidjet" 300 ms dan keyin o'z oynasini qo'shadi
  setTimeout(() => {
    const box = document.createElement("div");
    box.id = "bron-oyna";
    box.textContent = "Stol bron qilish";
    document.body.append(box);
  }, 300);

  const el = await waitForElement("#bron-oyna", 2000);
  console.log(`Topildi: ${el.id}`);
  try {
    await waitForElement("#yoq", 500);
  } catch (error) {
    console.log(error.message);
  }
</script>

Konsolda:

text
Topildi: bron-oyna
Topilmadi: #yoq

Callback yozuvlarni tekshirmaydi — shunchaki querySelector ni qayta chaqiradi: element chuqur ichkariga qo'shilgan bo'lsa ham topiladi. clearTimeout va disconnect ikkala yo'lda ham — "ish tugadi, kuzatuv ham tugadi". Bu funksiya brauzer kengaytmalari va avtomatik testlarda juda ko'p ishlatiladi: test vositalaridagi waitForSelector ("selektor paydo bo'lguncha kut") ham shu g'oyaga asoslangan.

4-mashq: Amaliy tajriba — vazifalar uchun DOM jurnali

vazifalar ilovasida qaysi amal DOM'ni qanchalik o'zgartirishini ko'ring. Kanon kodga tegmasdan, kurs/mashqlar/13/04-dom-jurnal.js faylini yozing — uni Live Server'da ochilgan vazifalar sahifasining DevTools konsoliga nusxalab qo'yasiz. U #royxat ni (childList, subtree, attributes) va #hisob ni (childList) kuzatsin va har callback'da bitta guruhlangan jurnal chiqarsin: nechta yozuv, qancha qo'shildi/o'chirildi, qaysi atributlar o'zgardi. Kuzatuvni to'xtatish uchun window.stopDomLog() funksiyasi bo'lsin.

Yechim
js
const log = new MutationObserver((records) => {
  let added = 0;
  let removed = 0;
  const attrs = new Set();
  for (const r of records) {
    added += r.addedNodes.length;
    removed += r.removedNodes.length;
    if (r.type === "attributes") attrs.add(r.attributeName);
  }
  console.groupCollapsed(`DOM: ${records.length} ta yozuv`);
  console.log(`qo'shildi: ${added}, o'chirildi: ${removed}`);
  console.log("atributlar:", [...attrs].join(", ") || "yo'q");
  console.groupEnd();
});

log.observe(document.getElementById("royxat"), {
  childList: true,
  subtree: true,
  attributes: true,
});
log.observe(document.getElementById("hisob"), { childList: true });

window.stopDomLog = () => log.disconnect();

Bitta kuzatuvchi bir nechta elementni observe qila oladi — yozuvlar bitta callback'ga keladi. Vazifa qo'shib, belgilab, o'chirib ko'ring: har amal nechta yozuv berishini taqqoslang. Butun ro'yxatni qaytadan chizadigan render bitta vazifa o'zgarsa ham ko'p childList yozuvi beradi — Ko'p elementni samarali chizish darsidagi savolning "o'lchovi". #hisob da textContent almashgani characterData emas, childList bo'lib keladi ("Matn tugunlari" bo'limi).

bash
git add 13/04-dom-jurnal.js
git commit -m "13/04: vazifalar uchun MutationObserver DOM jurnali"

11. Real ishda

  • Grafiklar va xaritalar. Chart.js, ECharts, Leaflet konteyner o'lchami o'zgarganda qayta chizish uchun ResizeObserver ishlatadi. React'dagi "o'lchamni bilish" hook'lari ham shu API'ga qurilgan (React — 17-qismda).
  • Vidjetlar va iframe'lar. Joylashtiriladigan vidjetlar (chat, to'lov oynasi) o'z balandligini ResizeObserver bilan o'lchab, ota sahifaga xabar qiladi.
  • Brauzer kengaytmalari va testlar. Reklama blokerlari, tarjimonlar, parol menejerlari sahifa o'zgarishini MutationObserver bilan kuzatadi. Playwright, Puppeteer, Testing Library (brauzerda sahifani avtomatik sinaydigan test vositalari, testlash qismida) dagi "element paydo bo'lguncha kutish" ham shunga o'xshash g'oyaga tayanadi.
  • Matn muharrirlari. contenteditable ga qurilgan muharrirlar foydalanuvchi o'zgarishini mutatsiyalar orqali kuzatadi.
  • Intervyu. "resize hodisasi va ResizeObserver farqi?", "MutationObserver callback'i qachon ishlaydi?", "ResizeObserver loop xatosini qanday tuzatasiz?" — middle darajada uchraydi.

Xulosa

  • ResizeObserver — istalgan elementning o'lchamini kuzatadi; callback layout'dan keyin, paint'dan oldin, boshida bir marta darhol keladi.
  • O'lcham: contentBoxSize[0] va borderBoxSize[0] (inlineSize, blockSize); contentRect — eski shakl.
  • Callback'da kuzatilayotgan elementning o'lchamini o'zgartirish — "loop completed with undelivered notifications" xatosi.
  • MutationObserver — childList, attributes, characterData (+ subtree, attributeFilter, …OldValue); yozuvlar yig'ilib, bitta microtask'da keladi.
  • Ko'rinish moslashuvi — CSS container queries; o'z kodingizdagi o'zgarish — hodisa yoki render; kuzatuvchilar — JS bilishi kerak bo'lgan o'lcham va begona DOM uchun.
  • Ish tugagach — disconnect().

Keyingi dars: matchMedia va foydalanuvchi sozlamalari — media query'larni JavaScript'dan tekshirish, qorong'i tema va "harakatni kamaytirish" sozlamasiga moslashish.

Manbalar

  • W3C: "Resize Observer" — drafts.csswg.org/resize-observer
  • WHATWG DOM Standard: "Mutation observers" — dom.spec.whatwg.org
  • MDN: ResizeObserver, ResizeObserverEntry, MutationObserver, MutationRecord — developer.mozilla.org
  • Chrome for Developers: "Mutation events will be removed from Chrome" (2024) — developer.chrome.com
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript ResizeObserver va MutationObserver: o'lcham va DOM o'zgarishini kuzatish — IlmHamroh