İçeriğe geç
academia.sh

Ders 15 / 24

Gölge DOM

Bir ögenin içine açılan ayrı belge ağacı; seçicinin durduğu sınır, biçemin iki yönlü yalıtımı, kalıtımın ve özel özelliklerin sınırdan geçişi, açık ile kapalı kip ve olay hedefinin çevrilmesi.

İçindekiler

Önceki ders ögeye bir ad ve bir yaşam döngüsü verdi; içini açık bıraktı. Ölçüm rozetinin işaretlemesi hâlâ belgenin ortak ağacındadır. Sayfanın herhangi bir yerinde yazılmış .deger { font-weight: 700 } kuralı rozetin içindeki değeri de yakalar; rozetin kendi kuralı sayfadaki başka bir .deger ögesine sızar; belgeye bağlanmış bir seçici rozetin iç düğümlerini bulur ve onlara dokunur.

Yeniden kullanılabilir bir parçanın gereksinimi bunun tersidir. Bu ders, belge ağacının içinde ayrı bir ağaç açan ve iki yönlü yalıtımı kuran mekanizmayı kurar.

Gölge Kökü, Konak ve Işık Ağacı

Bir öge, kendisine bağlı ikinci bir ağaç edinebilir. Bu ağacın köküne gölge kökü (shadow root), ağacın kendisine gölge ağacı (shadow tree), ağacı taşıyan ögeye konak (host) denir.

Gölge ağacı belge ağacının bir parçası değildir; ayrı bir ağaçtır ve kendi kökü vardır. Konak, iki ağacın birleştiği tek noktadır: belge ağacında bir öge, gölge ağacında bir kök sahibi.

Konağın belgede yazılı kendi çocukları kaybolmaz; oldukları yerde dururlar. Bunlara ışık ağacı (light DOM) denir. Gölge ağacı takılı bir ögede çizilen şey ışık ağacı değil gölge ağacıdır; ışık ağacının içeriğinin nasıl geri kazanıldığı sonraki dersin konusudur.

Bir ögeye gölge ağacı takmak her etiket için mümkün değildir. Kendi iç yapısı olan ögeler — form denetimleri, görsel, giriş alanı — buna kapalıdır; bunların zaten tarayıcı tarafından yönetilen bir iç yapısı vardır.

Seçicinin Durduğu Sınır

Yalıtımın ilk yüzü seçicidir. Belge ağacında yapılan bir sorgu gölge ağacına inmez; gölge ağacında yapılan bir sorgu dışarı çıkmaz.

// golge-sinir.mjs — secici sinirda durur, kalitim sinirdan gecer
const oge = (ad, ozn = {}, ...cocuklar) => {
  const d = { ad, ozn, cocuklar, ebeveyn: null, golge: null };
  for (const c of cocuklar) c.ebeveyn = d;
  return d;
};

// Golge kokunu konaga takar.
function golgeTak(konak, ...cocuklar) {
  const kok = { ad: "#golge-kok", ozn: {}, cocuklar, ebeveyn: null, konak, golge: null };
  for (const c of cocuklar) c.ebeveyn = kok;
  konak.golge = kok;
  return kok;
}

// Bir agacta dolasir; karsilastigi golge agaclarinin icine GIRMEZ.
function* agacta(kok) {
  yield kok;
  for (const c of kok.cocuklar) yield* agacta(c);
}

const sinifVar = (d, s) => (d.ozn.class ?? "").split(/\s+/).includes(s);
const yaz = (d) => `${d.ad}#${d.ozn.id ?? "-"}`;
const sorgu = (kok, sinif) => [...agacta(kok)].filter((d) => sinifVar(d, sinif)).map(yaz);

// Istasyon sayfasi: iki rozet; ilkinin golge agaci var, ikincisi duz isaretleme.
const rozetA = oge("olcum-rozeti", { class: "rozet", id: "sicaklik" });
golgeTak(rozetA,
  oge("div", { class: "kutu", id: "g-kutu" },
    oge("span", { class: "deger", id: "g-deger" }),
    oge("span", { class: "birim", id: "g-birim" })));

const rozetB = oge("p", { class: "rozet", id: "nem" },
  oge("span", { class: "deger", id: "l-deger" }));

const belge = oge("#belge", { id: "belge" }, oge("section", { id: "pano" }, rozetA, rozetB));

