ResizeObserver bir elemanın boyutu değiştiğinde, MutationObserver ise DOM ağacında bir değişiklik olduğunda (çocuk eklenip çıkarılması, öznitelik veya metin değişimi) sizi haberdar eder. İkisi de bir geri çağırmayla oluşturulur, observe() ile hedef alır, disconnect() ile durdurulur. Pencere resize olayı yalnızca pencere boyutunu bildirir; ResizeObserver ise tek bir kutunun, pencereyle hiç ilgisi olmayan sebeplerle değişen boyutunu da yakalar.

Bu yazıda önce ResizeObserver'ın hangi kutuyu ölçtüğünü, contentBoxSize ve borderBoxSize dizilerini ve meşhur "ResizeObserver loop completed with undelivered notifications" hatasının nedenini, ardından MutationObserver seçeneklerini, MutationRecord alanlarını ve takeRecords() kullanımını göreceksiniz. Görünürlük takibi bu yazının konusu değil; o iş için Intersection Observer yazısına bakın.

ResizeObserver ile eleman boyutunu izlemek

ResizeObserver ile eleman boyutunu izlemek: Hangi kutu ölçülüyor: box seçeneği, contentBoxSize neden dizi?, Gözlemi açıp kapatmak, Mini örnek: canvas çözünürlüğünü kaba uydurmak

new ResizeObserver(callback) tek parametre alır. Geri çağırma bir ResizeObserverEntry dizisi ve gözlemcinin kendisiyle çağrılır. MDN, ikinci parametrenin örneğin belirli bir koşul sağlandığında gözlemi bırakmak için kullanılabileceğini, ihtiyacınız yoksa yazmayabileceğinizi belirtir. Arayüzün kendine ait bir özelliği yoktur; üç yöntemi vardır: observe(), unobserve() ve disconnect().

javascript
const kart = document.querySelector(".kart");

const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const boyut = entry.contentBoxSize[0];
    // genişliğe göre düzen sınıfı değiştir
    entry.target.classList.toggle("dar", boyut.inlineSize < 400);
  }
});

ro.observe(kart);

Bu örnek kart 400 pikselden darsa dar sınıfını ekler. MDN'nin kendi örneği de benzer şekilde, bir kutunun genişliğine göre başlık ve paragrafın yazı boyutunu değiştirir ve bunun görüş alanıyla ilgisi olmayan boyut değişimlerinde de çalıştığını vurgular. classList.toggle() ayrıntısı classList yazısında anlatılıyor.

Hangi kutu ölçülüyor: box seçeneği

observe(hedef, { box }) ile hangi kutu modelinin izleneceğini seçersiniz. MDN üç değer sayar:

box değeriÖlçülen
content-box (varsayılan)CSS içerik alanı: kenarlık kutusundan dolgu ve kenarlık çıkarılmış hali
border-boxİçerik, dolgu ve kenarlığı kapsayan kenarlık kutusu
device-pixel-content-boxİçerik alanı, cihaz pikseli cinsinden ve CSS dönüşümleri uygulanmadan önce

device-pixel-content-box özellikle canvas için kullanışlıdır. MDN'nin örneğinde canvas'ın width ve height değerleri devicePixelContentBoxSize[0] içindeki boyutlarla ayarlanır; böylece canvas pikselleri fiziksel piksellerle bire bir eşleşir. MDN, geri çağırmanın yerleşimden sonra ve boyamadan önce çalıştığını, bunun da fiziksel piksel boyutunu boyamadan önce öğrenme fırsatı verdiğini belirtir. Canvas boyutlandırmanın neden önemli olduğu Canvas çizim rehberinde anlatılıyor.

contentBoxSize neden dizi?

