Klíče (Keys)
Co jsou klíče?
Klíče reprezentují sledované identity. Můžete je přidat do elementů, komponent a fragment vnode pomocí speciálního atributu key
. Použití vypadá takto:
m('.user', { key: user.id }, [
/* ... */
]);
Jsou užitečné v několika scénářích:
- Při renderování dat modelu nebo jiných stavových dat, potřebujete klíče, abyste udrželi lokální stav svázaný se správným podstromem.
- Při nezávislé animaci více sousedních uzlů pomocí CSS a možnosti kterýkoli z nich odebrat, potřebujete klíče, abyste zajistili, že animace zůstanou u správných elementů a neskončí neočekávaně přeskočením na jiné uzly.
- Pokud potřebujete na vyžádání znovu inicializovat podstrom, přidejte klíč a poté jej změňte a překreslete, kdykoli jej chcete znovu inicializovat.
Omezení klíčů (Keys)
Důležité: Potomci fragmentu musí obsahovat buď výhradně vnode s atributy klíčů (keyed fragment), nebo výhradně vnode bez atributů klíčů (unkeyed fragment). Atributy klíčů mohou existovat pouze na vnode, které podporují atributy, konkrétně element, component a fragment vnodes. Ostatní vnode, jako null
, undefined
a řetězce, nemohou mít žádné atributy, a proto nemohou mít ani atributy klíčů. Nelze je tedy použít ve fragmentech s klíči.
To znamená, že příklady jako [m(".foo", {key: 1}), null]
a ["foo", m(".bar", {key: 2})]
nebudou fungovat, ale [m(".foo", {key: 1}), m(".bar", {key: 2})]
a [m(".foo"), null]
budou. Pokud na to zapomenete, zobrazí se užitečná chybová zpráva, která to vysvětlí.
Propojení dat modelu v seznamech zobrazení
Při renderování seznamů, zejména upravitelných seznamů, často pracujete s daty, jako jsou upravitelné úkoly (TODO) a podobně. Tyto položky mají stav a identitu a musíte Mithril.js poskytnout informace, které potřebuje ke sledování.
Předpokládejme, že máme jednoduchý seznam příspěvků na sociálních sítích, kde můžete komentovat příspěvky a skrývat je (například z důvodu nahlášení).
// `User` a `ComposeWindow` vynechány pro stručnost
function CommentCompose() {
return {
view: function (vnode) {
var post = vnode.attrs.post;
return m(ComposeWindow, {
placeholder: 'Napište svůj komentář...',
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);
},
},
"Skrýt"
)
);
},
};
}
function PostCompose() {
return {
view: function (vnode) {
var comment = vnode.attrs.comment;
return m(ComposeWindow, {
placeholder: 'Napište svůj příspěvek...',
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,
' komentář',
post.commentCount === 1 ? '' : (post.commentCount >= 2 && post.commentCount <= 4) ? 'e' : 'ů'
),
m(
'a.post-hide',
{
onclick: function () {
Model.hidePost(post).then(m.redraw);
},
},
"Skrýt"
)
),
showComments
? m(
'.post-comments',
comments == null
? m('.comment-list-loading', 'Načítání...')
: [
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', 'Feed'),
posts == null
? m('.post-list-loading', 'Načítání...')
: m(
'.post-view',
m(PostCompose),
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
)
)
);
},
};
}
Kód zapouzdřuje spoustu funkcí, ale zaměříme se na tyto dvě části:
// V komponentě `Feed`
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
);
// V komponentě `Post`
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { comment: comment });
})
);
Každá z nich odkazuje na podstrom s přidruženým stavem, o kterém Mithril.js nic neví. (Mithril.js ví pouze o vnode.) Pokud je necháte bez klíče, chování může být divné a neočekávané. V tomto případě zkuste kliknout na "N komentářů" pro zobrazení komentářů, psát do pole pro psaní komentářů v dolní části a poté kliknout na "Skrýt" u příspěvku nad ním. Zde je živá ukázka, kterou si můžete vyzkoušet, včetně mock modelu. Poznámka: pokud používáte Edge nebo IE, můžete narazit na problémy kvůli délce hashe odkazu.
Místo očekávaného chování dochází k chybám a neočekávaným výsledkům. Zavře se seznam komentářů, který jste měli otevřený, a příspěvek pod ním, u kterého jste měli komentáře otevřené, nyní trvale zobrazuje "Načítání...", i když už komentáře načetl. Je to proto, že komentáře jsou načítány líně a předpokládá se, že je pokaždé předán stejný komentář (což se zdá rozumné), ale v tomto případě tomu tak není. Důvodem je způsob, jakým Mithril.js upravuje fragmenty bez klíče: upravuje je jeden po druhém postupně velmi jednoduchým způsobem. V tomto případě by rozdíl mohl vypadat takto:
- Před:
A, B, C, D, E
- Upraveno:
A, B, C -> D, D -> E, E -> (odstraněno)
A protože komponenta zůstává stejná (je to vždy Comment
), mění se pouze atributy a komponenta není nahrazena.
Chcete-li tuto chybu opravit, jednoduše přidejte klíč, aby Mithril.js věděl, že má potenciálně přesunout stav, pokud je to nutné k vyřešení problému. Zde je živý, funkční příklad opraveného kódu.
// V komponentě `Feed`
m(
'.post-list',
posts.map(function (post) {
return m(Post, { key: post.id, post: post });
})
);
// V komponentě `Post`
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { key: comment.id, comment: comment });
})
);
Poznámka: Ačkoli by to pro komentáře v tomto případě technicky fungovalo i bez klíčů, podobně by se to rozbilo, pokud byste přidali něco jako vnořené komentáře nebo možnost je upravovat, a museli byste k nim klíče přidat.
Předcházení chybám v kolekcích animovaných objektů
V některých případech můžete chtít animovat seznamy, boxy a podobné prvky. Začněme s tímto jednoduchým kódem:
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 }, 'Add box, click box to remove'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Vypadá to nevinně, ale vyzkoušejte si živý příklad. V tomto příkladu klikněte na vytvoření několika boxů, vyberte box a sledujte jeho velikost. Chceme, aby velikost a rotace byly vázány na box (označený barvou), a ne na pozici v mřížce. Zjistíte, že místo toho velikost náhle skočí nahoru, ale zůstává konstantní s umístěním. To znamená, že jim musíme přiřadit unikátní klíče (anglicky key).
V tomto případě je přiřazení unikátních klíčů (anglicky key) poměrně snadné: stačí vytvořit čítač, který se při každém čtení inkrementuje.
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 }, 'Add box, click box to remove'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
key: box.key,
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Zde je opravená ukázka, se kterou si můžete vyzkoušet a podívat se, jak to funguje jinak.
Reinicializace zobrazení s fragmenty obsahujícími jednoho potomka s klíčem
Při práci se stavovými entitami v modelech je často užitečné vykreslovat pohledy modelu s klíči (anglicky key). Předpokládejme, že máte toto rozvržení:
function Layout() {
// ...
}
function Person() {
// ...
}
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(Layout, m(Person, { id: m.route.param('id') }));
},
},
// ...
});
Je pravděpodobné, že váš komponent Person
vypadá takto:
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', 'Person not found.')
: m('.person-error', 'An error occurred. Zkuste to později.');
}
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Upravit'
),
m('.person-name', 'Jméno: ', person.name)
// ...
);
},
};
}
Řekněme, že jste přidali způsob, jak se z tohoto komponentu odkazovat na jiné lidi, například přidáním pole "manažer".
function Person(vnode) {
// ...
return {
view: function () {
// ...
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Upravit'
),
m('.person-name', person.name),
// ...
m(
'.manager',
'Manažer: ',
m(
m.route.Link,
{
href: '/person/:id',
params: { id: person.manager.id },
},
person.manager.name
)
)
// ...
);
},
};
}
Za předpokladu, že ID osoby bylo 1
a ID manažera bylo 2
, přepnuli byste se z /person/1
na /person/2
a zůstali byste na stejné trase. Protože jste ale použili metodu render
resolveru trasy, strom byl zachován a vy jste se pouze změnili z m(Layout, m(Person, {id: "1"}))
na m(Layout, m(Person, {id: "2"}))
. V tomto případě se Person
nezměnil, a proto se komponent neinicializuje znovu. Ale pro náš případ je to nežádoucí, protože to znamená, že se nový uživatel nenačítá. Zde jsou klíče (anglicky key) užitečné. Mohli bychom změnit resolver trasy takto, abychom to opravili:
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(
Layout,
// Zabalte to do pole, abychom mohli později přidat další prvky.
// Pamatujte: fragmenty musí obsahovat buď pouze potomky s klíčem (anglicky *key*),
// nebo žádné potomky bez klíče (anglicky *key*).
[m(Person, { id: m.route.param('id'), key: m.route.param('id') })]
);
},
},
// ...
});
Běžné problémy
Existuje několik častých problémů, se kterými se lidé setkávají při práci s klíči. Následující body vám pomohou pochopit, proč se váš kód nechová podle očekávání.
Obalování prvků s klíčem
Následující dva úryvky kódu nemají stejnou funkčnost:
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 })]);
});
V prvním případě je klíč svázán s komponentou User
, ale vnější fragment vytvořený pomocí users.map(...)
klíč nemá. Obalení prvku s klíčem tímto způsobem nefunguje a může vést k různým problémům, od zbytečných požadavků při každé změně seznamu až po ztrátu stavu vnitřních formulářových prvků. Výsledné chování by se podobalo nefunkčnímu příkladu seznamu příspěvků, ale bez problému s poškozením stavu.
V druhém případě je klíč svázán s prvkem .wrapper
, čímž je zajištěno, že vnější fragment má klíč. To je pravděpodobně to, čeho jste chtěli dosáhnout od začátku, a odstranění uživatele nezpůsobí žádné problémy se stavem ostatních instancí uživatelů.
Umístění klíčů uvnitř komponenty
Předpokládejme, že v příkladu s osobou uděláte toto:
// VYVARUJTE SE
function Person(vnode) {
var personId = vnode.attrs.id;
// ...
return {
view: function () {
return m.fragment(
{ key: personId }
// co jste měli dříve v zobrazení
);
},
};
}
Toto nebude fungovat, protože klíč se nevztahuje na komponentu jako celek. Vztahuje se pouze na zobrazení, a proto nedochází k opětovnému načtení dat, jak jste doufali.
Doporučuje se použít řešení, které je tam uvedeno, a umístit klíč do vnode používající komponentu, spíše než uvnitř samotné komponenty.
// PREFERUJTE
return [m(Person, { id: m.route.param('id'), key: m.route.param('id') })];
Zbytečné používání klíčů
Častým omylem je, že klíče jsou samy o sobě identity. Mithril.js vyžaduje, aby potomci všech fragmentů měli buď všechny klíče, nebo žádné klíče, a pokud na to zapomenete, vyvolá chybu. Předpokládejme, že máte následující rozvržení:
m('.page', m('.header', { key: 'header' }), m('.body'), m('.footer'));
Tento kód samozřejmě vyvolá chybu, protože .header
má klíč, zatímco .body
a .footer
klíče nemají. Důležité je, že v tomto případě klíče nepotřebujete. Pokud zjistíte, že používáte klíče pro podobné případy, řešením není přidávat klíče, ale odstranit je. Přidávejte je pouze tehdy, když je opravdu, opravdu potřebujete. Ano, základní DOM uzly mají identity, ale Mithril.js nepotřebuje sledovat tyto identity, aby je správně aktualizoval. Prakticky to nikdy nedělá. Klíče potřebujete pouze u seznamů, kde má každá položka nějaký přidružený stav, který Mithril.js sám nesleduje, ať už je to v modelu, v komponentě nebo v samotném DOM.
Ještě jedna věc: vyhněte se statickým klíčům. Jsou vždy zbytečné. Pokud svůj atribut key
nevypočítáváte, pravděpodobně děláte něco špatně.
Pokud opravdu potřebujete jediný prvek s klíčem izolovaně, použijte fragment s jedním potomkem s klíčem. Jedná se o pole s jediným potomkem, který je prvek s klíčem, například [m("div", {key: foo})]
.
Míchání typů klíčů
Klíče se čtou jako názvy vlastností objektu. To znamená, že 1
a "1"
jsou považovány za identické. Pokud se chcete vyhnout problémům, nemíchejte typy klíčů, pokud je to možné. Pokud to uděláte, můžete skončit s duplicitními klíči a neočekávaným chováním.
// VYVARUJTE SE
var things = [
{ id: '1', name: 'Book' },
{ id: 1, name: 'Cup' },
];
Pokud to absolutně musíte udělat a nemáte nad tím kontrolu, použijte předponu označující typ, aby zůstaly odlišné.
things.map(function (thing) {
return m(
'.thing',
{ key: typeof thing.id + ':' + thing.id }
// ...
);
});
Skrývání prvků s klíčem s prázdnými hodnotami
Prázdné hodnoty jako null
, undefined
a booleovské hodnoty jsou považovány za vnode bez klíče, takže následující kód nebude fungovat:
// VYVARUJTE SE
things.map(function (thing) {
return shouldShowThing(thing)
? m(Thing, { key: thing.id, thing: thing })
: null;
});
Místo toho před vrácením seznamu jej vyfiltrujte a Mithril.js provede správnou aktualizaci. Ve většině případů je Array.prototype.filter
přesně to, co potřebujete, a určitě byste to měli vyzkoušet.
// PREFERUJTE
things
.filter(function (thing) {
return shouldShowThing(thing);
})
.map(function (thing) {
return m(Thing, { key: thing.id, thing: thing });
});
Duplicitní klíče
Klíče pro položky fragmentu musí být jedinečné, jinak je nejasné a nejednoznačné, kam který klíč patří. Můžete mít také problémy s prvky, které se nepohybují tak, jak by měly.
// VYVARUJTE SE
var things = [
{ id: '1', name: 'Book' },
{ id: '1', name: 'Cup' },
];
Mithril.js používá prázdný objekt k mapování klíčů na indexy, aby věděl, jak správně opravit fragmenty s klíči. Pokud máte duplicitní klíč, už není jasné, kam se daný prvek přesunul, a Mithril.js se v této situaci chová neočekávaně při aktualizaci, zvláště pokud se seznam změnil. Pro správné propojení starých a nových uzlů jsou vyžadovány odlišné klíče, takže si musíte vybrat něco lokálně jedinečného, co použijete jako klíč.
Používání objektů pro klíče
Klíče pro položky fragmentu jsou považovány za klíče vlastností. Použití objektů jako klíčů nebude fungovat podle očekávání.
// VYVARUJTE SE
things.map(function (thing) {
return m(Thing, { key: thing, thing: thing });
});
Pokud má objekt metodu toString
, bude zavolána a budete závislí na tom, co vrátí. Možná si ani neuvědomíte, že je tato metoda volána. Pokud ne, všechny vaše objekty se převedou na řetězec "[object Object]"
a budete mít problém s duplicitním klíčem.