Ich habe ein Array von true / false / undefined-Werten, die ich als Liste von Kontrollkästchen wiedergebe.
Wenn Sie ein Array-Element in oder von true ändern, wird die Liste der Kontrollkästchen mit dem folgenden Kontrollkästchen (Index + 1) neu gerendert, das die Änderung zusammen mit dem geänderten Kontrollkästchen übernimmt.
Code:
{{#each range as |value idx|}}
<label><input type="checkbox" checked={{value}} {{action makeChange idx on="change"}}>{{idx}}: {{value}}</label><br/>
{{/each}}
Wenn ich {{#each range key="@index" as |value idx|}}
benutze, funktioniert es richtig.
Twiddle: https://ember-twiddle.com/6d63548f35f99da19cee9f58fb64db59
@andrewtimberlake scheint, als würde die Verwendung von {{#each range key="@index" as |value idx|}}
das Problem umgehen .
Aber es scheint ein Fehler zu sein, das key
dient einem anderen Zweck, https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor= jeder
Ich glaube ich weiß was hier los ist. Es ist ein Durcheinander, aber ich werde versuchen, es zu beschreiben. Viele Randfälle (Borderline-Benutzerfehler) haben dazu beigetragen, und ich bin mir nicht sicher, was ein Fehler ist / nicht ist, was und wie man einen dieser Fehler behebt.
Zunächst muss ich beschreiben, was der Parameter key
in {{#each}}
bewirkt. TL; DR Es wird versucht festzustellen, wann und ob es sinnvoll wäre, das vorhandene DOM wiederzuverwenden, anstatt nur das DOM von Grund auf neu zu erstellen.
Nehmen wir für unseren Zweck an, dass das "Berühren von DOM" (z. B. Aktualisieren des Inhalts eines Textknotens, eines Attributs, Hinzufügen oder Entfernen von Inhalten usw.) teuer ist und so weit wie möglich vermieden werden sollte.
Konzentrieren wir uns auf eine ziemlich einfache Vorlage:
<ul>
{{#each this.names as |name|}}
<li>{{name.first}} {{to-upper-case name.last}}</li>
{{/each}}
</ul>
Wenn this.names
ist ...
[
{ first: "Yehuda", last: "Katz" },
{ first: "Tom", last: "Dale" },
{ first: "Godfrey", last: "Chan" }
]
Dann wirst du bekommen ...
<ul>
<li>Yehuda KATZ</li>
<li>Tom DALE</li>
<li>Godfrey CHAN</li>
</ul>
So weit, ist es gut.
Was ist nun, wenn wir { first: "Andrew", last: "Timberlake" }
an die Liste anhängen? Wir würden erwarten, dass die Vorlage das folgende DOM erzeugt:
<ul>
<li>Yehuda KATZ</li>
<li>Tom DALE</li>
<li>Godfrey CHAN</li>
<li>Andrew TIMBERLAKE</li>
</ul>
Aber wie_?
Der naivste Weg, den {{#each}}
-Helfer zu implementieren, würde den gesamten Inhalt der Liste jedes Mal löschen, wenn sich der Inhalt der Liste ändert. Um dies zu tun, müssten Sie mindestens 23 Operationen ausführen:
<li>
Knoten<li>
Knoten einto-upper-case
-Helfer viermal aufDies scheint ... sehr unnötig und teuer. Wir wissen, dass sich die ersten drei Elemente nicht geändert haben, daher wäre es schön, wenn wir die Arbeit für diese Zeilen einfach überspringen könnten.
Eine bessere Implementierung wäre, zu versuchen, die vorhandenen Zeilen wiederzuverwenden und keine unnötigen Aktualisierungen vorzunehmen. Eine Idee wäre, die Zeilen einfach mit ihren Positionen in den Vorlagen abzugleichen. Dies ist im Wesentlichen das, was key="@index"
tut:
{ first: "Yehuda", last: "Katz" }
mit der ersten Zeile <li>Yehuda KATZ</li>
:to-upper-case
-Helfer nicht erneut aufrufen müssen, und daher kennen wir die Ausgabe dieses Helfers ("KATZ"). ) _auch_ hat sich nicht geändert, also hier nichts zu tun<li>
-Knoten einto-upper-case
-Helfer auf ("Timberlake" -> "TIMBERLAKE")Mit dieser Implementierung haben wir die Gesamtzahl der Operationen von 23 auf 5 reduziert (👋 Handbewegung über die Kosten der Vergleiche, aber für unseren Zweck gehen wir davon aus, dass sie im Vergleich zu den anderen relativ billig sind). Nicht schlecht.
Aber jetzt, was passieren würde , wenn statt _appending_ { first: "Andrew", last: "Timberlake" }
auf der Liste, wir _prepended_ es statt? Wir würden erwarten, dass die Vorlage das folgende DOM erzeugt:
<ul>
<li>Andrew TIMBERLAKE</li>
<li>Yehuda KATZ</li>
<li>Tom DALE</li>
<li>Godfrey CHAN</li>
</ul>
Aber wie_?
{ first: "Andrew", last: "Timberlake" }
mit der ersten Zeile <li>Yehuda KATZ</li>
:to-upper-case
-Helfer erneut auf{ first: "Yehuda", last: "Katz" }
mit der zweiten Zeile <li>Tom DALE</li>
, einer weiteren 3 Operation{ first: "Tom", last: "Dale" }
mit der zweiten Zeile <li>Godfrey CHAN</li>
, einer weiteren 3 Operation<li>
-Knoten einto-upper-case
-Helfer auf ("Chan" -> "CHAN")Das sind 14 Operationen. Autsch!
Das schien unnötig, da wir konzeptionell immer noch nur ein einzelnes Objekt im Array ändern (einfügen), unabhängig davon, ob wir vor- oder anhängen. Optimalerweise sollten wir in der Lage sein, diesen Fall genauso gut zu behandeln wie im Anhangsszenario.
Hier kommt key="@identity"
ins Spiel. Anstatt sich auf die Reihenfolge der Elemente im Array zu verlassen, verwenden wir deren JavaScript-Objektidentität ( ===
):
{ first: "Andrew", last: "Timberlake" }
übereinstimmen ( ===
). Da nichts gefunden wurde, fügen Sie eine neue Zeile ein (voranstellen):<li>
-Knoten einto-upper-case
-Helfer auf ("Timberlake" -> "TIMBERLAKE"){ first: "Yehuda", last: "Katz" }
übereinstimmen ( ===
). Gefunden <li>Yehuda KATZ</li>
:to-upper-case
-Helfer nicht erneut aufrufen müssen, und daher kennen wir die Ausgabe dieses Helfers ("KATZ"). ) _auch_ hat sich nicht geändert, also hier nichts zu tunDamit sind wir wieder bei den optimalen 5 Operationen.
Auch dies ist eine Handbewegung über die Vergleiche und Buchhaltungskosten. In der Tat sind diese auch nicht frei, und in diesem sehr einfachen Beispiel sind sie es möglicherweise nicht wert. Stellen Sie sich jedoch vor, die Liste ist groß und jede Zeile ruft eine komplizierte Komponente auf (mit vielen Hilfsprogrammen, berechneten Eigenschaften, Unterkomponenten usw.). Stellen Sie sich zum Beispiel den LinkedIn-Newsfeed vor. Wenn wir nicht die richtigen Zeilen mit den richtigen Daten abgleichen, können die Argumente Ihrer Komponenten möglicherweise stark abwandern und verursachen viel mehr DOM-Aktualisierungen, als Sie sonst erwarten würden. Es gibt auch Probleme beim Abgleichen der falschen DOM-Elemente und beim Verlieren des DOM-Status, z. B. Cursorposition und Textauswahlstatus.
Insgesamt sind die zusätzlichen Vergleichs- und Buchhaltungskosten in einer realen App die meiste Zeit leicht wert. Da key="@identity"
die Standardeinstellung in Ember ist und in fast allen Fällen gut funktioniert, müssen Sie sich normalerweise keine Gedanken über das Setzen des Arguments key
, wenn Sie {{#each}}
.
Aber warte, es gibt ein Problem. Was ist mit diesem Fall?
const YEHUDA = { first: "Yehuda", last: "Katz" };
const TOM = { first: "Tom", last: "Dale" };
const GODFREY = { first: "Godfrey", last: "Chan" };
this.list = [
YEHUDA,
TOM,
GODFREY,
TOM, // duplicate
YEHUDA, // duplicate
YEHUDA, // duplicate
YEHUDA // duplicate
];
Das Problem hierbei ist, dass dasselbe Objekt mehrmals in derselben Liste erscheinen könnte. Dies bricht unseren naiven @identity
-Algorithmus, insbesondere den Teil, in dem wir sagten "Finde eine vorhandene Zeile, deren Daten übereinstimmen ( ===
) ..." - dies funktioniert nur, wenn die Beziehung zwischen Daten und DOM 1 ist : 1, was in diesem Fall nicht zutrifft. Dies mag in der Praxis unwahrscheinlich erscheinen, aber als Rahmen müssen wir damit umgehen.
Um dies zu vermeiden, verwenden wir eine Art Hybridansatz, um diese Kollisionen zu behandeln. Intern sieht die Zuordnung von Schlüsseln zu DOM ungefähr so aus:
"YEHUDA" => <li>Yehuda...</li>
"TOM" => <li>Tom...</li>
"GODFREY" => <li>Godfrey...</li>
"TOM-1" => <li>Tom...</li>
"YEHUDA-1" => <li>Yehuda...</li>
"YEHUDA-2" => <li>Yehuda...</li>
"YEHUDA-3" => <li>Yehuda...</li>
Zum größten Teil ist dies ziemlich selten, und wenn es auftaucht, funktioniert dies die meiste Zeit gut genug. Wenn dies aus irgendeinem Grund nicht funktioniert, können Sie immer einen Schlüsselpfad verwenden (oder den noch erweiterten Schlüsselmechanismus in RFC 321 ).
Nach all dem Gerede sind wir nun bereit, das Szenario im Twiddle zu betrachten.
Im Wesentlichen haben wir mit dieser Liste begonnen: [undefined, undefined, undefined, undefined, undefined]
.
Hinweis ohne Bezug:
Array(5)
ist _nicht_ dasselbe wie[undefined, undefined, undefined, undefined, undefined]
. Es erzeugt ein "löchriges Array", das Sie generell vermeiden sollten. Es hat jedoch nichts mit diesem Fehler zu tun, denn wenn Sie auf die "Löcher" zugreifen, erhalten Sie tatsächlich einundefined
zurück. Also nur für unseren sehr engen Zweck sind sie gleich.
Da wir den Schlüssel nicht angegeben haben, verwendet Ember standardmäßig @identity
. Da es sich um Kollisionen handelt, haben wir außerdem Folgendes festgestellt:
"undefined" => <input ...> 0: ...,
"undefined-1" => <input ...> 1: ...,
"undefined-2" => <input ...> 2: ...,
"undefined-3" => <input ...> 3: ...,
"undefined-4" => <input ...> 4: ...
Angenommen, wir aktivieren das erste Kontrollkästchen:
{{action}}
abgefangen und erneut an die Methode makeChange
gesendet wird[true, undefined, undefined, undefined, undefined]
.Wie wird das DOM aktualisiert?
true
übereinstimmen ( ===
). Da nichts gefunden wurde, fügen Sie eine neue Zeile <input checked=true ...>0: true...
ein (voranstellen)undefined
übereinstimmen ( ===
). Gefunden <input ...>0: ...
(zuvor die ERSTE Zeile):{{idx}}
auf 1
undefined
übereinstimmen ( ===
). Da dies das zweite Mal ist, dass wir undefined
, lautet der interne Schlüssel undefined-1
. Wir haben also <input ...>1: ...
(zuvor die ZWEITE Zeile):{{idx}}
auf 2
undefined-2
und undefined-3
undefined-4
(da sich nach dem Update eine Zeile weniger undefined
im Array befindet).Dies erklärt also, wie wir die Ausgabe erhalten haben, die Sie im Twiddle hatten. Im Wesentlichen wurden alle DOM-Zeilen um eins nach unten verschoben, und oben wurde eine neue eingefügt, während die {{idx}}
für den Rest aktualisiert werden.
Der wirklich unerwartete Teil ist 2.2. Obwohl das erste Kontrollkästchen (das angeklickte) um eine Zeile nach unten auf die zweite Position verschoben wurde, hätten Sie wahrscheinlich erwartet, dass Ember seine checked
-Eigenschaft in true
geändert hat, und Da der gebundene Wert undefiniert ist, können Sie erwarten, dass Ember ihn wieder in false
ändert und das Kontrollkästchen deaktiviert.
Aber so funktioniert es nicht. Wie eingangs erwähnt, ist der Zugriff auf DOM teuer. Dies beinhaltet das Lesen aus dem DOM. Wenn wir bei jedem Update den neuesten Wert aus dem DOM für unsere Vergleiche lesen müssten, würde dies den Zweck unserer Optimierungen ziemlich zunichte machen. Um dies zu vermeiden, haben wir uns daher an den letzten Wert erinnert, den wir in das DOM geschrieben haben, und den aktuellen Wert mit dem zwischengespeicherten Wert verglichen, ohne ihn aus dem DOM zurücklesen zu müssen. Nur wenn es einen Unterschied gibt, schreiben wir den neuen Wert in das DOM (und zwischenspeichern ihn für das nächste Mal). In diesem Sinne teilen wir den gleichen "virtuellen DOM" -Ansatz, aber wir tun dies nur an den Blattknoten und nicht an der "Baumstruktur" des gesamten DOM.
Also, TL; DR, "Binden" der checked
-Eigenschaft (oder der value
-Eigenschaft eines Textfelds usw.) funktioniert nicht wirklich so, wie Sie es erwarten. Stellen Sie sich vor, Sie haben <div>{{this.name}}</div>
gerendert und das textContent
des div
Elements manuell mit jQuery
oder mit dem Chrome Inspector aktualisiert. Sie hätten nicht erwartet, dass Ember das bemerkt und this.name
für Sie aktualisiert. Dies ist im Grunde das Gleiche: Da die Aktualisierung der Eigenschaft checked
außerhalb von Ember erfolgte (über das Standardverhalten des Browsers für das Kontrollkästchen), wird Ember nichts davon wissen.
Aus diesem Grund gibt es den {{input}}
-Helfer. Es muss die relevanten Ereignis-Listener im zugrunde liegenden HTML-Element registrieren und die Operationen in die entsprechende Eigenschaftsänderung einfließen lassen, damit die interessierten Parteien (z. B. die Rendering-Ebene) benachrichtigt werden können.
Ich bin mir nicht sicher, wo uns das lässt. Ich verstehe, warum dies überraschend ist, aber ich neige dazu zu sagen, dass dies eine Reihe unglücklicher Benutzerfehler ist. Vielleicht sollten wir uns dagegen wehren, diese Eigenschaften an Eingabeelemente zu binden?
Relevanter Code:
https://github.com/emberjs/ember.js/blob/c24bc23e4139c90c8d8d96c4234d9c0c19e5c594/packages/@ember/ -internals / glimmer / lib / utils / iterable.ts # L390-L391
https://github.com/emberjs/ember.js/blob/c24bc23e4139c90c8d8d96c4234d9c0c19e5c594/packages/@ember/ -internals / glimmer / lib / utils / iterable.ts # L436-L445
https://github.com/emberjs/ember.js/blob/c24bc23e4139c90c8d8d96c4234d9c0c19e5c594/packages/@ember/ -internals / glimmer / lib / utils / iterable.ts # L451-L466
@chancancode - danke für die erstaunliche Erklärung. Bedeutet das, dass <input ... >
niemals verwendet werden sollte, sondern nur {{input ...}}
, um alle derartigen Fehler zu vermeiden?
@ boris-petrov Es kann einige begrenzte Fälle geben, in denen dies akzeptabel ist. Zum Beispiel ein schreibgeschütztes Textfeld für "Kopieren Sie diese URL in Ihre Zwischenablage", oder Sie können das Eingabeelement + {{action}}
, um das abzufangen DOM-Ereignis und reflektieren die Eigenschaftsaktualisierungen manuell (was das Twiddle versucht hat, außer dass es auch zu einer Kollision von @identity
), aber ja, irgendwann implementieren Sie nur {{input}}
und Behandlung aller Randfälle, die bereits für Sie bearbeitet wurden. Ich denke, es ist wahrscheinlich fair zu sagen, dass Sie die meiste, wenn nicht die ganze Zeit nur {{input}}
.
Dies hätte diesen Fall jedoch nicht "behoben", wenn es zu Kollisionen mit den Schlüsseln gekommen wäre. Siehe https://ember-twiddle.com/0f2369021128e2ae0c445155df5bb034?openFiles=templates.application.hbs%2C
Deshalb habe ich gesagt, ich bin mir zu 100% nicht sicher, was ich dagegen tun soll. Einerseits stimme ich zu, dass es überraschend und unerwartet ist, andererseits ist diese Art von Kollision in echten Apps ziemlich selten und deshalb ist das Argument "Schlüssel" anpassbar (dies ist ein Fall, in dem der Standardschlüssel "@identity" verwendet wird) Funktion ist nicht gut genug ™, weshalb diese Funktion vorhanden ist).
@chancancode - das erinnert mich an ein anderes Problem, das ich vor einiger Zeit geöffnet habe . Glaubst du, dass es dort etwas Ähnliches gibt? Die Antwort, die ich dort erhalten habe (über die Notwendigkeit, beim Festlegen von Array-Elementen replace
anstelle von set
verwenden), erscheint mir immer noch seltsam.
@ Boris-Petrov Ich glaube nicht, dass es verwandt ist
Hallo, wir verwenden sortablejs für ziehbare Listen mit Glut. Bitte überprüfen Sie diese Demo, um jedes Problem zu reproduzieren.
Schritt:
Sie können sehen, dass das gezogene Element im Dom-Baum bleibt.
Wenn Sie das Element jedoch an eine andere Position ziehen (nicht an das letzte Element), scheint dies gut zu funktionieren.
Hilfreichster Kommentar
Ich glaube ich weiß was hier los ist. Es ist ein Durcheinander, aber ich werde versuchen, es zu beschreiben. Viele Randfälle (Borderline-Benutzerfehler) haben dazu beigetragen, und ich bin mir nicht sicher, was ein Fehler ist / nicht ist, was und wie man einen dieser Fehler behebt.
Major 🔑
Zunächst muss ich beschreiben, was der Parameter
key
in{{#each}}
bewirkt. TL; DR Es wird versucht festzustellen, wann und ob es sinnvoll wäre, das vorhandene DOM wiederzuverwenden, anstatt nur das DOM von Grund auf neu zu erstellen.Nehmen wir für unseren Zweck an, dass das "Berühren von DOM" (z. B. Aktualisieren des Inhalts eines Textknotens, eines Attributs, Hinzufügen oder Entfernen von Inhalten usw.) teuer ist und so weit wie möglich vermieden werden sollte.
Konzentrieren wir uns auf eine ziemlich einfache Vorlage:
Wenn
this.names
ist ...Dann wirst du bekommen ...
So weit, ist es gut.
Anhängen eines Elements an die Liste
Was ist nun, wenn wir
{ first: "Andrew", last: "Timberlake" }
an die Liste anhängen? Wir würden erwarten, dass die Vorlage das folgende DOM erzeugt:Aber wie_?
Der naivste Weg, den
{{#each}}
-Helfer zu implementieren, würde den gesamten Inhalt der Liste jedes Mal löschen, wenn sich der Inhalt der Liste ändert. Um dies zu tun, müssten Sie mindestens 23 Operationen ausführen:<li>
Knoten<li>
Knoten einto-upper-case
-Helfer viermal aufDies scheint ... sehr unnötig und teuer. Wir wissen, dass sich die ersten drei Elemente nicht geändert haben, daher wäre es schön, wenn wir die Arbeit für diese Zeilen einfach überspringen könnten.
🔑 @index
Eine bessere Implementierung wäre, zu versuchen, die vorhandenen Zeilen wiederzuverwenden und keine unnötigen Aktualisierungen vorzunehmen. Eine Idee wäre, die Zeilen einfach mit ihren Positionen in den Vorlagen abzugleichen. Dies ist im Wesentlichen das, was
key="@index"
tut:{ first: "Yehuda", last: "Katz" }
mit der ersten Zeile<li>Yehuda KATZ</li>
:1.1. "Yehuda" === "Yehuda", nichts zu tun
1.2. (Der Speicherplatz enthält keine dynamischen Daten, daher ist kein Vergleich erforderlich.)
1.3. "Katz" === "Katz", da Helfer "rein" sind, wissen wir, dass wir den
to-upper-case
-Helfer nicht erneut aufrufen müssen, und daher kennen wir die Ausgabe dieses Helfers ("KATZ"). ) _auch_ hat sich nicht geändert, also hier nichts zu tun3.1. Fügen Sie einen
<li>
-Knoten ein3.2. Fügen Sie einen Textknoten ein ("Andrew")
3.3. Fügen Sie einen Textknoten ein (das Leerzeichen)
3.4. Rufen Sie den
to-upper-case
-Helfer auf ("Timberlake" -> "TIMBERLAKE")3.5. Fügen Sie einen Textknoten ein ("TIMBERLAKE")
Mit dieser Implementierung haben wir die Gesamtzahl der Operationen von 23 auf 5 reduziert (👋 Handbewegung über die Kosten der Vergleiche, aber für unseren Zweck gehen wir davon aus, dass sie im Vergleich zu den anderen relativ billig sind). Nicht schlecht.
Ein Element der Liste voranstellen
Aber jetzt, was passieren würde , wenn statt _appending_
{ first: "Andrew", last: "Timberlake" }
auf der Liste, wir _prepended_ es statt? Wir würden erwarten, dass die Vorlage das folgende DOM erzeugt:Aber wie_?
{ first: "Andrew", last: "Timberlake" }
mit der ersten Zeile<li>Yehuda KATZ</li>
:1.1. "Andrew"! == "Yehuda", aktualisiere den Textknoten
1.2. (Der Speicherplatz enthält keine dynamischen Daten, daher ist kein Vergleich erforderlich.)
1.3. "Timberlake"! == "Katz", rufe den
to-upper-case
-Helfer erneut auf1.4. Aktualisieren Sie den Textknoten von "KATZ" auf "TIMBERLAKE".
{ first: "Yehuda", last: "Katz" }
mit der zweiten Zeile<li>Tom DALE</li>
, einer weiteren 3 Operation{ first: "Tom", last: "Dale" }
mit der zweiten Zeile<li>Godfrey CHAN</li>
, einer weiteren 3 Operation3.1. Fügen Sie einen
<li>
-Knoten ein3.2. Fügen Sie einen Textknoten ein ("Godfrey")
3.3. Fügen Sie einen Textknoten ein (das Leerzeichen)
3.4. Rufen Sie den
to-upper-case
-Helfer auf ("Chan" -> "CHAN")3.5. Fügen Sie einen Textknoten ein ("CHAN")
Das sind 14 Operationen. Autsch!
🔑 @identity
Das schien unnötig, da wir konzeptionell immer noch nur ein einzelnes Objekt im Array ändern (einfügen), unabhängig davon, ob wir vor- oder anhängen. Optimalerweise sollten wir in der Lage sein, diesen Fall genauso gut zu behandeln wie im Anhangsszenario.
Hier kommt
key="@identity"
ins Spiel. Anstatt sich auf die Reihenfolge der Elemente im Array zu verlassen, verwenden wir deren JavaScript-Objektidentität (===
):{ first: "Andrew", last: "Timberlake" }
übereinstimmen (===
). Da nichts gefunden wurde, fügen Sie eine neue Zeile ein (voranstellen):1.1. Fügen Sie einen
<li>
-Knoten ein1.2. Fügen Sie einen Textknoten ein ("Andrew")
1.3. Fügen Sie einen Textknoten ein (das Leerzeichen)
1.4. Rufen Sie den
to-upper-case
-Helfer auf ("Timberlake" -> "TIMBERLAKE")1.5. Fügen Sie einen Textknoten ein ("TIMBERLAKE")
{ first: "Yehuda", last: "Katz" }
übereinstimmen (===
). Gefunden<li>Yehuda KATZ</li>
:2.1. "Yehuda" === "Yehuda", nichts zu tun
2.2. (Der Speicherplatz enthält keine dynamischen Daten, daher ist kein Vergleich erforderlich.)
2.3. "Katz" === "Katz", da Helfer "rein" sind, wissen wir, dass wir den
to-upper-case
-Helfer nicht erneut aufrufen müssen, und daher kennen wir die Ausgabe dieses Helfers ("KATZ"). ) _auch_ hat sich nicht geändert, also hier nichts zu tunDamit sind wir wieder bei den optimalen 5 Operationen.
Hochskalieren
Auch dies ist eine Handbewegung über die Vergleiche und Buchhaltungskosten. In der Tat sind diese auch nicht frei, und in diesem sehr einfachen Beispiel sind sie es möglicherweise nicht wert. Stellen Sie sich jedoch vor, die Liste ist groß und jede Zeile ruft eine komplizierte Komponente auf (mit vielen Hilfsprogrammen, berechneten Eigenschaften, Unterkomponenten usw.). Stellen Sie sich zum Beispiel den LinkedIn-Newsfeed vor. Wenn wir nicht die richtigen Zeilen mit den richtigen Daten abgleichen, können die Argumente Ihrer Komponenten möglicherweise stark abwandern und verursachen viel mehr DOM-Aktualisierungen, als Sie sonst erwarten würden. Es gibt auch Probleme beim Abgleichen der falschen DOM-Elemente und beim Verlieren des DOM-Status, z. B. Cursorposition und Textauswahlstatus.
Insgesamt sind die zusätzlichen Vergleichs- und Buchhaltungskosten in einer realen App die meiste Zeit leicht wert. Da
key="@identity"
die Standardeinstellung in Ember ist und in fast allen Fällen gut funktioniert, müssen Sie sich normalerweise keine Gedanken über das Setzen des Argumentskey
, wenn Sie{{#each}}
.Kollisionen 💥
Aber warte, es gibt ein Problem. Was ist mit diesem Fall?
Das Problem hierbei ist, dass dasselbe Objekt mehrmals in derselben Liste erscheinen könnte. Dies bricht unseren naiven
@identity
-Algorithmus, insbesondere den Teil, in dem wir sagten "Finde eine vorhandene Zeile, deren Daten übereinstimmen (===
) ..." - dies funktioniert nur, wenn die Beziehung zwischen Daten und DOM 1 ist : 1, was in diesem Fall nicht zutrifft. Dies mag in der Praxis unwahrscheinlich erscheinen, aber als Rahmen müssen wir damit umgehen.Um dies zu vermeiden, verwenden wir eine Art Hybridansatz, um diese Kollisionen zu behandeln. Intern sieht die Zuordnung von Schlüsseln zu DOM ungefähr so aus:
Zum größten Teil ist dies ziemlich selten, und wenn es auftaucht, funktioniert dies die meiste Zeit gut genug. Wenn dies aus irgendeinem Grund nicht funktioniert, können Sie immer einen Schlüsselpfad verwenden (oder den noch erweiterten Schlüsselmechanismus in RFC 321 ).
Zurück zum "🐛"
Nach all dem Gerede sind wir nun bereit, das Szenario im Twiddle zu betrachten.
Im Wesentlichen haben wir mit dieser Liste begonnen:
[undefined, undefined, undefined, undefined, undefined]
.Da wir den Schlüssel nicht angegeben haben, verwendet Ember standardmäßig
@identity
. Da es sich um Kollisionen handelt, haben wir außerdem Folgendes festgestellt:Angenommen, wir aktivieren das erste Kontrollkästchen:
{{action}}
abgefangen und erneut an die MethodemakeChange
gesendet wird[true, undefined, undefined, undefined, undefined]
.Wie wird das DOM aktualisiert?
true
übereinstimmen (===
). Da nichts gefunden wurde, fügen Sie eine neue Zeile<input checked=true ...>0: true...
ein (voranstellen)undefined
übereinstimmen (===
). Gefunden<input ...>0: ...
(zuvor die ERSTE Zeile):2.1. Aktualisieren Sie den Textknoten
{{idx}}
auf1
2.2. Ansonsten hat sich, soweit Ember das beurteilen kann, in dieser Zeile nichts anderes geändert, nichts anderes zu tun
undefined
übereinstimmen (===
). Da dies das zweite Mal ist, dass wirundefined
, lautet der interne Schlüsselundefined-1
. Wir haben also<input ...>1: ...
(zuvor die ZWEITE Zeile):3.1. Aktualisieren Sie den Textknoten
{{idx}}
auf2
3.2. Ansonsten hat sich, soweit Ember das beurteilen kann, in dieser Zeile nichts anderes geändert, nichts anderes zu tun
undefined-2
undundefined-3
undefined-4
(da sich nach dem Update eine Zeile wenigerundefined
im Array befindet).Dies erklärt also, wie wir die Ausgabe erhalten haben, die Sie im Twiddle hatten. Im Wesentlichen wurden alle DOM-Zeilen um eins nach unten verschoben, und oben wurde eine neue eingefügt, während die
{{idx}}
für den Rest aktualisiert werden.Der wirklich unerwartete Teil ist 2.2. Obwohl das erste Kontrollkästchen (das angeklickte) um eine Zeile nach unten auf die zweite Position verschoben wurde, hätten Sie wahrscheinlich erwartet, dass Ember seine
checked
-Eigenschaft intrue
geändert hat, und Da der gebundene Wert undefiniert ist, können Sie erwarten, dass Ember ihn wieder infalse
ändert und das Kontrollkästchen deaktiviert.Aber so funktioniert es nicht. Wie eingangs erwähnt, ist der Zugriff auf DOM teuer. Dies beinhaltet das Lesen aus dem DOM. Wenn wir bei jedem Update den neuesten Wert aus dem DOM für unsere Vergleiche lesen müssten, würde dies den Zweck unserer Optimierungen ziemlich zunichte machen. Um dies zu vermeiden, haben wir uns daher an den letzten Wert erinnert, den wir in das DOM geschrieben haben, und den aktuellen Wert mit dem zwischengespeicherten Wert verglichen, ohne ihn aus dem DOM zurücklesen zu müssen. Nur wenn es einen Unterschied gibt, schreiben wir den neuen Wert in das DOM (und zwischenspeichern ihn für das nächste Mal). In diesem Sinne teilen wir den gleichen "virtuellen DOM" -Ansatz, aber wir tun dies nur an den Blattknoten und nicht an der "Baumstruktur" des gesamten DOM.
Also, TL; DR, "Binden" der
checked
-Eigenschaft (oder dervalue
-Eigenschaft eines Textfelds usw.) funktioniert nicht wirklich so, wie Sie es erwarten. Stellen Sie sich vor, Sie haben<div>{{this.name}}</div>
gerendert und dastextContent
desdiv
Elements manuell mitjQuery
oder mit dem Chrome Inspector aktualisiert. Sie hätten nicht erwartet, dass Ember das bemerkt undthis.name
für Sie aktualisiert. Dies ist im Grunde das Gleiche: Da die Aktualisierung der Eigenschaftchecked
außerhalb von Ember erfolgte (über das Standardverhalten des Browsers für das Kontrollkästchen), wird Ember nichts davon wissen.Aus diesem Grund gibt es den
{{input}}
-Helfer. Es muss die relevanten Ereignis-Listener im zugrunde liegenden HTML-Element registrieren und die Operationen in die entsprechende Eigenschaftsänderung einfließen lassen, damit die interessierten Parteien (z. B. die Rendering-Ebene) benachrichtigt werden können.Ich bin mir nicht sicher, wo uns das lässt. Ich verstehe, warum dies überraschend ist, aber ich neige dazu zu sagen, dass dies eine Reihe unglücklicher Benutzerfehler ist. Vielleicht sollten wir uns dagegen wehren, diese Eigenschaften an Eingabeelemente zu binden?