console.log("belge agacindan .rozet :", sorgu(belge, "rozet"));
console.log("belge agacindan .deger :", sorgu(belge, "deger"));
console.log("golge agacindan .deger :", sorgu(rozetA.golge, "deger"));
console.log("golge agacindan .rozet :", sorgu(rozetA.golge, "rozet"));

// Kalitilan ozellikler sinirdan gecer; secici tabanli kurallar gecmez.
const KALITILAN = new Set(["color", "font-size", "line-height"]);
const belgeKurali = { sinif: "deger", bildirim: { "font-weight": "700" } };
const konaktanGelen = { color: "#b23", "font-weight": "400" };

function hesapla(dugum) {
  const deger = Object.fromEntries(
    Object.entries(konaktanGelen).filter(([ad]) => KALITILAN.has(ad)));
  const belgeAgacinda = [...agacta(belge)].includes(dugum);
  if (belgeAgacinda && sinifVar(dugum, belgeKurali.sinif)) Object.assign(deger, belgeKurali.bildirim);
  return deger;
}

const golgeDeger = [...agacta(rozetA.golge)].find((d) => d.ozn.id === "g-deger");
const belgeDeger = [...agacta(belge)].find((d) => d.ozn.id === "l-deger");

console.log("hesaplanan span#l-deger:", hesapla(belgeDeger));
console.log("hesaplanan span#g-deger:", hesapla(golgeDeger));
belge agacindan .rozet : [ 'olcum-rozeti#sicaklik', 'p#nem' ]
belge agacindan .deger : [ 'span#l-deger' ]
golge agacindan .deger : [ 'span#g-deger' ]
golge agacindan .rozet : []
hesaplanan span#l-deger: { color: '#b23', 'font-weight': '700' }
hesaplanan span#g-deger: { color: '#b23' }

İlk dört satır sınırın simetrik olduğunu gösterir. Belgede .deger arayan sorgu iki eşleşmeden yalnızca birini bulur; gölge ağacındaki span#g-deger görünmez. Ters yönde de aynısı geçerlidir: gölge ağacında .rozet arayan sorgu, konağın kendisi o sınıfa sahip olduğu hâlde boş döner — konak gölge ağacının dışındadır.

Bunun pratik iki sonucu vardır. Birincisi, gölge ağacındaki kimlikler belge kimlikleriyle çakışmaz; her gölge ağacı kendi kimlik uzayına sahiptir. Aynı bileşeni sayfada on kez kullanmak on tane aynı kimliği üretse bile çakışma doğmaz. İkincisi, bileşenin içine dokunmak isteyen dış kod bunu ancak konaktan gölge köküne geçerek yapabilir — yani kasıtlı ve görünür bir adımla.

Biçemin İki Yönlü Yalıtımı

Yalıtımın ikinci yüzü biçemdir ve seçiciyle aynı kuraldan doğar. Basamaklama ve Özgüllük dersinde tanımlanan eşleme, bir kuralın kendi ağacındaki ögelere uygulanmasıdır. Belgeye yazılmış bir kuralın seçicisi gölge ağacındaki ögelere eşleşmez; gölge ağacına yazılmış bir kural belge ağacına eşleşmez.

Çıktının son iki satırı bu ayrımı gösterir. Belge ağacındaki span#l-deger ögesine belge kuralındaki font-weight: 700 uygulanmış, gölge ağacındaki span#g-deger ögesine uygulanmamıştır.

Sınırın biçem tarafında iki tanımlı geçiş vardır ve her ikisi de kasıtlıdır.

Birincisi kalıtımdır. Kalıtılan özellikler ağaç sınırını tanımaz: konağa uygulanan yazı rengi, satır yüksekliği, yazı boyutu gölge ağacına devreder. Çıktıda her iki ögenin de color değerini konaktan almış olması budur. Bu yüzden gölge ağacı biçemi tam bir sıfırlama değildir; bileşen dışarıdan gelen tipografiye uyum sağlar ve bu genellikle istenen davranıştır.

İkincisi özel özelliklerdir. Kalıtılan bir değer olarak tanımlandıkları için sınırdan geçerler ve bileşene dışarıdan ayarlanabilir bir yüzey kazandırırlar. Bileşen, kendi kurallarında bir özel özelliği yedekli değeriyle okur; sayfa o özelliği konakta tanımlayarak bileşenin görünümünü, iç yapısına hiç dokunmadan değiştirir. Özel Özellikler dersinde kurulan bu mekanizma, gölge ağacı ile birlikte bir bileşenin biçem sözleşmesi hâline gelir.

