Anahtarlar
Anahtarlar Nedir?
Anahtarlar, takip edilmesi gereken öğelerin kimliklerini temsil eder. Bunları özel key
özelliği ile element, component ve fragment vnode'larına ekleyebilirsiniz ve kullanıldığında şöyle görünürler:
m('.user', { key: user.id }, [
/* ... */
]);
Birkaç durumda faydalıdırlar:
- Model verilerini veya state'li verileri işlerken, yerel durumu doğru alt ağaca bağlamak için anahtarlara ihtiyacınız vardır.
- CSS kullanarak birden çok bitişik düğümü bağımsız olarak canlandırırken ve bunlardan herhangi birini ayrı ayrı kaldırabiliyorsanız, animasyonların doğru elementlere uygulanması ve beklenmedik atlamaların önlenmesi için anahtarlara ihtiyacınız vardır.
- Bir alt ağacı zorla yeniden başlatmanız gerektiğinde, bir anahtar eklemeniz ve ardından yeniden başlatmak istediğinizde bu anahtarı değiştirip yeniden render etmeniz gerekir.
Anahtar Kısıtlamaları
Önemli: Tüm fragmentler için, çocukları ya yalnızca anahtar niteliğine sahip vnode'lar (anahtarlı fragment) ya da yalnızca anahtar niteliğine sahip olmayan vnode'lar (anahtarsız fragment) içermelidir. Anahtar niteliği, yalnızca özellikleri destekleyen vnode'larda, yani element, component ve fragment vnode'larında bulunabilir. null
, undefined
ve dizeler gibi diğer vnode'lar herhangi bir özelliğe sahip olamazlar, bu nedenle anahtar niteliğine sahip olamazlar ve bu nedenle anahtarlı fragmentlerde kullanılamazlar.
Bunun anlamı, [m(".foo", {key: 1}), null]
ve ["foo", m(".bar", {key: 2})]
gibi kullanımlar hatalı olacaktır. Doğru kullanımlar ise [m(".foo", {key: 1}), m(".bar", {key: 2})]
ve [m(".foo"), null]
şeklindedir. Bunu unutursanız, açıklayıcı ve yardımcı bir hata alırsınız.
Görüntüleme Listelerinde Model Verilerini Bağlama
Listeleri, özellikle düzenlenebilir listeleri işlerken, genellikle düzenlenebilir görevler ve benzeri şeylerle uğraşırsınız. Bunların state'leri ve kimlikleri vardır ve Mithril.js'nin bunları izlemesi için ihtiyaç duyduğu bilgileri vermeniz gerekir.
Yorum yapılabilen ve bildirme gibi nedenlerle gizlenebilen gönderilerden oluşan basit bir sosyal medya akışı olduğunu varsayalım.
// `User` ve `ComposeWindow` kısalık açısından çıkarıldı
function CommentCompose() {
return {
view: function (vnode) {
var post = vnode.attrs.post;
return m(ComposeWindow, {
placeholder: 'Yorumunuzu yazın...',
submit: function (text) {
return Model.addComment(post, text);
},
});
},
};
}
function Comment() {
return {
view: function (vnode) {
var comment = vnode.attrs.comment;
return m(
'.comment',
m(User, { user: comment.user }),
m('.comment-body', comment.text),
m(
'a.comment-hide',
{
onclick: function () {
Model.hideComment(comment).then(m.redraw);
},
},
"Bunu beğenmedim"
)
);
},
};
}
function PostCompose() {
return {
view: function (vnode) {
var comment = vnode.attrs.comment;
return m(ComposeWindow, {
placeholder: 'Gönderinizi yazın...',
submit: Model.createPost,
});
},
};
}
function Post(vnode) {
var showComments = false;
var commentsFetched = false;
return {
view: function (vnode) {
var post = vnode.attrs.post;
var comments = showComments ? Model.getComments(post) : null;
return m(
'.post',
m(User, { user: post.user }),
m('.post-body', post.text),
m(
'.post-meta',
m(
'a.post-comment-count',
{
onclick: function () {
if (!showComments && !commentsFetched) {
commentsFetched = true;
Model.fetchComments(post).then(m.redraw);
}
showComments = !showComments;
},
},
post.commentCount,
' yorum',
post.commentCount === 1 ? '' : 's'
),
m(
'a.post-hide',
{
onclick: function () {
Model.hidePost(post).then(m.redraw);
},
},
"Bunu beğenmedim"
)
),
showComments
? m(
'.post-comments',
comments == null
? m('.comment-list-loading', 'Yükleniyor...')
: [
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { comment: comment });
})
),
m(CommentCompose, { post: post }),
]
)
: null
);
},
};
}
function Feed() {
Model.fetchPosts().then(m.redraw);
return {
view: function () {
var posts = Model.getPosts();
return m(
'.feed',
m('h1', 'Akış'),
posts == null
? m('.post-list-loading', 'Yükleniyor...')
: m(
'.post-view',
m(PostCompose),
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
)
)
);
},
};
}
Bu örnek, birçok işlevi içermektedir. Şimdi iki önemli noktaya odaklanalım:
// `Feed` bileşeninde
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
);
// `Post` bileşeninde
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { comment: comment });
})
);
Bunların her biri, Mithril.js'nin hakkında hiçbir fikrinin olmadığı ilişkili state'e sahip bir alt ağaca atıfta bulunur. (Mithril.js yalnızca vnode'lar ile etkileşim kurar.) Bunları anahtarsız bıraktığınızda, beklenmedik sorunlar ortaya çıkabilir. Örneğin, yorumları göstermek için "N yorum" üzerine tıklayın, yorum kutusuna bir şeyler yazın ve ardından yukarıdaki bir gönderide "Bunu beğenmedim" seçeneğine tıklayın. İşte üzerinde deneyebileceğiniz canlı bir demo, mock bir modelle birlikte. (Not: Edge veya IE'deyseniz, bağlantının karma uzunluğu nedeniyle bağlantının hash uzunluğu nedeniyle sorunlarla karşılaşabilirsiniz.)
Ancak, beklenen davranış yerine, uygulama hatalı çalışır: açtığınız yorum listesi kapanır ve yorumların yüklendiği sanılmasına rağmen, sonraki gönderi sürekli olarak "Yükleniyor..." mesajını gösterir. Bunun nedeni, yorumların geç yükleme (lazy loading) ile yüklenmesi ve her seferinde aynı yorumun geçtiğini varsayıyor olmalarıdır.
Bunun nedeni, Mithril.js'nin anahtarsız fragmentleri nasıl işlediğidir: bunları çok basit bir şekilde yinelemeli olarak tek tek işler. Yani bu durumda, fark şöyle görünebilir:
- Önce:
A, B, C, D, E
- Değiştirilmiş:
A, B, C -> D, D -> E, E -> (kaldırıldı)
Ve bileşen aynı kaldığı için (her zaman Comment
), yalnızca props değişir ve değiştirilmez.
Bu hatayı düzeltmek için, Mithril.js'nin sorunu düzeltmek için gerekirse state'i potansiyel olarak taşıması gerektiğini bilmesi için basitçe bir anahtar eklersiniz. Bu bağlantıda, sorunun anahtarlar ile çözüldüğü çalışan bir örneği bulabilirsiniz.
// `Feed` bileşeninde
m(
'.post-list',
posts.map(function (post) {
return m(Post, { key: post.id, post: post });
})
);
// `Post` bileşeninde
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { key: comment.id, comment: comment });
})
);
Yorumlar için, bu durumda anahtarlara teknik olarak ihtiyaç duyulmasa da, iç içe geçmiş yorumlar veya düzenleme özelliği gibi şeyler eklerseniz benzer şekilde bozulur ve bunlara anahtar eklemeniz gerekirdi.
Animasyonlu Nesne Koleksiyonlarını Sorunsuz Tutma
Bazı durumlarda, listelere, kutulara ve benzeri öğelere animasyon uygulamak isteyebilirsiniz. Basit bir örnekle başlayalım:
var colors = ['red', 'yellow', 'blue', 'gray'];
var counter = 0;
function getColor() {
var color = colors[counter];
counter = (counter + 1) % colors.length;
return color;
}
function Boxes() {
var boxes = [];
function add() {
boxes.push({ color: getColor() });
}
function remove(box) {
var index = boxes.indexOf(box);
boxes.splice(index, 1);
}
return {
view: function () {
return [
m('button', { onclick: add }, 'Kutu ekle, kaldırmak için kutuya tıklayın'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Kod basit görünse de, canlı örneği deneyerek ne olduğunu görebilirsiniz. Birkaç kutu oluşturmak için tıklayın, bir kutu seçin ve boyutunu gözlemleyin. Boyut ve dönüşün, ızgaradaki konuma değil, renk ile belirtilen kutuya bağlı olmasını bekleriz. Ancak, boyutun aniden arttığını, ancak konumla sabit kaldığını fark edeceksiniz. Bunun nedeni, öğelere anahtar vermemiz gerektiğidir.
Bu durumda, öğelere benzersiz anahtar vermek oldukça basittir: her yeni öğe için artırdığınız bir sayaç kullanın.
var colors = ['red', 'yellow', 'blue', 'gray'];
var counter = 0;
function getColor() {
var color = colors[counter];
counter = (counter + 1) % colors.length;
return color;
}
function Boxes() {
var boxes = [];
var nextKey = 0;
function add() {
boxes.push({ color: getColor() });
var key = nextKey;
nextKey++;
boxes.push({ key: key, color: getColor() });
}
function remove(box) {
var index = boxes.indexOf(box);
boxes.splice(index, 1);
}
return {
view: function () {
return [
m('button', { onclick: add }, 'Kutu ekle, kaldırmak için kutuya tıklayın'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
key: box.key,
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Düzenlenmiş örneğin nasıl farklı çalıştığını görmek için deneyebilirsiniz.
Tek Alt Öğeli Anahtarlı Parçalarla Görüntüleri Yeniden Başlatma
Modellerde ve benzeri durumlarda, durum bilgisi olan varlıklarla çalışırken, model görünümlerini anahtarlarla oluşturmak genellikle faydalıdır. Örneğin, aşağıdaki gibi bir düzeniniz olduğunu varsayalım:
function Layout() {
// ...
}
function Person() {
// ...
}
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(Layout, m(Person, { id: m.route.param('id') }));
},
},
// ...
});
Büyük olasılıkla, Person
bileşeniniz aşağıdaki gibi bir şeye benzeyecektir:
function Person(vnode) {
var personId = vnode.attrs.id;
var state = 'pending';
var person, error;
m.request('/api/person/:id', { params: { id: personId } }).then(
function (p) {
person = p;
state = 'ready';
},
function (e) {
error = e;
state = 'error';
}
);
return {
view: function () {
if (state === 'pending') return m(LoadingIcon);
if (state === 'error') {
return error.code === 404
? m('.person-missing', 'Kişi bulunamadı.')
: m('.person-error', 'Bir hata meydana geldi. Lütfen daha sonra tekrar deneyin');
}
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Değiştir'
),
m('.person-name', 'Ad: ', person.name)
// ...
);
},
};
}
Şimdi, bu bileşenden diğer kişilere bağlantı vermenin bir yolunu eklediğinizi varsayalım, örneğin bir "yönetici" alanı ekleyerek.
function Person(vnode) {
// ...
return {
view: function () {
// ...
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Değiştir'
),
m('.person-name', person.name),
// ...
m(
'.manager',
'Yönetici: ',
m(
m.route.Link,
{
href: '/person/:id',
params: { id: person.manager.id },
},
person.manager.name
)
)
// ...
);
},
};
}
Kişinin ID'si 1
ve yöneticinin ID'si 2
olduğunu varsayarsak, /person/1
adresinden /person/2
adresine geçiş yaparsınız ve aynı rotada kalırsınız. Ancak rota çözümleyici render
yöntemini kullandığınız için ağaç korunur. Bu nedenle m(Layout, m(Person, {id: "1"}))
ifadesinden m(Layout, m(Person, {id: "2"}))
ifadesine geçiş yaparsınız. Burada, Person
bileşeni değişmediği için yeniden başlatılmaz. Ancak bu durum bizim için sorunlu, çünkü yeni kullanıcı bilgileri yüklenmiyor. İşte anahtarların devreye girdiği yer burasıdır. Düzeltmek için rota çözümleyiciyi şu şekilde değiştirebiliriz:
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(
Layout,
// Daha sonra başka öğeler ekleyebilmek için bir dizi içine alın.
// Unutmayın: parçalar ya sadece anahtarlı alt öğeler içermeli
// ya da hiç anahtarlı alt öğeler içermemelidir.
[m(Person, { id: m.route.param('id'), key: m.route.param('id') })]
);
},
},
// ...
});
Sıkça Yapılan Hatalar
Anahtarlar ile ilgili olarak insanların karşılaştığı yaygın hatalar vardır. İşte bunların bazıları, neden işe yaramadıklarını anlamanıza yardımcı olmak için.
Anahtarlı Elementleri Sarmalamak
Bu iki kod parçacığı aynı şekilde çalışmaz:
users.map(function (user) {
return m('.wrapper', [m(User, { user: user, key: user.id })]);
});
users.map(function (user) {
return m('.wrapper', { key: user.id }, [m(User, { user: user })]);
});
İlki, anahtarı User
bileşenine bağlar, ancak users.map(...)
tarafından oluşturulan dış bileşen tamamen anahtarsızdır. Anahtarlı bir elementi bu şekilde sarmalamak işe yaramaz ve sonuç, listenin her değiştirilmesinde fazladan isteklerden, iç form girişlerinin durumunu kaybetmesine kadar çeşitli sorunlara yol açabilir. Ortaya çıkan davranış, gönderi listesinin bozuk örneğine benzer olacaktır, ancak durum bozulması sorunu olmadan.
İkincisi, anahtarı .wrapper
elementine bağlar ve dış parçanın anahtarlı olmasını sağlar. Bu, muhtemelen başından beri yapmak istediğiniz şeyi yapar ve bir kullanıcının kaldırılması, diğer kullanıcı örneklerinin durumuyla ilgili herhangi bir soruna neden olmaz.
Anahtarları Bileşenin İçine Koymak
Kişi örneğinde, bunun yerine şunu yaptığınızı varsayalım:
// KAÇININ
function Person(vnode) {
var personId = vnode.attrs.id;
// ...
return {
view: function () {
return m.fragment(
{ key: personId }
// görünümde daha önce sahip olduğunuz şey
);
},
};
}
Bu çalışmaz, çünkü anahtar bileşenin tamamına uygulanmaz. Sadece görünüme uygulanır ve bu nedenle verileri beklediğiniz gibi yeniden yükleyemezsiniz.
Burada kullanılan çözümü tercih edin, anahtarı bileşenin içinde değil, bileşeni kullanarak vnode'a koyun.
// TERCİH EDİN
return [m(Person, { id: m.route.param('id'), key: m.route.param('id') })];
Gereksiz Yere Elementleri Anahtarlamak
Anahtarların kendilerinin tanımlayıcılar olduğu yaygın bir yanılgıdır. Mithril.js, tüm fragment'ler için çocuklarının ya hepsinin anahtarları olması ya da hiçbirinin olmaması gerektiğini zorlar ve bunu unutursanız bir hata verir. Şu düzene sahip olduğunuzu varsayalım:
m('.page', m('.header', { key: 'header' }), m('.body'), m('.footer'));
Bu açıkça bir hata verecektir, çünkü .header
'ın bir anahtarı vardır ve .body
ve .footer
'ın her ikisinde de anahtar yoktur. Ancak mesele şu ki: bunun için anahtarlara ihtiyacınız yok. Kendinizi bunun gibi şeyler için anahtarlar kullanırken bulursanız, çözüm anahtar eklemek değil, onları kaldırmaktır. Sadece gerçekten, gerçekten ihtiyacınız varsa ekleyin. Evet, temel DOM düğümlerinin kimlikleri vardır, ancak Mithril.js'nin bunları doğru şekilde güncellemek için bu kimlikleri izlemesi gerekmez. Pratik olarak asla yapmaz. Anahtarlara yalnızca, her bir öğenin Mithril.js tarafından doğrudan izlenmeyen bir duruma sahip olduğu listelerde ihtiyaç duyulur. Bu durum, bir modelde, bir bileşende veya DOM'un kendisinde saklanabilir.
Bir son not: statik anahtarlardan kaçının. Bunlar her zaman gereksizdir. key
özelliğinizi hesaplamıyorsanız, muhtemelen yanlış bir şey yapıyorsunuzdur.
Yalıtılmış olarak tek bir anahtarlı elemente gerçekten ihtiyacınız varsa, tek çocuklu anahtarlı bir parça kullanın. Bu, sadece tek bir çocuğu olan bir dizidir; bu çocuk anahtarlı bir elementtir, örneğin [m("div", {key: foo})]
.
Anahtar Türlerini Karıştırmak
Anahtarlar, nesne özellik adları olarak okunur. Bu, 1
ve "1"
'in aynı şekilde ele alındığı anlamına gelir. Sorun yaşamamak için, mümkünse anahtar türlerini karıştırmayın. Karıştırırsanız, yinelenen anahtarlarla ve beklenmedik davranışlarla karşılaşabilirsiniz.
// KAÇININ
var things = [
{ id: '1', name: 'Kitap' },
{ id: 1, name: 'Bardak' },
];
Kesinlikle yapmanız gerekiyorsa ve bunun üzerinde kontrolünüz yoksa, türünü belirten bir önek kullanın, böylece ayırt edilebilirler.
things.map(function (thing) {
return m(
'.thing',
{ key: typeof thing.id + ':' + thing.id }
// ...
);
});
Delikli Anahtarlı Elementleri Gizlemek
null
, undefined
ve boolean'lar gibi "delikler" anahtarsız vnode'lar olarak kabul edilir, bu nedenle aşağıdaki gibi kodlar işe yaramaz:
// KAÇININ
things.map(function (thing) {
return shouldShowThing(thing)
? m(Thing, { key: thing.id, thing: thing })
: null;
});
Bunun yerine, listeyi döndürmeden önce filtreleyin; Mithril.js bunu doğru şekilde işleyecektir. Çoğu zaman, Array.prototype.filter
tam olarak ihtiyacınız olan şeydir.
// TERCİH EDİN
things
.filter(function (thing) {
return shouldShowThing(thing);
})
.map(function (thing) {
return m(Thing, { key: thing.id, thing: thing });
});
Yinelenen Anahtarlar
Fragment öğeleri için anahtarlar benzersiz olmalıdır; aksi takdirde hangi anahtarın nereye gitmesi gerektiği belirsiz ve muğlaktır. Ayrıca, elementlerin olması gerektiği gibi hareket etmemesiyle ilgili sorunlarınız da olabilir.
// KAÇININ
var things = [
{ id: '1', name: 'Kitap' },
{ id: '1', name: 'Bardak' },
];
Mithril.js, anahtarlı fragment'leri nasıl düzgün şekilde güncelleyeceğini bilmek için anahtarları dizinlere eşlerken boş bir nesne kullanır. Yinelenen bir anahtarınız olduğunda, o elementin nereye taşındığı artık net değildir ve bu nedenle Mithril.js bu durumda bozulacak ve özellikle liste değiştiyse, güncellemede beklenmedik davranışlar sergileyecektir. Mithril.js'nin eski düğümleri yenilere düzgün şekilde bağlaması için farklı anahtarlar gereklidir; bu nedenle anahtar olarak kullanmak için yerel olarak benzersiz bir şey seçmelisiniz.
Anahtarlar için Nesneler Kullanmak
Fragment öğeleri için anahtarlar, özellik anahtarları olarak ele alınır. Bu tür kullanımlar düşündüğünüz gibi çalışmayacaktır.
// KAÇININ
things.map(function (thing) {
return m(Thing, { key: thing, thing: thing });
});
Üzerinde bir toString
yöntemi varsa bu çağrılır ve döndürdüğü değere bağlı kalırsınız; bu yöntemin çağrıldığını fark etmeyebilirsiniz bile. Eğer yoksa, tüm nesneleriniz "[object Object]"
'e dönüştürülür ve böylece ciddi bir yinelenen anahtar sorununuz olur.