contentBoxSize, borderBoxSize ve devicePixelContentBoxSize tek bir nesne değil, nesne dizisidir. MDN bunun parçalara bölünen elemanları desteklemek için olduğunu söyler; çok sütunlu düzende bir eleman birden fazla parçaya bölünebilir. Her dizi öğesi bir ResizeObserverSize nesnesidir ve iki alan taşır: inlineSize ve blockSize. Yatay yazım yönünde inlineSize genişliğe, blockSize yüksekliğe karşılık gelir; dikey yazım yönünde tersidir. Çok sütunlu düzende dönen boyut ilk sütunun boyutudur.

Eski bir alan olan contentRect da hâlâ vardır ve bir DOMRectReadOnly döndürür. MDN bunu yalnızca geriye dönük uyumluluk için tutulan eski (legacy) bir özellik olarak tanımlar ve ileride kullanımdan kaldırılabileceğini belirtir. MDN örnekleri bu yüzden önce contentBoxSize var mı diye bakar, yoksa contentRect kullanır. Yeni kodda boyut dizilerini tercih edin.

Gözlemi açıp kapatmak

MDN örneğinde bir onay kutusu gözlemi açıp kapatır: kutu işaretliyken observe(), değilken unobserve() çağrılır. Gözlem kapalıyken kap genişlese de daralsa da yazı boyutu değişmez. unobserve() tek hedefi, disconnect() ise gözlemcinin izlediği bütün hedefleri bırakır; ikisi de değer döndürmez ve MDN'ye göre istisna fırlatmaz. Bir bileşen sayfadan kaldırılırken ona bağlı gözlemciyi de durdurmak iyi bir alışkanlıktır; böylece artık kullanılmayan bir elemana ait geri çağırma çalışmaya devam etmez.

javascript
checkbox.addEventListener("change", () => {
  if (checkbox.checked) {
    resizeObserver.observe(divElem);
  } else {
    resizeObserver.unobserve(divElem);
  }
});

Mini örnek: canvas çözünürlüğünü kaba uydurmak

Duyarlı bir sayfada canvas'ın CSS boyutu kabıyla birlikte değişir, ama çizim yüzeyinin piksel sayısı width ve height öznitelikleriyle belirlenir. İkisi uyuşmazsa çizim ya bulanık ya da çarpık görünür. MDN'nin devicePixelContentBoxSize örneği bu sorunu ResizeObserver ile çözer: canvas'ın içerik kutusu cihaz pikseli cinsinden ölçülür ve çizim yüzeyi bu boyuta ayarlanır.

javascript
const canvas = document.querySelector("canvas");

const observer = new ResizeObserver((entries) => {
  const entry = entries.find((e) => e.target === canvas);
  canvas.width = entry.devicePixelContentBoxSize[0].inlineSize;
  canvas.height = entry.devicePixelContentBoxSize[0].blockSize;
  ciz(); // boyut atandığı için bağlam sıfırlandı, yeniden çiz
});

observer.observe(canvas, { box: "device-pixel-content-box" });

Burada iki ayrıntıya dikkat edin. Birincisi, canvas.width ataması bağlamı sıfırlar ve içeriği siler; bu yüzden her boyut değişiminden sonra çizimi yeniden yapmanız gerekir. İkincisi, canvas'ın ekrandaki CSS boyutu genellikle kabından gelir; çizim yüzeyinin piksel sayısını değiştirmek CSS boyutunu değiştirmez. Geri çağırma içinde canvas'ın CSS genişliğini siz değiştirirseniz ise aşağıda anlatılan döngü sorunu ortaya çıkabilir. Canvas boyutlandırma kuralları Canvas çizim rehberinde ayrıntılı anlatılıyor.

ResizeObserver döngü hatası

Tarayıcı boyut bildirimlerini boyamadan önce gönderir. Geri çağırmanız elemanın boyutunu yine değiştirirse stil ve yerleşim yeniden hesaplanır ve bu yeni boyut bildirimleri doğurabilir. Sonsuz döngüyü önlemek için her turda yalnızca DOM'da daha derinde olan elemanlar işlenir; koşulu sağlamayan bildirimler bir sonraki boyamaya ertelenir ve Window nesnesinde şu iletiyle bir error olayı tetiklenir: ResizeObserver loop completed with undelivered notifications.