Gölge ağacının içinden yazılan kurallar için iki seçici tanımlıdır. Biri konağın kendisini hedefler ve bileşenin dış kutusunu biçemler; koşullu biçimi, konağın belgedeki bağlamına ya da özniteliğine göre farklı biçem vermeyi sağlar. Diğeri, bileşenin dışarıya açtığı iç parçaları hedefler: bileşen bir iç ögeyi ad vererek işaretler, dışarıdaki biçem o adı kullanarak yalnızca işaretlenmiş parçalara erişir. Bu, kapsüllemeyi delmek değil, kapsüllemenin içinde tanımlı bir pencere açmaktır — bileşen hangi parçalarının biçemlenmeye açık olduğuna kendisi karar verir.

Açık ve Kapalı Kip

Gölge ağacı takılırken iki kipten biri seçilir. Açık kipte konağın gölge kökü program tarafından okunabilir; dışarıdaki kod bileşenin içine erişebilir. Kapalı kipte konak üzerinden gölge köküne erişilemez; başvuru yalnızca bileşenin kendi kodunda tutulur.

Kapalı kip bir güvenlik sınırı değildir. Bileşenin kodu sayfayla aynı ortamda çalışır; kararlı bir kod gölge köküne başka yollardan ulaşabilir. Kapalı kip bir niyet bildirimidir: bileşenin iç yapısı arayüzünün parçası değildir, ona dayanan kod kırılacaktır.

Bedeli de vardır. Kapalı bir ağaç, sayfanın erişilebilirlik denetimlerini, test araçlarını ve hata ayıklamayı zorlaştırır. Yaygın seçim açık kiptir; kapsülleme sözleşmeyle korunur, erişim engeliyle değil.

Olayların Sınırı Aşması

Olay Modeli dersinde yayılım yolu kökten hedefe uzanan ata zinciri olarak tanımlanmıştı. Gölge ağacı bu zincire bir ek getirir: yol, gölge kökünde durmaz, konağa atlar ve belgeye kadar sürer. Buna birleşik yol (composed path) denir.

Yol dışarı taştığında bir sorun doğar. Belgeye bağlanmış bir dinleyici, olayın hedefi olarak gölge ağacının içindeki bir düğümü görürse kapsülleme delinmiş olur. Bu yüzden tarayıcı hedefi çevirir (retargeting): her dinleyici, olayın hedefi olarak kendi ağacındaki en yakın karşılığını görür.

// hedef-cevirme.mjs — olay golge sinirini asarken hedef nasil degisir
const oge = (ad, ...cocuklar) => {
  const d = { ad, cocuklar, ebeveyn: null, konak: null, golge: null };
  for (const c of cocuklar) c.ebeveyn = d;
  return d;
};

function golgeTak(konak, ...cocuklar) {
  const kok = { ad: "#golge-kok", cocuklar, ebeveyn: null, konak, golge: null };
  for (const c of cocuklar) c.ebeveyn = kok;
  konak.golge = kok;
  return kok;
}

// Bir dugumun ait oldugu agacin koku: ya belge ya bir golge kok.
const agacKoku = (d) => { let a = d; while (a.ebeveyn) a = a.ebeveyn; return a; };

// Birlesik yol: hedeften baslar, agac koklerinde konaga atlayarak belgeye kadar cikar.
function birlesikYol(hedef) {
  const yol = [];
  for (let d = hedef; d; d = d.ebeveyn ?? d.konak) yol.push(d);
  return yol;
}

// Bir dinleyici icin hedef: birlesik yolda dinleyiciyle AYNI agacta olan ilk dugum.
function cevrilmisHedef(hedef, dinleyici) {
  const dinleyiciAgaci = agacKoku(dinleyici);
  return birlesikYol(hedef).find((d) => agacKoku(d) === dinleyiciAgaci);
}

const dugme = oge("button.kaydet");
const kutu = oge("div.kutu", dugme);
const rozet = oge("olcum-rozeti");
const golgeKok = golgeTak(rozet, kutu);
const pano = oge("section#pano", rozet);
const belge = oge("#belge", pano);

