Ders 07 / 20
Gezinme
Gezinme, uygulamanın ekranlarını yığın, sekme ya da çekmece olarak düzenler. Ders yeni bir rota itilince ve geri dönülünce görünüm ağacındaki düğümlerin akıbetini ölçer: alttaki ekran korunan kalır.
Yayın Güncelleme
Bu derste
- Yığın, sekme ve çekmece gezinmesini rota ve geri yığını kavramlarıyla açıklar
- Yeni rota itmenin ve geri dönmenin görünüm ağacında hangi düğümleri kurup söktüğünü ölçer
- Alttaki ekranın yeni rota itilince neden korunan çıktığını gösterir
- Görünmeyen sekmenin ağaçta kalıp kalmamasının akıbeti nasıl değiştirdiğini karşılaştırır
İçindekiler
Önceki ders, tek platformda doğan bir anahtarın ortak kodun içinde nasıl durduğunu ele aldı. Bu dersten itibaren kurs tek ekranlı örneklerden çıkar ve uygulamanın katmanlarına geçer. İlk katman gezinmedir: kullanıcı bir ekrandan ötekine nasıl geçer ve geçerken eski ekrana ne olur.
Kursun sorusu ders boyunca değişmez: bu güncelleme görünüm ağacındaki düğüme ne yapıyor? Yanıtın dört biçimi var. Düğüm aynı kalır ve özellikleri de aynıysa korunandır. Düğüm aynı kalır ama bir özelliği değişirse güncellenendir. Düğüm ağaçta ilk kez belirirse kurulan, ağaçtan çıkarsa sökülendir. Güncellenen, kurulan ve sökülen düğümlerin toplamına komut denir: çerçevenin yerel tarafa göndermesi gereken iş budur. Akıbet elle atanmaz. Aşağıdaki çekirdek iki ağacı karşılaştırır ve her düğümün akıbetini 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}`); }
Kural, alt ağaç yalnız sonraki ağaçtaysa kurulan, yalnız öncekindeyse sökülen der. Tip ya da anahtar farklıysa eski düğümün tamamı sökülen, yenisinin tamamı kurulandır. Aksi hâlde özellikleri karşılaştırır; çocukları anahtarla ya da sırayla eşler. Model bu akıbetin kuralını kurar; model çerçevenin kendisi değildir.
Yığın: Rota İtmek ve Geri Dönmek
Gezinmenin en yaygın biçimi yığındır. Her ekran bir rotaya karşılık gelir: rota ekranın adıdır. Açık rotaların sıralı listesine geri yığını denir. Kullanıcı bir satıra dokununca yeni rota yığının sonuna itilir; geri düğmesine basınca son rota çıkarılır. Yığın gezinme kitaplığı bu listeyi tutar ve her rota için bir ekran kurar. Uygulama yazarının işi, rotaları ve hangi ekrana gideceklerini tanımlamaktır.
// Platform kodu: RootStack
function RootStack() {
return (
<Stack>
<Screen name="liste" component={ListScreen} />
<Screen name="detay" component={DetailScreen} />
</Stack>
);
}
Bu parça ölçülmez; model kitaplığı çalıştırmaz. Modelde yığın, rota adlarının listesidir ve renderStack her rotayı, rota adıyla anahtarlanmış bir Screen düğümüne çevirir. Anahtar önemlidir: aynı adlı rota iki ağaçta aynı ekran sayılır.
UK1. Model, yığındaki her rotanın ekranını görünüm ağacında tutar; üstte kalan rota alttakini örtse de alttaki ekranın düğümü ağaçtan çıkmaz. Gerçek kitaplığın bu seçimi nasıl yaptığı kitaplığa bağlıdır; model bir seçim yapar ve bunu açıkça söyler.
// stack.mjs — gezinme yiginin rotalarindan gorunum agaci kurar import { el } from "./fate.mjs"; export function renderStack(routes) { return el("Stack", {}, routes.map((name) => el("Screen", { baslik: name }, [el("Text", { metin: name })], name))); }
Önce liste ekranındayız ve bir satıra dokunuyoruz. Yığın liste rotasından liste, detay rotalarına geçer.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderStack } from "./stack.mjs"; const ops = reconcile(renderStack(["liste"]), renderStack(["liste", "detay"])); printOps(ops); countFates(ops);
korunan: /Stack korunan: /Stack/Screen[liste] korunan: /Stack/Screen[liste]/Text kurulan: /Stack/Screen[detay] kurulan: /Stack/Screen[detay]/Text korunan: 3 guncellenen: 0 kurulan: 2 sokulen: 0 komut: 2
Yeni rota itildiğinde detay ekranı ve içindeki metin kurulandır: iki düğüm. Alttaki /Stack/Screen[liste] korunandır, çünkü özellikleri değişmedi ve anahtarı aynı kaldı. Komut sayısı yalnız yeni ekranın düğümlerini sayar. Kural uygulamanın ne kadar ekranı olduğuna değil, bu güncellemede hangi düğümün değiştiğine bakar.
Geri dönüş aynı kuralın tersidir. Üç ekranlı bir yığından bir rota çıkarılır; odak satırı, yani dönülen ekran, ayrıca yazdırılır.
// main.mjs import { reconcile, printOps, countFates } from "./fate.mjs"; import { renderStack } from "./stack.mjs"; const ops = reconcile( renderStack(["liste", "detay", "not"]), renderStack(["liste", "detay"]), ); printOps(ops.filter((op) => op.path === "/Stack/Screen[detay]")); countFates(ops);
korunan: /Stack/Screen[detay] korunan: 5 guncellenen: 0 kurulan: 0 sokulen: 2 komut: 2
Geri dönülen detay ekranı korunandır, çıkarılan not ekranı ve metni sökülendir. Sökülen bir ekranın durumu, yani üzerinde yazılmış bir giriş ya da kaydırma konumu, düğümle birlikte yok olur. Geri dönüp aynı rotaya yeniden gidildiğinde ekran yeniden kurulur. Bu yüzden ekran kapanırken kaybolmaması gereken bilgi ekranın dışında, sonraki derslerde ele alınan katmanlarda durur.
Rotayı Değiştirmek
Gezinmede bir de değiştirme vardır: yığının son rotası çıkarılır ve yerine başka bir rota konur. Bir ürün sayfasından başka bir ürün sayfasına geçmek bunun örneğidir. Ekranın kodu aynıdır, yalnız hangi ürünü gösterdiği değişir. Hangi ürünün gösterildiği rotaya parametre olarak girer.
UK3. Model rota adını ve parametresini tek anahtarda birleştirir: urun-1 ve urun-2 iki ayrı rotadır. Gerçek kitaplıklar anahtarı çoğunlukla rota adından ve kimliğinden kurar; model bu birleşimi tek dizgeye indirir.
// main.mjs import { reconcile, printOps } from "./fate.mjs"; import { renderStack } from "./stack.mjs"; printOps(reconcile(renderStack(["liste", "urun-1"]), renderStack(["liste", "urun-2"])));
korunan: /Stack korunan: /Stack/Screen[liste] korunan: /Stack/Screen[liste]/Text kurulan: /Stack/Screen[urun-2] kurulan: /Stack/Screen[urun-2]/Text sokulen: /Stack/Screen[urun-1] sokulen: /Stack/Screen[urun-1]/Text
Anahtar farklı olduğu için urun-1 ekranı metniyle birlikte sökülür, urun-2 ekranı metniyle birlikte kurulur; liste korunandır. Ekran tipi aynı olsa da kural düğümleri anahtarla eşler, bu yüzden iki ürün sayfası aynı düğüm sayılmaz. Sonuç kullanıcıya şöyle yansır: birinci ürün sayfasında kaydırılmış konum ya da yazılmış bir not ikinci ürüne taşınmaz. Taşınması isteniyorsa ekranın kimliği rota adı olur ve ürün bilgisi özellik olarak girer; o durumda aynı düğüm güncellenen olurdu. Hangisinin doğru olduğu uygulama özelliğine bağlıdır ve kural bu kararı vermez.
Sekme ve Çekmece
Sekme gezinmesinde rotalar bir yığın oluşturmaz, kardeştir: kullanıcı bir sekme çubuğundan birini seçer ve yalnız o sekmenin ekranı görünür. Çekmece gezinmesi aynı fikrin kenardan açılan biçimidir; rotalar kardeştir, seçimi çubuk yerine bir menü yapar. Üçünün gezinme olarak farkı geri yığınındadır; görünüm ağacı açısından asıl soru başka: görünmeyen sekmenin ekranı ağaçta kalıyor mu?
UK2. Bu soruyu model iki seçenekle ölçer. Birinde görünmeyen sekmeler ağaçta durur ve yalnız görünürlük özelliği değişir. Öteki seçenekte yalnız seçili sekme ağaçtadır. Gerçek kitaplıklar bu iki davranıştan birini varsayılan sunar ve çoğu ayar olarak değiştirmeye izin verir; model hangisinin varsayılan olduğunu söylemez.
// tabs.mjs — sekme cubugunun gorunum agacini kurar; saklayan secenek gizli sekmeyi agacta tutar import { el } from "./fate.mjs"; export function renderTabs(active, names, keepHidden) { const shown = keepHidden ? names : names.filter((name) => name === active); return el("Tabs", {}, shown.map((name) => el("Screen", { gorunen: name === active }, [el("Text", { metin: name })], name))); }
Kullanıcı ana sekmesinden ara sekmesine geçiyor. İki seçenek aynı geçişi yaşar ve yalnız akıbet sayıları karşılaştırılır.
// main.mjs import { reconcile, countFates } from "./fate.mjs"; import { renderTabs } from "./tabs.mjs"; const names = ["ana", "ara", "profil"]; console.log("saklayan:"); countFates(reconcile(renderTabs("ana", names, true), renderTabs("ara", names, true))); console.log("saklamayan:"); countFates(reconcile(renderTabs("ana", names, false), renderTabs("ara", names, false)));
saklayan: korunan: 5 guncellenen: 2 kurulan: 0 sokulen: 0 komut: 2 saklamayan: korunan: 1 guncellenen: 0 kurulan: 2 sokulen: 2 komut: 4
Saklayan seçenekte iki ekran güncellenendir: biri görünür olur, öteki görünmez. Metin düğümleri ve üçüncü sekme korunandır. Saklamayan seçenekte ana ekranı sökülür, ara ekranı kurulur; her biri metniyle birlikte iki düğümdür. Aynı geçiş, iki seçenekte iki ayrı akıbet dağılımı verir. Saklayan seçenek sekmeye dönüldüğünde durumu korur ama ağaçta görünmeyen ekranların maliyetini taşır. Saklamayan seçenek ağacı küçük tutar ve geri dönüşte ekranı baştan kurar.
Kaynak Parçayı Satır Satır Okumak
Yukarıdaki yığın tanımı kısadır ama dört ayrı karar taşır. İlk karar, Stack kapsayıcısının kendisidir: ekranlar bir yığının çocuğu olarak tanımlanır, yani aralarındaki ilişki sıradır. İkinci karar, her Screen satırındaki name değeridir. Bu ad rotanın kimliğidir; ağaçta anahtara dönüşen şey de odur. Üçüncü karar, component satırıdır: rota yalnızca bir ad değil, o adla açılacak ekranın tanımıdır. Dördüncü karar, tanımların sırasıdır: ilk tanımlanan rota başlangıç ekranı olur, ötekilere ancak bir geçişle varılır.
Bu satırlardan hiçbiri bir düğüm kurmaz. Tanım, ekranların hangi adla var olabileceğini söyler; hangisinin şu an açık olduğunu söylemez. Açık olanlar yığının çalışma anındaki içeriğidir: rota listesi. Ağaç bu listeden türer ve kural, liste değişince ağacın ne yaptığını sınıflar. Tanımı değiştirmek ile yığını değiştirmek bu yüzden farklı işlerdir: ilki uygulamanın kodunu, ikincisi kullanıcının o anki yolunu etkiler.
Yığın Derinleştikçe
Kullanıcı bir listeden ayrıntıya, ayrıntıdan bir alt ayrıntıya iner. Her adımda yığına bir rota eklenir. Bu ölçümde itme alttaki ekranları korunan bıraktı ve yalnız yeni ekranın düğümlerini kurdu. Bu, gezinmeyi ucuz gösterir: bir adımın maliyeti, yeni ekranın büyüklüğüyle sınırlıdır, yığının uzunluğuyla değil.
Ama aynı sonucun bir bedeli vardır. Alttaki ekranlar ağaçta durduğu için her biri düğümlerini, durumunu ve yerel görünümünü taşımaya devam eder. Yığın derinleştikçe ağaç büyür. Kullanıcının geri dönüşte kaldığı yeri bulması bu seçimin kazancıdır; ağacın büyümesi bedelidir. Bir uygulama çok derin gezinmeye izin veriyorsa kazancın bedelini bilerek ödemesi gerekir. Gezinmeyi tasarlarken sorulacak soru “geri dönülebilir mi?” kadar “geri dönüldüğünde hangi durum duruyor olmalı?” sorusudur.
Rotayı değiştirmek bu dengeyi değiştirir. Değiştirme ölçümünde eski ekran söküldü, yenisi kuruldu ve eski ekranın durumu yok oldu. Kullanıcı bir ürünün ayrıntısından başka bir ürünün ayrıntısına geçiyorsa bu çoğu zaman istenen davranıştır: iki ürünün kaydırma konumunu paylaşması anlamsızdır. Aynı davranış, kullanıcı aynı ürüne dönmek istediğinde bir kayıptır. Hangi davranışın doğru olduğunu akıbet söylemez; akıbet yalnız hangisinin olduğunu söyler.
Sınırlayıcı Ölçüm
Yığında alttaki ekranın korunan çıkması gezinmenin kendisinden değil, modelin bir varsayımından gelir: UK1, alttaki ekranın özelliğine örtüldüğünü yazmıyor. Bir kitaplık alttaki ekrana ustte gibi bir özellik taşısaydı, aynı itme başka bir akıbet verirdi. Aşağıda aynı itme iki ağaç kuruluşuyla ölçülür.
// main.mjs import { el, reconcile, printOps, countFates } from "./fate.mjs"; import { renderStack } from "./stack.mjs"; function renderMarked(routes) { return el("Stack", {}, routes.map((name, i) => el("Screen", { ustte: i === routes.length - 1 }, [el("Text", { metin: name })], name))); } const focus = (op) => op.path === "/Stack/Screen[liste]"; const plain = reconcile(renderStack(["liste"]), renderStack(["liste", "detay"])); const marked = reconcile(renderMarked(["liste"]), renderMarked(["liste", "detay"])); console.log("isaretsiz:"); printOps(plain.filter(focus)); console.log("isaretli:"); printOps(marked.filter(focus)); countFates(marked);
isaretsiz: korunan: /Stack/Screen[liste] isaretli: guncellenen: /Stack/Screen[liste] korunan: 2 guncellenen: 1 kurulan: 2 sokulen: 0 komut: 3
Aynı itme, ağacın kurulum biçimine göre /Stack/Screen[liste] düğümünü korunan ya da güncellenen yaptı. Kural ekranın örtüldüğünü görmez; yalnız ağaçtaki özelliğin değiştiğini görür. Örtülme bilgisi ağaca özellik olarak yazılıyorsa her itme alttaki ekran için bir komut doğurur. Bu yüzden kitaplığın örtülmeyi nasıl bildirdiği, gezinme maliyetinin bir parçasıdır ve kural bu kararı vermez; ders bunu düzyazıda söyler.
Yaygın Yanılgılar
- “Geri dönmek önceki ekranı yeniden kurar.” Geri dönme ölçümünde önceki ekranın düğümleri korunan çıktı; sökülen yalnızca çıkarılan rotanın ekranıydı. Yeniden kurulan bir şey olmadığı için kaydırma konumu da yerinde kalır.
- “Yığındaki her ekran ağaçtan çıkarılabilir, kullanıcı fark etmez.” Alttaki ekran ağaçtan çıkarılırsa durumu da gider. Kullanıcı geri döndüğünde ekran sıfırdan kurulur ve yarım kalan iş kaybolur.
- “Rota adı yeterli kimliktir.” Aynı ad farklı parametrelerle iki kez açılabiliyorsa ad tek başına anahtar olamaz; model bu yüzden adı ve parametreyi birlikte anahtar yapar. Anahtar eşleşmezse aynı ekran farklı bir ekran sayılır ve sökülüp yeniden kurulur.
- “Sekme ve yığın aynı geçiştir.” Yığında yeni rota eklenir ve geri dönüş yolu oluşur. Sekmede rotalar kardeştir ve aralarındaki geçiş, yukarıdaki iki seçenekten hangisi kurulduysa onun akıbetini üretir.
Özet
- Bu dersin akıbeti: korunan. Yeni rota itilince
/Stack/Screen[liste]düğümü, özellikleri ve anahtarı değişmediği için korunan çıktı. - Rota itmek yeni ekranın düğümlerini kurar, geri dönmek çıkarılan ekranın düğümlerini sökerek bu ekranın durumunu da yok eder.
- Sekme geçişi, görünmeyen sekmenin ağaçta kalıp kalmamasına göre güncellenen ya da sökülen ile kurulan üretir.
- Alttaki ekranın korunan olması, örtülmenin ağaca özellik olarak yazılmadığı varsayımına bağlıdır; yazılırsa aynı düğüm güncellenen olur.
- Kural gezinme kitaplığının iç tutumunu görmez; yalnız iki ağacı karşılaştırır.
Sonraki Adım
Bu derste gezinme ekranların hangi düğümlerde kurulup söküldüğünü belirledi. Ekranlar arasında paylaşılan bilgi ise ayrı bir sorudur: bir sepet sayısı bir ekranda değişince başka bir ekrandaki rozet de değişmelidir. Sıradaki ders, Durum Yönetimi, tek bir değişikliğin hangi düğümü güncellediğini, hangisini korunan bıraktığını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.