MDN önemli bir noktanın altını çizer: bu mekanizma yalnızca tarayıcının kilitlenmesini önler, döngünün kendisini engellemez. Aşağıdaki kod her bildirimde genişliği 10 piksel artırdığı için eleman sonsuza kadar büyür ve konsolda aynı ileti her karede tekrar eder:

javascript
const divElem = document.querySelector("body > div");

const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    entry.target.style.width = `${entry.contentBoxSize[0].inlineSize + 10}px`;
  }
});

resizeObserver.observe(divElem);

Çözüm amacınıza bağlıdır. Döngüyü gerçekten istiyorsanız (örneğin sürekli büyüyen bir efekt), boyut değiştiren kodu requestAnimationFrame geri çağırmasına taşıyarak boyamadan sonraya erteleyin. Döngü istemiyorsanız geri çağırmanızın kendi kendini tetiklemesini engelleyin; MDN bunun bir yolunu "beklenen boyutu" bir WeakMap içinde saklamak olarak gösterir:

javascript
const expectedSizes = new WeakMap();

const resizeObserver = new ResizeObserver((entries) => {
  requestAnimationFrame(() => {
    for (const entry of entries) {
      const expectedSize = expectedSizes.get(entry.target);
      if (entry.contentBoxSize[0].inlineSize === expectedSize) {
        continue; // bu değişikliği zaten biz yaptık
      }
      const newSize = entry.contentBoxSize[0].inlineSize + 10;
      entry.target.style.width = `${newSize}px`;
      expectedSizes.set(entry.target, newSize);
    }
  });
});

Hata olayı sonsuza kadar tetiklenmediği sürece gözlemci sonunda dengeye oturur ve büyük olasılıkla doğru bir yerleşim üretir. Ancak MDN'nin uyardığı gibi kullanıcı, tek karede olması gereken değişiklikler birkaç kareye yayıldığı için kısa bir bozuk yerleşim görebilir. requestAnimationFrame ayrıntıları animasyon döngüsü yazısında.

MutationObserver ile DOM değişikliklerini izlemek

MutationObserver ile DOM değişikliklerini izlemek: observe() seçenekleri, MutationRecord alanları, oldValue ne zaman dolu gelir?, Belirli öznitelikleri ve eski değeri izlemek

MutationObserver, eski DOM3 Mutation Events özelliğinin yerine tasarlanmıştır. new MutationObserver(callback) gözlemciyi oluşturur ama izleme hemen başlamaz; MDN'ye göre önce observe() çağrılarak hangi DOM bölümünün ve hangi tür değişikliklerin izleneceği belirtilmelidir. Geri çağırma bir MutationRecord dizisi ve gözlemciyi alır; ikinci parametre çoğu zaman gözlemciyi disconnect() ile durdurmak için kullanılır.

javascript
const hedef = document.getElementById("some-id");

const config = { attributes: true, childList: true, subtree: true };

const observer = new MutationObserver((mutationList, observer) => {
  for (const mutation of mutationList) {
    if (mutation.type === "childList") {
      console.log("Bir çocuk düğüm eklendi veya çıkarıldı.");
    } else if (mutation.type === "attributes") {
      console.log(`${mutation.attributeName} özniteliği değişti.`);
    }
  }
});

observer.observe(hedef, config);
// daha sonra: observer.disconnect();

observe() seçenekleri

SeçenekVarsayılanAçıklama
childListfalseHedefe çocuk düğüm eklenmesini veya çıkarılmasını izler
attributesfalse (attributeFilter veya attributeOldValue varsa true)Öznitelik değerlerindeki değişimi izler
characterDatafalse (characterDataOldValue varsa true)Metin düğümlerinin içeriğindeki değişimi izler
subtreefalseDiğer seçenekleri hedefin bütün alt ağacına genişletir
attributeFilteryokYalnızca bu dizideki öznitelik adlarını izler
attributeOldValuefalseDeğişen özniteliğin eski değerini kaydeder
characterDataOldValuefalseDeğişen metnin eski değerini kaydeder

