Keys
Was sind Keys?
Keys repräsentieren verfolgte Identitäten. Sie können sie über das spezielle Attribut key
zu Element-, Komponenten- und Fragment-VNodes hinzufügen. Die Verwendung sieht dann ungefähr so aus:
m('.user', { key: user.id }, [
/* ... */
]);
Sie sind in verschiedenen Szenarien nützlich:
- Wenn Sie Modelldaten oder andere zustandsbehaftete Daten rendern, benötigen Sie Keys, um den lokalen Zustand mit dem richtigen Teilbaum zu verknüpfen.
- Wenn Sie mehrere benachbarte Knoten unabhängig voneinander mit CSS animieren und einen davon einzeln entfernen möchten, benötigen Sie Keys, um sicherzustellen, dass die Animationen korrekt den Elementen zugeordnet bleiben und nicht unerwartet zu anderen Knoten springen.
- Wenn Sie einen Teilbaum manuell neu initialisieren müssen, fügen Sie einen Key hinzu, ändern Sie ihn und erzwingen Sie ein Neuzeichnen, wann immer Sie ihn neu initialisieren möchten.
Key-Beschränkungen
Wichtig: Innerhalb eines Fragments müssen entweder alle Kind-VNodes ein key
-Attribut besitzen (keyed fragment – Fragment mit Key) oder keiner (unkeyed fragment – Fragment ohne Key). key
-Attribute können nur auf VNodes vorhanden sein, die Attribute unterstützen, nämlich Element-, Komponenten- und Fragment-VNodes. Andere VNodes wie null
, undefined
und Strings können keine Attribute jeglicher Art haben, daher können sie keine key
-Attribute haben und können daher nicht in Keyed Fragments verwendet werden.
Das bedeutet, dass Konstruktionen wie [m(".foo", {key: 1}), null]
und ["foo", m(".bar", {key: 2})]
nicht funktionieren, aber [m(".foo", {key: 1}), m(".bar", {key: 2})]
und [m(".foo"), null]
funktionieren. Wenn Sie dies vergessen, erhalten Sie eine hilfreiche Fehlermeldung.
Verknüpfen von Modelldaten in Listen von Ansichten
Beim Rendern von Listen, insbesondere bearbeitbaren Listen, arbeitet man oft mit Elementen wie bearbeitbaren TODOs. Diese haben einen Zustand und eine Identität. Sie müssen Mithril.js die Informationen geben, die es benötigt, um sie zu verfolgen.
Nehmen wir an, wir haben eine einfache Social-Media-Post-Liste, in der Sie Beiträge kommentieren und Beiträge aus Gründen wie Meldungen ausblenden können.
// `User` und `ComposeWindow` zur Kürze weggelassen
function CommentCompose() {
return {
view: function (vnode) {
var post = vnode.attrs.post;
return m(ComposeWindow, {
placeholder: 'Schreibe deinen Kommentar...',
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);
},
},
"Ich mag das nicht"
)
);
},
};
}
function PostCompose() {
return {
view: function (vnode) {
var comment = vnode.attrs.comment;
return m(ComposeWindow, {
placeholder: 'Schreibe deinen Beitrag...',
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,
' Kommentar',
post.commentCount === 1 ? '' : 'e(n)'
),
m(
'a.post-hide',
{
onclick: function () {
Model.hidePost(post).then(m.redraw);
},
},
"Ich mag das nicht"
)
),
showComments
? m(
'.post-comments',
comments == null
? m('.comment-list-loading', 'Lade...')
: [
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', 'Lade...')
: m(
'.post-view',
m(PostCompose),
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
)
)
);
},
};
}
Wie Sie sehen, kapselt es eine Menge Funktionalität, aber ich möchte auf zwei Dinge näher eingehen:
// In der `Feed`-Komponente
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
);
// In der `Post`-Komponente
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { comment: comment });
})
);
Jeder dieser Abschnitte bezieht sich auf einen Teilbaum mit zugehörigem Zustand, von dem Mithril.js nichts weiß. (Mithril.js kennt nur VNodes, sonst nichts.) Wenn Sie diese ohne Key lassen, kann es zu seltsamen und unerwarteten Verhalten kommen. In diesem Fall versuchen Sie, auf "N Kommentare" zu klicken, um die Kommentare anzuzeigen, etwas in das Kommentarfeld unten einzugeben und dann auf "Ich mag das nicht" in einem Beitrag darüber zu klicken. Hier ist eine Live-Demo, die Sie ausprobieren können, komplett mit einem Mock-Modell. (Hinweis: Wenn Sie Edge oder IE verwenden, können aufgrund der Hash-Länge des Links Probleme auftreten.)
Statt des erwarteten Verhaltens kommt es zu unerwarteten Fehlern: Es schließt die Kommentarliste, die Sie geöffnet hatten, und der Beitrag nach dem, bei dem Sie die Kommentare geöffnet hatten, zeigt jetzt dauerhaft "Lade...", obwohl er denkt, dass er die Kommentare bereits geladen hat. Dies liegt daran, dass die Kommentare verzögert geladen werden und sie einfach davon ausgehen, dass jedes Mal derselbe Kommentar übergeben wird (was hier relativ vernünftig klingt), aber in diesem Fall ist dies nicht der Fall. Dies liegt daran, wie Mithril.js unkeyed fragments patcht: Es patcht sie einzeln iterativ auf sehr einfache Weise. In diesem Fall könnte der Diff also wie folgt aussehen:
- Vorher:
A, B, C, D, E
- Gepatcht:
A, B, C -> D, D -> E, E -> (gelöscht)
Und da die Komponente gleich bleibt (es ist immer Comment
), ändern sich nur die Attribute und sie wird nicht ersetzt.
Um diesen Fehler zu beheben, fügen Sie einfach einen Key hinzu, damit Mithril.js weiß, dass es möglicherweise den Zustand verschieben muss, um das Problem zu beheben. Hier ist ein Live-, funktionierendes Beispiel für alles, was behoben wurde.
// In der `Feed`-Komponente
m(
'.post-list',
posts.map(function (post) {
return m(Post, { key: post.id, post: post });
})
);
// In der `Post`-Komponente
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { key: comment.id, comment: comment });
})
);
Beachten Sie, dass es für die Kommentare zwar technisch gesehen auch ohne Schlüssel funktionieren würde, es aber ähnlich fehlschlagen würde, wenn Sie so etwas wie verschachtelte Kommentare oder die Möglichkeit, sie zu bearbeiten, hinzufügen würden, und Sie müssten ihnen Schlüssel hinzufügen.
Fehlerfreie Sammlungen von animierten Objekten
In bestimmten Fällen ist es erforderlich, Listen, Container oder ähnliche Elemente zu animieren. Betrachten wir zunächst diesen einfachen Code:
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 }, 'Box hinzufügen; klicke auf eine Box, um sie zu entfernen'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Auf den ersten Blick scheint alles in Ordnung zu sein, aber teste dieses Live-Beispiel. In diesem Beispiel kannst du einige Boxen erstellen, eine Box auswählen und ihre Größe beobachten. Wir möchten, dass die Größe und Drehung an die Box (gekennzeichnet durch die Farbe) und nicht an ihre Position im Raster gebunden sind. Du wirst feststellen, dass die Größe stattdessen plötzlich nach oben springt, aber an den Ort gebunden bleibt. Das bedeutet, dass wir den Boxen eindeutige Schlüssel zuweisen müssen.
In diesem Fall ist es recht einfach, eindeutige Schlüssel zu vergeben: Erstelle einfach einen Zähler, den du bei jeder Verwendung erhöhst.
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 }, 'Box hinzufügen; klicke auf eine Box, um sie zu entfernen'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
key: box.key,
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Hier ist eine aktualisierte Demo, die den Unterschied verdeutlicht.
Reinitialisierung von Ansichten mit einzelnen, schlüsselgebundenen Fragmenten
Wenn du mit zustandsbehafteten Elementen in Modellen und ähnlichem arbeitest, ist es oft hilfreich, Modell-Ansichten mit Schlüsseln zu rendern. Nehmen wir an, du hast folgendes Layout:
function Layout() {
// ...
}
function Person() {
// ...
}
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(Layout, m(Person, { id: m.route.param('id') }));
},
},
// ...
});
Deine Person
-Komponente könnte wie folgt aussehen:
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 nicht gefunden.')
: m('.person-error', 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut');
}
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Bearbeiten'
),
m('.person-name', 'Name: ', person.name)
// ...
);
},
};
}
Nehmen wir an, du hast eine Möglichkeit hinzugefügt, von dieser Komponente aus zu anderen Personen zu verlinken, beispielsweise durch Hinzufügen eines "Manager"-Felds.
function Person(vnode) {
// ...
return {
view: function () {
// ...
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Bearbeiten'
),
m('.person-name', person.name),
// ...
m(
'.manager',
'Manager: ',
m(
m.route.Link,
{
href: '/person/:id',
params: { id: person.manager.id },
},
person.manager.name
)
)
// ...
);
},
};
}
Angenommen, die ID der Person war 1
und die ID des Managers war 2
. Wenn du nun von /person/1
zu /person/2
wechselst, bleibst du auf derselben Route. Da du jedoch die Routenauflösungsmethode render
verwendet hast, wurde die Baumstruktur beibehalten und du hast lediglich von m(Layout, m(Person, {id: "1"}))
zu m(Layout, m(Person, {id: "2"}))
gewechselt. Dabei hat sich die Person
-Komponente nicht geändert, sodass sie nicht neu initialisiert wird. Dies ist jedoch problematisch, da der neue Benutzer dadurch nicht abgerufen wird. Hier kommen die Schlüssel ins Spiel. Wir könnten die Routenauflösung wie folgt ändern, um dies zu beheben:
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(
Layout,
// In ein Array einfügen, falls später weitere Elemente hinzugefügt werden.
// Beachte: Fragmente dürfen entweder nur Kindelemente mit Schlüsseln oder ausschließlich Kindelemente ohne Schlüssel enthalten.
[m(Person, { id: m.route.param('id'), key: m.route.param('id') })]
);
},
},
// ...
});
Häufige Fehler
Es gibt einige häufige Fehler, die im Zusammenhang mit Keys auftreten. Hier sind einige Beispiele, die Ihnen helfen sollen zu verstehen, warum sie nicht funktionieren.
Verschachtelung von Elementen mit Key
Diese beiden Code-Schnipsel funktionieren nicht auf die gleiche Weise:
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 })]);
});
Der erste Code bindet den Key an die User
-Komponente, aber das äußere Fragment, das von users.map(...)
erzeugt wird, ist nicht mit einem Key versehen. Das Einbetten eines Elements mit Key auf diese Weise funktioniert nicht wie erwartet. Die Folgen können von unnötigen Anfragen bei jeder Listenänderung bis hin zum Verlust des Zustands von Formularelementen innerhalb der Komponente reichen. Das resultierende Verhalten wäre ähnlich dem fehlerhaften Listen-Beispiel, jedoch ohne das Problem des Zustandsverlusts.
Der zweite Code bindet den Key an das .wrapper
-Element und stellt sicher, dass das äußere Fragment einen Key hat. Dies ist wahrscheinlich das gewünschte Verhalten, und das Entfernen eines Benutzers verursacht keine Probleme mit dem Zustand anderer Benutzerinstanzen.
Keys innerhalb der Komponente platzieren
Angenommen, Sie hätten im Personen-Beispiel Folgendes getan:
// VERMEIDEN
function Person(vnode) {
var personId = vnode.attrs.id;
// ...
return {
view: function () {
return m.fragment(
{ key: personId }
// was Sie zuvor in der Ansicht hatten
);
},
};
}
Dies funktioniert nicht, da der Key nicht für die Komponente als Ganzes gilt, sondern nur für die View. Dadurch werden die Daten nicht wie gewünscht neu geladen.
Bevorzugen Sie die im Beispiel verwendete Lösung, bei der der Key im VNode verwendet wird, der die Komponente verwendet, anstatt innerhalb der Komponente selbst.
// BEVORZUGEN
return [m(Person, { id: m.route.param('id'), key: m.route.param('id') })];
Unnötiges Verwenden von Keys
Es ist ein weit verbreitetes Missverständnis, dass Keys selbst Identitäten sind. Mithril.js erzwingt, dass die Kinder aller Fragmente entweder alle Keys haben oder alle keine Keys haben dürfen, und gibt einen Fehler aus, wenn Sie dies vergessen. Angenommen, Sie haben dieses Layout:
m('.page', m('.header', { key: 'header' }), m('.body'), m('.footer'));
Dies löst offensichtlich einen Fehler aus, da .header
einen Key hat, während .body
und .footer
keine Keys haben. Aber hier ist der Punkt: Sie brauchen keine Keys dafür. Wenn Sie feststellen, dass Sie Keys für solche Elemente verwenden, ist die Lösung nicht, Keys hinzuzufügen, sondern sie zu entfernen. Fügen Sie sie nur hinzu, wenn Sie sie wirklich, wirklich brauchen. Ja, die zugrunde liegenden DOM-Knoten haben Identitäten, aber Mithril.js muss diese Identitäten nicht verfolgen, um sie korrekt zu patchen. Das ist fast nie der Fall. Nur bei Listen, bei denen jeder Eintrag einen zugehörigen Zustand hat, den Mithril.js nicht selbst verfolgt, sei es in einem Modell, in einer Komponente oder im DOM selbst, benötigen Sie Keys.
Ein letzter Hinweis: Vermeiden Sie statische Keys. Sie sind immer unnötig. Wenn Sie Ihr key
-Attribut nicht berechnen, machen Sie wahrscheinlich etwas falsch.
Beachten Sie, dass Sie, wenn Sie wirklich ein einzelnes Element mit Key in Isolation benötigen, ein einzelnes Fragment mit Key verwenden. Es ist einfach ein Array mit einem einzelnen Kind, das ein Element mit Key ist, wie [m("div", {key: foo})]
.
Mischen von Key-Typen
Keys werden als Objektnamen interpretiert. Das bedeutet, dass 1
und "1"
identisch behandelt werden. Um Probleme zu vermeiden, sollten Sie Key-Typen nicht mischen, wenn Sie es vermeiden können. Andernfalls könnten Sie doppelte Keys und unerwartetes Verhalten erhalten.
// VERMEIDEN
var things = [
{ id: '1', name: 'Book' },
{ id: 1, name: 'Cup' },
];
Wenn Sie es unbedingt tun müssen und keine Kontrolle darüber haben, verwenden Sie ein Präfix, das den Typ angibt, damit sie eindeutig bleiben.
things.map(function (thing) {
return m(
'.thing',
{ key: typeof thing.id + ':' + thing.id }
// ...
);
});
Ausblenden von Elementen mit Key mit Platzhaltern
Platzhalter wie null
, undefined
und boolesche Werte werden als VNodes ohne Key behandelt, weshalb Code wie dieser nicht funktioniert:
// VERMEIDEN
things.map(function (thing) {
return shouldShowThing(thing)
? m(Thing, { key: thing.id, thing: thing })
: null;
});
Filtern Sie stattdessen die Liste, bevor Sie sie zurückgeben, und Mithril.js verhält sich korrekt. In den meisten Fällen ist Array.prototype.filter
genau das, was Sie brauchen, und Sie sollten es auf jeden Fall ausprobieren.
// BEVORZUGEN
things
.filter(function (thing) {
return shouldShowThing(thing);
})
.map(function (thing) {
return m(Thing, { key: thing.id, thing: thing });
});
Doppelte Keys
Keys für Fragmentelemente müssen eindeutig sein, da sonst unklar ist, welcher Key welchem Element zugeordnet werden soll. Dies kann auch dazu führen, dass sich Elemente nicht wie erwartet bewegen.
// VERMEIDEN
var things = [
{ id: '1', name: 'Book' },
{ id: '1', name: 'Cup' },
];
Mithril.js verwendet ein leeres Objekt, um Keys auf Indizes abzubilden, um zu wissen, wie Fragmente mit Keys richtig gepatcht werden. Wenn Sie einen doppelten Key haben, ist nicht mehr klar, wohin dieses Element verschoben wurde, und Mithril.js wird in diesem Fall fehlschlagen und unerwartete Dinge beim Update tun, insbesondere wenn sich die Liste geändert hat. Für Mithril.js sind unterschiedliche Keys erforderlich, um alte mit neuen Knoten richtig zu verbinden, daher müssen Sie etwas lokal Eindeutiges auswählen, das als Key verwendet werden soll.
Verwenden von Objekten für Keys
Keys für Fragmentelemente werden als Eigenschafts-Keys behandelt. Das funktioniert nicht wie erwartet.
// VERMEIDEN
things.map(function (thing) {
return m(Thing, { key: thing, thing: thing });
});
Wenn das Objekt eine toString
-Methode besitzt, würde diese aufgerufen, und das Ergebnis dieser Methode würde verwendet, möglicherweise ohne dass Sie dies bemerken. Wenn dies nicht der Fall ist, werden alle Ihre Objekte in "[object Object]"
umgewandelt, und Sie haben ein ernsthaftes Problem mit doppelten Keys.