Ders 09 / 20
Ağ ve Veri
Ağ katmanı bir isteği yükleniyor, hata ve veri hâlleri üzerinden ekrana taşır; çevrimdışı önbellek ilk hâli değiştirir. Ders veri gelince yükleniyor göstergesinin sökülen, listenin kurulan olduğunu ölçer.
Yayın Güncelleme
Bu derste
- İstek katmanını ve bir isteğin yükleniyor, hata ve veri hâllerini açıklar
- Veri gelince yükleniyor göstergesinin sökülen, listenin kurulan çıktığını ölçer
- Çevrimdışı önbelleğin açılıştaki komut sayısını ve gösterge ihtiyacını nasıl değiştirdiğini karşılaştırır
- Bayat bir değerin korunan bir düğümde durabildiğini gösterir
İçindekiler
Önceki ders durumun uygulamanın içinde nasıl tutulduğunu ölçtü. Uygulamanın en önemli veri kaynağı ise dışarıdadır: bir sunucu. Sunucudan gelen veri ekrana hemen varmaz. İstek gider, yanıt gelir ve arada ekranın kullanıcıya bir şey göstermesi gerekir. Bu ders o arayı ve arayı kapatan önbelleği ele alır.
Kursun sorusu değişmez: bu güncelleme görünüm ağacındaki düğüme ne yapıyor? Akıbet korunan, güncellenen, kurulan ya da sökülendir; güncellenen, kurulan ve sökülen düğümlerin toplamı komuttur. Akıbeti elle atamayız, kural verir:
// fate.mjs — bir guncellemenin gorunum agacindaki her dugume ne yaptigini siniflandirir export const FATES = ["korunan", "guncellenen", "kurulan", "sokulen"]; export function el(type, props = {}, children = [], key = null) { return { type, props, children, key }; } function segment(node) { return node.key === null ? node.type : `${node.type}[${node.key}]`; } function identity(node, index) { return node.key === null ? `#${index}` : `key:${node.key}`; } function sameProps(a, b) { const names = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort(); return names.every((n) => JSON.stringify(a[n]) === JSON.stringify(b[n])); } function whole(node, path, fate, ops) { const here = `${path}/${segment(node)}`; ops.push({ fate, path: here }); node.children.forEach((child) => whole(child, here, fate, ops)); } export function reconcile(prev, next, path = "", ops = []) { if (prev === null) { whole(next, path, "kurulan", ops); return ops; } if (next === null) { whole(prev, path, "sokulen", ops); return ops; } if (prev.type !== next.type || prev.key !== next.key) { whole(prev, path, "sokulen", ops); whole(next, path, "kurulan", ops); return ops; } const here = `${path}/${segment(next)}`; ops.push({ fate: sameProps(prev.props, next.props) ? "korunan" : "guncellenen", path: here }); const before = new Map(prev.children.map((child, i) => [identity(child, i), child])); const matched = new Set(); next.children.forEach((child, i) => { const id = identity(child, i); matched.add(id); reconcile(before.has(id) ? before.get(id) : null, child, here, ops); }); prev.children.forEach((child, i) => { if (!matched.has(identity(child, i))) reconcile(child, null, here, ops); }); return ops; } export function printOps(ops) { ops.forEach((op) => console.log(`${op.fate}: ${op.path}`)); } export function countFates(ops) { for (const name of FATES) { console.log(`${name}: ${ops.filter((op) => op.fate === name).length}`); } console.log(`komut: ${ops.filter((op) => op.fate !== "korunan").length}`); }
Model, bu akıbetin kuralını kurar; model çerçevenin kendisi değildir. Kural ağaçları karşılaştırır, isteğin ağda ne yaptığını görmez.
İstek Katmanı ve Üç Hâl
Uygulama ağ isteklerini dağınık yerlerden atmaz. İstek katmanı adresi, başlıkları ve hata işlemeyi tek yerde toplar; ekranlar yalnız “şu adresin verisini ver” der. Bir istek zamana yayılan bir iştir ve sonucu bir söz (Promise) olarak döner: ya veriyle çözülür ya da bir hatayla reddedilir. İstek sürerken ekranın üç hâlden birinde olduğu söylenir. Yükleniyor hâlinde yanıt henüz gelmemiştir. Veri hâlinde yanıt gelmiştir ve liste gösterilir. Hata hâlinde istek reddedilmiştir ve ekran kullanıcıya bunu bildirir.
Bu üç hâl ekranda üç farklı düğümdür: bir yükleniyor göstergesi, bir liste ve bir hata iletisi. Çerçevede istek genellikle bir yan etki kancasında başlatılır: ekran ilk kez görününce kanca çalışır, istek gider, yanıt gelince durum değişir ve ekran yeniden çizilir.
// Platform kodu: OrderFeed
function OrderFeed() {
const [status, setStatus] = useState({ phase: "loading", rows: [] });
useEffect(() => {
loadOrders().then((rows) => setStatus({ phase: "data", rows }));
}, []);
return <Feed status={status} />;
}
Bu parça ölçülmez; model çerçeveyi ve ağı çalıştırmaz. Modelde istek, kendi dosyasındaki bir Promise zinciridir ve sunucu bellekte tutulan sabit bir tablodur.
UK1. Model, gerçek ağı bellekteki bir tabloyla değiştirir: bilinen adres veriyle çözülür, bilinmeyen adres reddedilir. Gecikme ve kopma modele girmez; model yalnız yanıtın hangi hâlle geldiğini ölçer.
// request.mjs — istek katmani: bellekteki sahte sunucuya Promise ile istek atar const SERVER = { "/siparisler": [ { id: "a1", metin: "Kalem" }, { id: "b2", metin: "Defter" }, ], }; export function request(url) { return new Promise((resolve, reject) => { if (url in SERVER) resolve(SERVER[url]); else reject(new Error("bulunamadi")); }); }
Ekranın ağacını istek hâlinden renderFeed(status) kurar. Yükleniyor hâlinde başlığın altında bir gösterge, veri hâlinde bir liste, hata hâlinde bir ileti vardır. Satırlar kimlikle anahtarlanır.
// feed.mjs — siparis akisi ekraninin gorunum agacini istek halinden kurar import { el } from "./fate.mjs"; export function renderFeed(status) { let body; if (status.durum === "yukleniyor") { body = el("Spinner"); } else if (status.durum === "hata") { body = el("Notice", { metin: status.mesaj }); } else { body = el("List", {}, status.satirlar.map((row) => el("Row", { metin: row.metin }, [], row.id))); } return el("Feed", {}, [el("Title", { metin: "Siparisler" }), body]); }
Önce istek başarılı geçiyor. Ekran yükleniyor hâlinde açılır, yanıt gelince veri hâline döner.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderFeed } from "./feed.mjs"; import { request } from "./request.mjs"; const loading = renderFeed({ durum: "yukleniyor" }); request("/siparisler") .then((rows) => renderFeed({ durum: "veri", satirlar: rows })) .catch(() => renderFeed({ durum: "hata", mesaj: "yuklenemedi" })) .then((next) => { const ops = reconcile(loading, next); printOps(ops); countFates(ops); });
korunan: /Feed korunan: /Feed/Title sokulen: /Feed/Spinner kurulan: /Feed/List kurulan: /Feed/List/Row[a1] kurulan: /Feed/List/Row[b2] korunan: 2 guncellenen: 0 kurulan: 3 sokulen: 1 komut: 4
Veri gelince /Feed/Spinner sökülendir. Yerine liste ve iki satırı kurulur. Başlık ve ekranın kendisi korunandır. Komut sayısı 4’tür ve bunun üçü yeni listenin düğümleridir. Gösterge ile liste ağaçta aynı yeri paylaştığı için kural onları aynı düğümün iki hâli saymaz: tipleri farklıdır, biri tamamen çıkar, ötekisi tamamen kurulur.
Hata hâli aynı geçişin başka bir sonucudur. Aynı akışı, sunucunun bilmediği bir adresle yineliyoruz.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderFeed } from "./feed.mjs"; import { request } from "./request.mjs"; const loading = renderFeed({ durum: "yukleniyor" }); request("/yok") .then((rows) => renderFeed({ durum: "veri", satirlar: rows })) .catch(() => renderFeed({ durum: "hata", mesaj: "yuklenemedi" })) .then((next) => { const ops = reconcile(loading, next); printOps(ops); countFates(ops); });
korunan: /Feed korunan: /Feed/Title sokulen: /Feed/Spinner kurulan: /Feed/Notice korunan: 2 guncellenen: 0 kurulan: 1 sokulen: 1 komut: 2
Yine gösterge sökülendir, ama yerine kurulan tek düğüm hata iletisidir: komut 2. Yükleniyor hâlinden çıkan her yol göstergeyi söker. Hangi düğümün kurulacağını isteğin sonucu belirler.
Çevrimdışı Önbellek
Çevrimdışı önbellek, son başarılı yanıtı uygulamanın belleğinde ya da yerel depolamasında tutar. Ağ yoksa ya da yanıt gecikiyorsa ekran son bilinen veriyi gösterir. Bu, ekranın ilk hâlini değiştirir: önbellek varsa ekran yükleniyor göstergesiyle değil, doğrudan listeyle açılır.
// cache.mjs — cevrimdisi onbellek: son basarili yaniti bellekte tutar const store = new Map(); export function saveCache(url, rows) { store.set(url, rows); } export function readCache(url) { return store.has(url) ? store.get(url) : null; }
Ekranın ilk kez açıldığı iki durumu yan yana ölçüyoruz: önbelleksiz açılış ve önbellekli açılış. Ağaç henüz yok, bu yüzden önceki ağaç null’dır ve her düğüm kurulandır.
// main.mjs import { reconcile, countFates } from "./fate.mjs"; import { renderFeed } from "./feed.mjs"; import { saveCache, readCache } from "./cache.mjs"; saveCache("/siparisler", [ { id: "a1", metin: "Kalem" }, { id: "b2", metin: "Defter" }, ]); console.log("onbelleksiz:"); countFates(reconcile(null, renderFeed({ durum: "yukleniyor" }))); console.log("onbellekli:"); countFates(reconcile(null, renderFeed({ durum: "veri", satirlar: readCache("/siparisler") })));
onbelleksiz: korunan: 0 guncellenen: 0 kurulan: 3 sokulen: 0 komut: 3 onbellekli: korunan: 0 guncellenen: 0 kurulan: 5 sokulen: 0 komut: 5
Önbellekli açılış daha çok düğüm kurar: üç düğüm yerine beş. Ama ekran hiçbir an gösterge göstermez, dolayısıyla sonradan sökülecek bir gösterge de yoktur. Önbellek açılışta daha çok iş, sonraki geçişte daha az iş demektir: yanıt gelince listenin düğümleri yeniden kurulmaz, olduğu yerde karşılaştırılır.
Hata Sonrası Yeniden Deneme
Hata hâli yolun sonu değildir; kullanıcı çoğu zaman “yeniden dene” der. Yeniden deneme ağaç açısından bir geri dönüştür: hata bildirimi kalkar ve yükleniyor göstergesi yeniden belirir. Bu iki düğüm, kökün altında birbirinin yerine geçer.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderFeed } from "./feed.mjs"; const ops = reconcile( renderFeed({ durum: "hata", mesaj: "yuklenemedi" }), renderFeed({ durum: "yukleniyor" }), ); printOps(ops.filter((op) => op.fate !== "korunan")); countFates(ops);
sokulen: /Feed/Notice kurulan: /Feed/Spinner korunan: 2 guncellenen: 0 kurulan: 1 sokulen: 1 komut: 2
Kök ve başlık korunandır, çünkü her hâlde aynı tiptedir ve aynı özellikleri taşır. Hata bildirimi sökülen, gösterge kurulandır; iki düğüm aynı konumda olsa da tipleri farklı olduğundan kural biri diğerinin güncellenmişi saymaz. Bu, yeniden denemenin ağaçtaki maliyetinin sabit olduğunu gösterir: hata mesajının uzunluğu ya da denemenin kaçıncı olduğu komut sayısını değiştirmez.
Burada ince bir nokta var. Yeniden deneme başarısız olursa ekran yeniden hata hâline döner ve bildirim yeniden kurulur. Kullanıcı art arda başarısız denemelerde göstergenin ve bildirimin sürekli kurulup söküldüğünü görür. Bu bir ağaç sorunu değil, bir tasarım sorusudur: yeniden deneme sürerken eski hata bildirimini tutmak ağacı sakinleştirir ama kullanıcıya sürenin bittiğini söylemez. Kural yalnız hangisinin kaç komut olduğunu söyler; hangisinin daha iyi olduğunu söylemez.
Sınırlayıcı Ölçüm
Önbellekten gelen değer yanıt gelene kadar eskimiş olabilir. M30/K04’te bir ekranın modelle uyuşup uyuşmadığı sorulur ve bayat bir ekran bir sonuçtur. Bu derste ölçülen akıbet uyumu ölçmez: bayat bir değer de korunan bir düğümde durabilir. Yeni yanıt eski değerle aynıysa düğüm korunandır, ister değer güncel olsun ister yanıt hiç gelmesin. Aşağıdaki ölçüm iki durumu karşılaştırır. Birinde yeni yanıt önbellekle aynı satırları taşır. Öteki durumda ikinci satırın metni değişmiştir.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderFeed } from "./feed.mjs"; const cached = [ { id: "a1", metin: "Kalem" }, { id: "b2", metin: "Defter" }, ]; const stale = renderFeed({ durum: "veri", satirlar: cached }); const same = renderFeed({ durum: "veri", satirlar: cached.map((row) => ({ ...row })) }); const changed = renderFeed({ durum: "veri", satirlar: [cached[0], { id: "b2", metin: "Defter 2" }], }); console.log("ayni yanit:"); countFates(reconcile(stale, same)); console.log("degisen yanit:"); printOps(reconcile(stale, changed).filter((op) => op.fate !== "korunan"));
ayni yanit: korunan: 5 guncellenen: 0 kurulan: 0 sokulen: 0 komut: 0 degisen yanit: guncellenen: /Feed/List/Row[b2]
Aynı yanıtta bütün düğümler korunandır ve komut 0’dır. Değişen yanıtta yalnız Row[b2] güncellenendir. İlk durumda kural “hiçbir şey olmadı” der, ama bu önbelleğin güncel olduğu anlamına gelmez: yanıt hiç gelmemiş olsaydı da aynı ağaç aynı biçimde korunan kalırdı. Bayat ile korunan aynı soru değildir. Birincisi değerin sunucudaki gerçeğe uyup uymadığıdır ve ağacın dışındadır. İkincisi güncellemenin düğüme yaptığıdır.
Yaygın Yanılgılar
İlk yanılgı, “yükleniyor göstergesi liste gelince gizlenir” düşüncesidir. Ağaç açısından gösterge gizlenmez, ağaçtan çıkar: düğüm sökülür ve yerel görünümü yok olur. Tekrar yükleme istenirse gösterge yeniden kurulur ve eskisinin hiçbir izi kalmaz.
İkinci yanılgı, “önbellek ağ yükünü azalttığı için ekranı da hafifletir” düşüncesidir. Önbellekli açılış, ölçümde görüldüğü gibi, daha çok düğüm kurar. Kazanç göstergenin ve bekleyişin ortadan kalkmasıdır, komut sayısının azalması değil.
Üçüncü yanılgı, hata hâlini bir istisna saymaktır. Hata istek katmanının olağan sonuçlarından biridir ve ağaçta olağan bir düğümdür. Ekranın hata düğümünü kurmayı bilmesi, hata hâlinde boş ekran kalmamasını sağlar.
Dördüncü yanılgı, yeniden denemenin ağaçta ucuz ve görünmez olduğunu varsaymaktır. Yeniden deneme ölçümünde hata bildirimi sökülür ve gösterge kurulur: iki komut. Her başarısız denemede bu iki komut tekrarlanır. Denemeler art arda başarısız oluyorsa ekran, kullanıcının gözünde, hata ile yükleniyor arasında gidip gelir. Ağaç açısından her gidiş geliş aynı iki komuttur; bunun kullanıcıya yorucu gelip gelmediği ise ölçümün dışında kalan bir sorudur.
Beşinci yanılgı, istek ve ekranı tek şey saymaktır. İstek bir söz döndürür ve üç sonuçtan birine varır; ekran ise bu sonucu bir ağaca çevirir. İstek katmanı hata mesajını üretir, ağaç yalnız bu mesajı bir düğümün özelliği olarak taşır. İki katman ayrı kalırsa istek katmanı ekrana dokunmadan test edilebilir ve ekran isteği çalıştırmadan çizilebilir.
Altıncı yanılgı, çevrimdışı önbelleği veri tazeliğinden bağımsız saymaktır. Önbellekteki liste son başarılı yanıttır ve sunucudaki güncel durumdan eski olabilir. Ağaç bunu görmez: önbellekten gelen satırlarla sunucudan gelen satırlar aynı biçimdeyse kural ikisini ayırt etmez. Eskiliği kullanıcıya göstermek istiyorsak bunu ağaca açıkça yazmak, örneğin ayrı bir düğüm eklemek gerekir.
Yedinci yanılgı, üç hâli birbirinden bağımsız ekranlar saymaktır. Yükleniyor, hata ve veri aynı kök düğümün altında gövdeyi değiştirir; kök ve başlık üç hâlde de korunandır. Bu yüzden hâller arası geçişin maliyeti ekranın büyüklüğüyle değil, gövdenin büyüklüğüyle ölçülür.
Özet
- Bu dersin akıbeti: sökülen. Veri gelince
/Feed/Spinnerdüğümü, yerini listeye bıraktığı için sökülen çıktı; liste ve satırları kurulandır. - Hata hâlinde de gösterge sökülür, yerine hata iletisi kurulur; yükleniyor hâlinden çıkan her yol göstergeyi söker.
- Önbellekli açılış daha çok düğüm kurar ama gösterge kurmaz; önbelleksiz açılış göstergeyi kurar ve sonradan söker.
- Yeni yanıt önbellekle aynı değeri taşıyorsa satırlar korunandır; yanıtın değişen satırı güncellenendir.
- Akıbet uyumu ölçmez: bayat bir değer korunan bir düğümde durabilir.
Sonraki Adım
Bu derste önbellek bellekte durdu ve uygulama kapanınca kayboldu. Kalıcı olması gereken veri için yerel depolama gerekir. Sıradaki ders, Yerel Depolama, açılışta depodan okunan bir taslağın ağaçta nasıl belirdiğini ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.