childList, attributes ve characterData seçeneklerinden en az biri true olmalıdır. MDN üç durumda TypeError fırlatıldığını söyler: hiçbir şey izlenmeyecek şekilde ayar verilirse; attributes açıkça false iken attributeOldValue true veya attributeFilter verilmişse; characterDataOldValue true iken characterData false ise. Aynı gözlemci üzerinde observe() birden çok kez çağrılarak farklı DOM bölümleri izlenebilir, aynı düğüm de birden fazla gözlemci tarafından izlenebilir.

MutationRecord alanları

Alanİçerik
typeattributes, characterData veya childList
targetattributes için özniteliği değişen eleman, characterData için metin düğümü, childList için çocukları değişen düğüm
addedNodes / removedNodesEklenen veya çıkarılan düğümler; yoksa boş NodeList
previousSibling / nextSiblingEklenen veya çıkarılan düğümlerin önceki ve sonraki kardeşi ya da null
attributeNameDeğişen özniteliğin adı ya da null
oldValueattributes ve characterData için önceki değer (ilgili OldValue seçeneği açıksa), childList için null

addedNodes ve removedNodes birer NodeList olduğu için doğrudan for...of ile gezilebilir. NodeList ile HTMLCollection farkı DOM ağacında gezinme yazısında anlatılıyor.

Kaydın türünü görmek için MDN'nin type sayfasındaki örnek iyi bir başlangıçtır: aynı gözlemci childList, attributes ve subtree seçenekleriyle bir kabı izler. Kaba yeni bir paragraf eklendiğinde kaydın türü childList, kabın class özniteliği red ile blue arasında değiştirildiğinde ise attributes olur. Tek geri çağırma içinde switch (mutation.type) ile türlere göre ayrı işlem yapmak bu yüzden yaygın bir kalıptır; MDN'nin sohbet odası örneği de önce türe, sonra attributeName değerine göre dallanır.

oldValue ne zaman dolu gelir?

oldValue alanı her kayıtta dolu gelmez. MDN'ye göre bir öznitelik değişiminde eski değeri görmek için observe() çağrısında attributeOldValue true olmalı, attributes true olmalı ya da hiç yazılmamalı ve kaydın türü attributes olmalıdır. Metin düğümlerinde aynı kural characterDataOldValue ve characterData için geçerlidir. Bu koşullar sağlanmazsa ya da kayıt childList türündeyse oldValue değeri null olur. Yeni değeri ise kayıtta aramazsınız; mutation.target üzerinden o anki özniteliği okursunuz. MDN'nin renk örneği bir başlığın style özniteliğini izler ve her değişimde eski stil metnini ekrana yazar.

Belirli öznitelikleri ve eski değeri izlemek

MDN'nin sohbet odası örneği attributeFilter ile yalnızca status ve username özniteliklerini izler, attributeOldValue ile de eski kullanıcı adını yakalar. attributeFilter verildiği için attributes seçeneği yazılmasa da varsayılan olarak true kabul edilir:

javascript
const observer = new MutationObserver((mutationList) => {
  for (const mutation of mutationList) {
    if (mutation.type !== "attributes") continue;
    if (mutation.attributeName === "username") {
      console.log(`Eski kullanıcı adı: ${mutation.oldValue}`);
    }
  }
});

observer.observe(document.querySelector("#user-list"), {
  attributeFilter: ["status", "username"],
  attributeOldValue: true,
  subtree: true,
});

subtree ile çıkarılan alt ağaçlar