console.log("birlesik yol:", birlesikYol(dugme).map((d) => d.ad).join(" → "));
console.log();
console.log("dinleyicinin dugumu   hedef olarak gordugu");
for (const d of [dugme, kutu, golgeKok, rozet, pano, belge])
  console.log(d.ad.padEnd(22), cevrilmisHedef(dugme, d).ad);

// composed: false olan olay golge kokunden yukari cikmaz.
const yayilanlar = (hedef, birlesik) => {
  const yol = birlesikYol(hedef);
  if (birlesik) return yol;
  const sinir = yol.findIndex((d) => d.ad === "#golge-kok");
  return sinir === -1 ? yol : yol.slice(0, sinir + 1);
};

console.log();
console.log("composed: true  →", yayilanlar(dugme, true).map((d) => d.ad).join(" → "));
console.log("composed: false →", yayilanlar(dugme, false).map((d) => d.ad).join(" → "));
birlesik yol: button.kaydet → div.kutu → #golge-kok → olcum-rozeti → section#pano → #belge

dinleyicinin dugumu   hedef olarak gordugu
button.kaydet          button.kaydet
div.kutu               button.kaydet
#golge-kok             button.kaydet
olcum-rozeti           olcum-rozeti
section#pano           olcum-rozeti
#belge                 olcum-rozeti
composed: true  → button.kaydet → div.kutu → #golge-kok → olcum-rozeti → section#pano → #belge
composed: false → button.kaydet → div.kutu → #golge-kok

Gölge ağacının içindeki üç dinleyici gerçek hedefi, dışındaki üç dinleyici konağı görür. Belgeye bağlanmış bir olay devri kodu, bileşenin içinde hangi düğmeye tıklandığını öğrenemez; yalnızca “ölçüm rozetine tıklandı” bilgisini alır. Bileşenin dışarıya ne bildireceğine kendisi karar verir ve bunu genellikle kendi tanımladığı bir olay türüyle yapar.

Son iki satır ikinci ayrımı gösterir. Bir olayın gölge sınırını aşıp aşmayacağı olay nesnesinin bir alanıyla belirlenir. Sınırı aşmayan bir olay gölge kökünde durur ve dışarıya hiç ulaşmaz. Kullanıcı girdisinden doğan yerleşik olaylar sınırı aşar; bileşenin kendi tanımladığı olaylar ise varsayılan olarak aşmaz ve dışarı çıkması isteniyorsa bu açıkça bildirilir.

Özet

  • Gölge ağacı, konak ögesine bağlı ayrı bir belge ağacıdır; konağın belgedeki kendi çocukları ışık ağacı olarak yerinde kalır.
  • Seçici sınırı iki yönde de aşmaz: belge sorgusu gölge ağacını, gölge sorgusu belge ağacını görmez. Kimlik uzayları da ayrıdır.
  • Biçem eşlemesi seçiciyle aynı sınıra uyar; kalıtılan özellikler ve özel özellikler ise sınırdan geçer ve bileşenin biçem sözleşmesini oluşturur.
  • Bileşen, dışarıdan biçemlenebilecek iç parçalarını ad vererek açar; kapsülleme bu pencerelerin dışında korunur.
  • Kapalı kip erişimi zorlaştırır ama bir güvenlik sınırı değildir; taşıdığı bilgi, iç yapının arayüzün parçası olmadığıdır.
  • Olay yolu gölge kökünden konağa atlayarak sürer; her dinleyici hedefi kendi ağacındaki karşılığıyla görür ve sınırı aşmayan olaylar gölge kökünde durur.

Sonraki Adım

Bu ders gölge ağacının nasıl yalıtıldığını gösterdi, ama içeriğinin nereden geldiğini göstermedi. İki soru açık kaldı. Birincisi, bileşenin iç yapısı her örnek için düğüm düğüm kurulursa on beş rozet on beş kez aynı kurulum kodunu çalıştırır; işaretlemenin çizilmeden duran ve ucuz çoğaltılabilen bir biçimi gerekir. İkincisi, konağın belgede yazılı ışık ağacı şu an hiçbir yerde görünmüyor; bileşenin dışarıdan aldığı içeriği gölge ağacının belirlediği bir yere yerleştirmesi gerekir. Sonraki ders bu iki mekanizmayı — çizilmeyen işaretleme kabını ve içeriğin yerleştirildiği noktaları — birlikte kurar.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat