Ders 14 / 24
Özel Elemanlar
Tarayıcının öge kaydına yeni bir etiket adı tanıtmak; ad kuralı, yaşam döngüsü geri çağrılarının sırası, tanımdan önce ağaca girmiş ögenin yükseltilmesi ve öznitelik–özellik yansıması.
İçindekiler
Önceki ders ana iş parçacığındaki yükü çalışan iş parçacıklarına dağıttı: hesap bir yerde, arayüz başka bir yerde. Yük dağıtıldıktan sonra geriye arayüzün kendi sorunu kalır. İstasyon sayfasındaki ölçüm rozeti — değeri, birimi ve eşik rengiyle bir sayıyı gösteren küçük parça — sayfada beş kez, gösterge panosunda on beş kez tekrarlanır. Her tekrarda aynı işaretleme elle yazılır, aynı sınıflar elle verilir, aynı dinleyiciler elle bağlanır.
Bu tekrarı bir yere toplamanın yolu, tarayıcıya yeni bir etiket adı tanıtmaktır: yapısı, davranışı ve yaşam döngüsü kendi içinde tanımlı, belgede tek satırla yazılan bir öge. Bu derste kurulan mekanizmanın adı özel öge (custom element) dir.
Ad Kuralı ve Öge Kaydı
Tarayıcı, etiket adlarını bir tabloda tutar: p, section, input ve geri kalanı. Bu
tabloya program içinden yeni bir ad eklenir; ada karşılık gelen bir sınıf verilir ve
tarayıcı o addaki her ögeyi bu sınıfın bir örneğiyle ilişkilendirir. Bu tabloya öge
kaydı (custom element registry) denir.
Eklenen adın tek bir biçim kuralı vardır: en az bir tire içermeli ve küçük harfle
başlamalıdır. olcum-rozeti geçerlidir, rozet değildir. Kuralın gerekçesi kısıt değil
korumadır. HTML’e sonradan eklenen bir etiket adı hiçbir zaman tire içermez; tire içeren
ad uzayı böylece belge yazarına ayrılmış olur. Bu ayrım olmasaydı, bugün tanımlanan bir
rozet ögesi ileride standarda giren bir rozet etiketiyle çakışır ve sayfa sessizce
başka bir şey yapmaya başlardı.
Bir ad yalnızca bir kez tanımlanabilir. Aynı adı ikinci kez tanımlamaya çalışmak hata üretir; bu, aynı bileşenin iki ayrı sürümünün aynı sayfaya girmesini erken yakalayan bir denetimdir.
İki tür özel öge vardır. Birincisi, temel öge sınıfını genişleten ve kendi etiket adıyla
yazılan bağımsız ögedir. İkincisi, var olan bir etiketi genişletir ve o etiketin
davranışını devralır; belgede is özniteliğiyle yazılır. İkinci türün desteği koşulludur
ve özellik saptamayla sınanır; bu ders birinci türü esas alır.
Yaşam Döngüsü Geri Çağrıları
Özel ögenin sınıfı dört noktada tarayıcı tarafından çağrılır. Bunlara yaşam döngüsü geri çağrısı (lifecycle callback) denir.
Kurucu, öge nesnesi ilk kez var edildiğinde çalışır. Bağlanma geri çağrısı, öge belgeye bağlı bir ağaca girdiğinde çalışır. Bağlantı kesilme geri çağrısı, öge o ağaçtan çıkarıldığında. Öznitelik değişimi geri çağrısı, önceden bildirilmiş bir özniteliğin değeri değiştiğinde.
Son geri çağrının bir koşulu vardır: sınıf, hangi özniteliklerin izleneceğini durağan bir listeyle bildirmelidir. Listede olmayan bir özniteliğin değişmesi hiçbir çağrı üretmez. Bu, her öznitelik yazımının bir geri çağrı tetiklemesini önleyen bilinçli bir sınırlamadır.
Sıra, elle yazılmış bir kurulum kodundan farklıdır ve tahmin edilerek öğrenilemez. Aşağıdaki model çağrı sırasını görünür kılar.
// yasam-dongusu.mjs — ozel oge yasam dongusunun ve yukseltmenin kucuk bir modeli const cagrilar = []; const belge = { ad: "#belge", ozn: {}, cocuklar: [], ebeveyn: null, ornek: null }; const bagliMi = (d) => { let a = d; while (a.ebeveyn) a = a.ebeveyn; return a === belge; }; function* hepsi(kok) { yield kok; for (const c of kok.cocuklar) yield* hepsi(c); } const tanimlar = new Map(); // Yukseltme sirasi sabittir: kurucu → izlenen oznitelikler → bagliysa baglanma. function yukselt(dugum) { const sinif = tanimlar.get(dugum.ad); if (!sinif || dugum.ornek) return; dugum.ornek = new sinif(dugum); for (const ad of sinif.izlenenOznitelikler ?? []) if (ad in dugum.ozn) dugum.ornek.oznitelikDegisti(ad, null, dugum.ozn[ad]); if (bagliMi(dugum)) dugum.ornek.baglandi(); } // Oge olusturulurken tanim varsa yukseltme aninda olur, yoksa oge tanimsiz kalir. function oge(ad, ozn = {}) { const dugum = { ad, ozn, cocuklar: [], ebeveyn: null, ornek: null }; yukselt(dugum); return dugum; } function tanimla(ad, sinif) { if (!ad.includes("-")) throw new Error(`gecersiz ad: ${ad}`); if (tanimlar.has(ad)) throw new Error(`zaten tanimli: ${ad}`); tanimlar.set(ad, sinif); for (const d of hepsi(belge)) if (d.ad === ad) yukselt(d); // belge sirasina gore } function cikar(dugum) { const kardesler = dugum.ebeveyn.cocuklar; const bagliydi = bagliMi(dugum); kardesler.splice(kardesler.indexOf(dugum), 1); dugum.ebeveyn = null; if (bagliydi) dugum.ornek?.baglantiKesildi(); } function ekle(ebeveyn, cocuk) { if (cocuk.ebeveyn) cikar(cocuk); cocuk.ebeveyn = ebeveyn; ebeveyn.cocuklar.push(cocuk); if (!cocuk.ornek) yukselt(cocuk); else if (bagliMi(cocuk)) cocuk.ornek.baglandi(); } function oznitelikYaz(dugum, ad, deger) { const eski = dugum.ozn[ad] ?? null; dugum.ozn[ad] = deger; const izlenen = tanimlar.get(dugum.ad)?.izlenenOznitelikler ?? []; if (dugum.ornek && izlenen.includes(ad)) dugum.ornek.oznitelikDegisti(ad, eski, deger); } let sayac = 0; class OlcumRozeti { static izlenenOznitelikler = ["deger", "birim"]; constructor(dugum) { this.etiket = `rozet#${++sayac}`; cagrilar.push(`${this.etiket} kurucu`); } oznitelikDegisti(ad, eski, yeni) { cagrilar.push(`${this.etiket} oznitelikDegisti(${ad}, ${eski}, ${yeni})`); } baglandi() { cagrilar.push(`${this.etiket} baglandi`); } baglantiKesildi() { cagrilar.push(`${this.etiket} baglantiKesildi`); } } // A. Tanimdan ONCE agaca girmis iki oge (ayristiricinin urettigi durum). const pano = oge("section", { id: "pano" }); ekle(belge, pano); ekle(pano, oge("olcum-rozeti", { deger: "-4.2", birim: "C" })); ekle(pano, oge("olcum-rozeti", { deger: "72" })); cagrilar.push("--- tanimla('olcum-rozeti') ---"); tanimla("olcum-rozeti", OlcumRozeti); // B. Tanimdan SONRA olusturulan bir oge. cagrilar.push("--- tanimdan sonra olusturulan oge ---"); const yeni = oge("olcum-rozeti"); oznitelikYaz(yeni, "deger", "3.4"); ekle(pano, yeni); // C. Bagli bir oge baska bir ebeveyne tasiniyor. cagrilar.push("--- oge baska bir ebeveyne tasiniyor ---"); const yanPano = oge("aside", { id: "yan" }); ekle(belge, yanPano); ekle(yanPano, yeni); // D. Tire icermeyen ad. cagrilar.push("--- tire icermeyen ad ---"); try { tanimla("rozet", OlcumRozeti); } catch (h) { cagrilar.push(h.message); } console.log(cagrilar.join("\n"));
--- tanimla('olcum-rozeti') ---
rozet#1 kurucu
rozet#1 oznitelikDegisti(deger, null, -4.2)
rozet#1 oznitelikDegisti(birim, null, C)
rozet#1 baglandi
rozet#2 kurucu
rozet#2 oznitelikDegisti(deger, null, 72)
rozet#2 baglandi
--- tanimdan sonra olusturulan oge ---
rozet#3 kurucu
rozet#3 oznitelikDegisti(deger, null, 3.4)
rozet#3 baglandi
--- oge baska bir ebeveyne tasiniyor ---
rozet#3 baglantiKesildi
rozet#3 baglandi
--- tire icermeyen ad ---
gecersiz ad: rozet
Çıktının okunması gereken dört yeri var.
Birincisi, ilk yedi satır tanım çağrılana kadar hiçbir geri çağrının çalışmadığını gösterir. Ögeler ağaçtaydı, öznitelikleri yerindeydi; bekleyen şey tanımdı.
İkincisi, her öge için sıra aynıdır: kurucu, sonra izlenen özniteliklerin her biri için bir öznitelik değişimi çağrısı, sonra bağlanma. Bağlanma her zaman sonuncudur. Bir bileşenin ilk çizimini bağlanma geri çağrısında yapması bu yüzden güvenlidir: o noktada öznitelik değerleri okunmuştur.
Üçüncüsü, ikinci rozette birim özniteliği belgede yazılı olmadığı için çağrı üretmemiş,
eksik öznitelik için varsayılan bir çağrı yapılmamıştır. Varsayılan değeri kurmak sınıfın
işidir.
Dördüncüsü, taşınan öge için bağlantı kesilme ve bağlanma art arda çalışmıştır. Ağaçta yer değiştirmek, ögeyi çıkarıp yeniden eklemektir. Buradan bağlayıcı bir kural çıkar: bağlanma geri çağrısı ögenin ömrü boyunca birden çok kez çalışabilir, bu yüzden yinelemeye dayanıklı yazılmalıdır. Dinleyici bağlayan bir bağlanma geri çağrısının karşılığında bağlantı kesilme geri çağrısında dinleyiciyi kaldırması gerekir; yoksa her taşımada bir dinleyici birikir.
Kurucuda Yapılamayanlar
Kurucu, ögenin en erken uğrağıdır ve bu erkenlik bir kısıt getirir. Kurucu çalıştığı anda öge henüz ağaçta olmayabilir, öznitelikleri okunmamış olabilir, çocukları ayrıştırılmamış olabilir. Ayrıştırıcı bir etiketi gördüğü anda örneği kurar; kapanış etiketine daha gelmemiştir.
Bundan üç kural çıkar. Kurucu öznitelik okumaz ve yazmaz. Kurucu çocuk düğüm eklemez ve okumaz. Kurucu belgeye erişmez. Kurucunun işi yalnızca ögenin kendi alanlarını ilk değerlerine kurmaktır; belgeyle ilgili her şey bağlanma geri çağrısına kalır.
Ayrıştırma sırasının bir yan sonucu daha vardır: belgenin ortasındaki bir özel ögenin bağlanma geri çağrısı çalıştığında, o ögenin çocukları henüz ayrıştırılmamış olabilir. Çocuklarını okuması gereken bir bileşen bunu ya ayrıştırmanın bitmesini bekleyerek ya da çocuk listesini gözlemleyerek yapar; bağlanma anında sayarak yapamaz.
Yükseltme ve Tanımın Gecikmesi
Tanımdan önce ağaca girmiş bir ögenin tanım geldiğinde sınıfla ilişkilendirilmesine yükseltme (upgrade) denir. Yükseltme yeni bir nesne üretmez: ağaçtaki nesne olduğu gibi kalır, sınıfın davranışını kazanır.
Bu, sayfanın tanımdan önceki hâlinin bir sorun değil bir ara durum olduğu anlamına gelir. Tanımı taşıyan betik ertelenmiş olarak yüklendiğinde, kullanıcı belgedeki ham ögeleri kısa bir süre görür. Bu ara durum biçemle yönetilir: tanımlanmamış ögeleri seçen bir sözde sınıf vardır ve bu ögeler tanım gelene kadar gizlenebilir ya da yer tutucu biçimiyle gösterilebilir. Kurulacak denge, boş bir alan göstermekle biçimsiz içerik göstermek arasındadır; ölçüm rozetinde doğru seçim, sayıyı ham hâliyle göstermek ve yalnızca eşik rengini tanım gelene kadar uygulamamaktır — okunabilir içerik hiç gizlenmez.
Program tarafında da bir bekleme noktası gerekir: bir adın tanımlanmasını bekleyen ve tanım geldiğinde çözülen bir söz. Bir ögenin özelliklerine yükseltme öncesinde erişen kod bu sözü bekler; beklemeyen kod bir sonraki bölümdeki tuzağa düşer.
Öznitelik ile Özellik Arasında Yansıma
DOM API’si dersinde kurulan ayrım burada tasarım kararına dönüşür: öznitelik belgede yazılı dizgidir, özellik program tarafındaki yaşayan değerdir. Bir özel öge ikisini de sunar ve arada bir yansıma (reflection) kurar: özelliğe yazılan değer özniteliğe yazılır, özniteliğe yazılan değer özelliği günceller.
İki yönlü bu bağ dikkatsiz kurulduğunda kendi kendini besler. Özellik ayarlayıcısı özniteliği yazar, öznitelik değişimi geri çağrısı özelliği yazar, o da özniteliği yeniden yazar. Döngüyü kesen kural sadedir: her iki yönde de yazmadan önce değerin gerçekten değiştiği sınanır. Aynı değeri yeniden yazan bir adım zincirin devamını kesmelidir.
İkinci tuzak daha sinsidir ve yükseltmenin gecikmesinden doğar.
// yukseltme-tuzagi.mjs — tanimdan once atanan ozellik erisimciyi golgeler // Yukseltme, ogenin prototipini sinif prototipiyle degistirmektir; oge nesnesi aynidir. class Rozet { get deger() { return this.degerAlani ?? null; } set deger(v) { this.degerAlani = Number(v); this.cizimSayisi = (this.cizimSayisi ?? 0) + 1; // gercek bir ogede bu, yeniden cizim olurdu } } function yukselt(dugum, { kurtar }) { Object.setPrototypeOf(dugum, Rozet.prototype); if (kurtar && Object.hasOwn(dugum, "deger")) { const bekleyen = dugum.deger; // erisimciyi golgeleyen kendi ozelligi delete dugum.deger; // golge kaldirilir dugum.deger = bekleyen; // atama artik erisimciye gider } } console.log("cagiran kod, tanim yuklenmeden once ozellige deger atiyor:"); for (const kurtar of [false, true]) { const dugum = {}; // tanimi henuz yuklenmemis oge dugum.deger = "5"; // erisimci yok: ogenin kendi ozelligi olusur yukselt(dugum, { kurtar }); console.log( ` kurtarma ${kurtar ? "var" : "yok"} → deger: ${JSON.stringify(dugum.deger)},`, `tip: ${typeof dugum.deger}, cizim: ${dugum.cizimSayisi ?? 0}`, ); }
cagiran kod, tanim yuklenmeden once ozellige deger atiyor: kurtarma yok → deger: "5", tip: string, cizim: 0 kurtarma var → deger: 5, tip: number, cizim: 1
Tanım yüklenmeden önce özelliğe yapılan atama, ögenin kendi özelliğini oluşturur. Yükseltme prototipi değiştirir, ama prototipteki erişimci ögenin kendi özelliğinin altında kalır ve hiçbir zaman çağrılmaz. İlk satırda değer dizgi olarak durur, dönüşüm yapılmamış, çizim tetiklenmemiştir. Sorun sessizdir: bir hata iletisi yoktur, yalnızca bileşen çalışmaz.
Kurtarma tek kalıptır ve kurucuda uygulanır: sınıfın tanımladığı her özellik için, ögede aynı adda bir kendi özelliği varsa değeri alınır, özellik silinir ve yeniden atanır. Silme prototipteki erişimciyi görünür kılar, yeniden atama onu çalıştırır. İkinci satırda değer sayıya dönüşmüş ve çizim bir kez tetiklenmiştir.
Özet
- Özel öge, tarayıcının öge kaydına bir etiket adı ile bir sınıf çifti eklemektir; ad en az bir tire içermeli ve yalnızca bir kez tanımlanmalıdır.
- Tire koşulu, sonradan standarda girecek etiket adlarıyla çakışmayı önlemek için ad uzayını ayırır.
- Yaşam döngüsü çağrıları sabit sırayla işler: kurucu, izlenen özniteliklerin değişim çağrıları, sonra bağlanma; bağlantı kesilme ise ağaçtan çıkarılışta.
- Bağlanma geri çağrısı ögenin ömrü boyunca birden çok kez çalışabilir; kurduğu her şeyi bağlantı kesilme geri çağrısı sökmelidir.
- Kurucu öznitelik okumaz, çocuk eklemez, belgeye erişmez; belgeyle ilgili iş bağlanma geri çağrısına aittir.
- Tanımdan önce ağaçta bulunan ögeler yükseltilir; tanımdan önce özelliğe yazılan değer erişimciyi gölgeler ve kurucuda sil–yeniden ata kalıbıyla kurtarılır.
Sonraki Adım
Bu ders ögeye bir ad ve bir yaşam döngüsü verdi, ama içini açık bıraktı. Rozetin işaretlemesi hâlâ belgenin ortak ağacında durur: sayfanın herhangi bir yerinde yazılmış bir biçem kuralı rozetin içine sızabilir, rozetin kendi kuralı da dışarı taşabilir; sayfadaki bir seçici rozetin iç düğümlerini bulur ve onlara dokunur. Yeniden kullanılabilir bir parçanın gereksinimi tam da bunun tersidir: içerisi dışarıdan görünmesin, dışarısı içeriden etkilenmesin. Sonraki ders belge ağacının içine ayrı bir ağaç açan ve bu iki yönlü yalıtımı kuran mekanizmayı inceler.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.