subtree: true ile izlenen bir alt ağaçtan bir parça çıkarılırsa, çıkarma bildirimi teslim edilene kadar o parçadaki değişiklikler de raporlanmaya devam eder; bildirim teslim edildikten sonra ayrılmış parçadaki değişiklikler artık gözlemciyi tetiklemez. MDN bu davranışın bağlantının koptuğu an ile sizin yeni izleme başlatabileceğiniz an arasındaki değişiklikleri kaçırmamak için olduğunu açıklar ve şu çıktıyı gösterir:

javascript
const target = document.getElementById("target");
const child = document.getElementById("child");

const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    console.log(mutation.type, mutation.target.id, mutation.attributeName);
    if (mutation.type === "childList" && mutation.target.id === "target") {
      child.setAttribute("data-bar", "");
    }
  });
});

observer.observe(target, { attributes: true, childList: true, subtree: true });

target.removeChild(child);
child.setAttribute("data-foo", "");
// Çıktı (MDN):
// childList target null
// attributes child data-foo
// "attributes child data-bar" bildirimi gelmez.
html
<div id="target">
  <div id="child"></div>
</div>

disconnect ve takeRecords

disconnect() gözlemciyi durdurur; aynı gözlemci daha sonra observe() ile yeniden kullanılabilir. Önemli ayrıntı: MDN'ye göre algılanmış ama henüz geri çağırmaya teslim edilmemiş bütün bildirimler disconnect() ile atılır. Bunları kaybetmek istemiyorsanız önce takeRecords() çağırın; bu yöntem bekleyen kayıtları dizi olarak döndürür ve kuyruğu boşaltır:

javascript
const bekleyenler = observer.takeRecords();
observer.disconnect();
if (bekleyenler.length > 0) {
  callback(bekleyenler); // aynı işleyiciyle son kayıtları da işle
}

İzlenen eleman DOM'dan çıkarılıp çöp toplayıcı tarafından serbest bırakılırsa gözlemci o elemanı izlemeyi bırakır, ama gözlemcinin kendisi diğer elemanları izlemek için var olmaya devam edebilir. Yani tek bir elemanın kaldırılması, gözlemcinin başka yerlerde kurduğu izlemeleri etkilemez; hepsini bırakmak için yine disconnect() gerekir.

Mini örnek: liste boşalınca gözlemi bırakmak

MDN'nin yapıcı sayfasındaki örnek, iki gözlemci yönteminin birlikte nasıl kullanıldığını iyi gösterir. Bir listeye düğmeyle öğe eklenir ve çıkarılır; gözlemci her eklemeyi ve çıkarmayı bir günlüğe yazar, liste tamamen boşaldığında da kendini durdurur. Aşağıdaki sürüm aynı mantığı Türkçe iletilerle kurar:

html
<button id="ekle" type="button">Öğe ekle</button>
<button id="sil" type="button">Öğe sil</button>
<ul id="kap"></ul>
<pre id="gunluk"></pre>
javascript
const kap = document.querySelector("#kap");
const gunluk = document.querySelector("#gunluk");
let sayac = 0;

document.querySelector("#ekle").addEventListener("click", () => {
  const li = document.createElement("li");
  li.textContent = `öğe ${sayac++}`;
  kap.appendChild(li);
});

document.querySelector("#sil").addEventListener("click", () => {
  kap.querySelector("li")?.remove();
});

const izleyici = new MutationObserver((kayitlar, observer) => {
  for (const kayit of kayitlar) {
    for (const dugum of kayit.addedNodes) {
      gunluk.textContent = `Eklendi: ${dugum.textContent}\n${gunluk.textContent}`;
    }
    for (const dugum of kayit.removedNodes) {
      gunluk.textContent = `Çıkarıldı: ${dugum.textContent}\n${gunluk.textContent}`;
    }
    if (kayit.target.childNodes.length === 0) {
      gunluk.textContent = `Gözlem durduruldu\n${gunluk.textContent}`;
      observer.disconnect();
    }
  }
});

izleyici.observe(kap, { childList: true, subtree: true });

Dikkat edilecek iki nokta var. Birincisi, çıkarılan düğüm artık belgede olmasa bile removedNodes içinde gelir ve textContent değeri okunabilir. İkincisi, geri çağırmanın ikinci parametresi olan gözlemci, dış değişkene ihtiyaç duymadan disconnect() çağırmayı sağlar. Bir öğe ekleyip silmek tek bir kayıtta değil, iki ayrı childList kaydında raporlanır; hangisinin ne olduğunu addedNodes ve removedNodes uzunluklarından anlarsınız. Gözlem durduktan sonra yeni öğe eklemek günlüğe hiçbir şey yazmaz; yeniden izlemek için observe() çağrısını tekrarlamanız gerekir.

Üç gözlemcinin karşılaştırması

Üç gözlemcinin karşılaştırması: IntersectionObserver, ResizeObserver, MutationObserver
GözlemciCevapladığı soruGirdi türüAyar nerede
IntersectionObserverEleman kök alanında ne kadar görünüyor?IntersectionObserverEntryYapıcıda (sonradan değişmez)
ResizeObserverElemanın kutusu hangi boyuta geldi?ResizeObserverEntryobserve() içinde box
MutationObserverDOM ağacında ne değişti?MutationRecordobserve() içinde seçenekler

Sık yapılan hatalar

  • contentBoxSize değerini nesne sanmak. Dizi olduğu için contentBoxSize[0].inlineSize şeklinde okunur.
  • Geri çağırmada izlenen elemanın boyutunu koşulsuz değiştirmek. Döngü hatasına yol açar; beklenen boyutu saklayın veya değişikliği requestAnimationFrame ile erteleyin.
  • MutationObserver'a boş ayar vermek. En az bir izleme türü açık olmalı, yoksa TypeError alırsınız.
  • Gözlemcinin kendi yaptığı değişikliği yeniden işlemek. MutationObserver geri çağırmasında izlenen ağacı değiştirirseniz bu da yeni kayıt üretir; bir bayrak veya kontrol ile kendi değişikliğinizi ayırın.
  • disconnect() öncesi bekleyen kayıtları unutmak. Gerekirse önce takeRecords() çağırın.
  • Her şey için subtree: true ve tüm öznitelikleri açmak. Gereksiz kayıt üretir; attributeFilter ile daraltın.

Sık Sorulan Sorular

ResizeObserver ile window resize olayı arasındaki fark nedir?

resize olayı pencerenin boyutu değiştiğinde çalışır. ResizeObserver ise tek tek elemanların içerik veya kenarlık kutusunu izler; MDN'nin örneğinde bir kaydırıcıyla kutunun genişliği değiştirildiğinde pencere aynı kalsa bile geri çağırma çalışır.

MutationObserver geri çağırması ne zaman çalışır?

Kayıtlar bir kuyrukta toplanır ve geri çağırmaya dizi halinde teslim edilir; bu yüzden aynı anda yapılan birçok değişikliği tek çağrıda görürsünüz. Tarayıcıda işlerin hangi sırayla çalıştığına dair genel resim için olay döngüsü yazısına bakın.

SVG elemanları izlenebilir mi?

Evet. ResizeObserver bir Element'in içerik veya kenarlık kutusunu ya da bir SVGElement'in sınır kutusunu izler; SVG hedefinde contentRect SVG'nin sınır kutusunu verir.

Kendi bileşenimdeki öznitelik değişikliğini başka yolla izleyebilir miyim?

Özel elemanlarda observedAttributes ve attributeChangedCallback bu iş için tasarlanmıştır; konu Web Components yazısında anlatılıyor. Başkasının oluşturduğu DOM'u izlemek gerektiğinde MutationObserver uygundur.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Web API öğrenme rehberi · Intersection Observer · requestAnimationFrame · DOM ağacında gezinme · DOM eleman oluşturma · Web Components