Ember.js: #jeder Fehler beim erneuten Rendern

Erstellt am 14. Sept. 2018  ·  8Kommentare  ·  Quelle: emberjs/ember.js

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

embereach

Bug Has Reproduction Rendering

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:

<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.

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:

<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:

  • Entfernen Sie 3 <li> Knoten
  • Fügen Sie 4 <li> Knoten ein
  • Fügen Sie 12 Textknoten ein (einen für den Vornamen, einen für den Zwischenraum und einen für den Nachnamen, mal 4 Zeilen).
  • Rufen Sie den to-upper-case -Helfer viermal auf

Dies 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:

  1. Vergleichen Sie das erste Objekt { 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 tun
  2. Ebenso nichts für Zeile 2 und 3 zu tun
  3. Es gibt keine vierte Zeile im DOM. Fügen Sie daher eine neue ein
    3.1. Fügen Sie einen <li> -Knoten ein
    3.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:

<ul>
  <li>Andrew TIMBERLAKE</li>
  <li>Yehuda KATZ</li>
  <li>Tom DALE</li>
  <li>Godfrey CHAN</li>
</ul>

Aber wie_?

  1. Vergleichen Sie das erste Objekt { 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 auf
    1.4. Aktualisieren Sie den Textknoten von "KATZ" auf "TIMBERLAKE".
  2. Vergleichen Sie das zweite Objekt { first: "Yehuda", last: "Katz" } mit der zweiten Zeile <li>Tom DALE</li> , einer weiteren 3 Operation
  3. Vergleichen Sie das zweite Objekt { first: "Tom", last: "Dale" } mit der zweiten Zeile <li>Godfrey CHAN</li> , einer weiteren 3 Operation
  4. Es gibt keine vierte Zeile im DOM. Fügen Sie daher eine neue ein
    3.1. Fügen Sie einen <li> -Knoten ein
    3.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 ( === ):

  1. Suchen Sie eine vorhandene Zeile, deren Daten mit dem ersten Objekt { 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 ein
    1.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")
  2. Suchen Sie eine vorhandene Zeile, deren Daten mit dem zweiten Objekt { 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 tun
  3. Ebenso nichts für Toms und Godfreys Reihen zu tun
  4. Entfernen Sie alle Zeilen mit nicht übereinstimmenden Objekten (keine, also in diesem Fall nichts zu tun)

Damit 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 Arguments key , wenn Sie {{#each}} .

Kollisionen 💥

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 ).

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] .

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 ein undefined 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:

  1. Es löst das Standardverhalten des Auswahlfelds aus: Ändern des aktivierten Status in true
  2. Es löst das Klickereignis aus, das vom Modifikator {{action}} abgefangen und erneut an die Methode makeChange gesendet wird
  3. Es ändert die Liste in [true, undefined, undefined, undefined, undefined] .
  4. Es aktualisiert das DOM.

Wie wird das DOM aktualisiert?

  1. Suchen Sie eine vorhandene Zeile, deren Daten mit dem ersten Objekt true übereinstimmen ( === ). Da nichts gefunden wurde, fügen Sie eine neue Zeile <input checked=true ...>0: true... ein (voranstellen)
  2. Suchen Sie eine vorhandene Zeile, deren Daten mit dem zweiten Objekt undefined übereinstimmen ( === ). Gefunden <input ...>0: ... (zuvor die ERSTE Zeile):
    2.1. Aktualisieren Sie den Textknoten {{idx}} auf 1
    2.2. Ansonsten hat sich, soweit Ember das beurteilen kann, in dieser Zeile nichts anderes geändert, nichts anderes zu tun
  3. Suchen Sie eine vorhandene Zeile, deren Daten mit dem dritten Objekt 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):
    3.1. Aktualisieren Sie den Textknoten {{idx}} auf 2
    3.2. Ansonsten hat sich, soweit Ember das beurteilen kann, in dieser Zeile nichts anderes geändert, nichts anderes zu tun
  4. Aktualisieren Sie in ähnlicher Weise die undefined-2 und undefined-3
  5. Entfernen Sie schließlich die nicht übereinstimmende Zeile 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?

Alle 8 Kommentare

@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.

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:

<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.

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:

<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:

  • Entfernen Sie 3 <li> Knoten
  • Fügen Sie 4 <li> Knoten ein
  • Fügen Sie 12 Textknoten ein (einen für den Vornamen, einen für den Zwischenraum und einen für den Nachnamen, mal 4 Zeilen).
  • Rufen Sie den to-upper-case -Helfer viermal auf

Dies 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:

  1. Vergleichen Sie das erste Objekt { 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 tun
  2. Ebenso nichts für Zeile 2 und 3 zu tun
  3. Es gibt keine vierte Zeile im DOM. Fügen Sie daher eine neue ein
    3.1. Fügen Sie einen <li> -Knoten ein
    3.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:

<ul>
  <li>Andrew TIMBERLAKE</li>
  <li>Yehuda KATZ</li>
  <li>Tom DALE</li>
  <li>Godfrey CHAN</li>
</ul>

Aber wie_?

  1. Vergleichen Sie das erste Objekt { 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 auf
    1.4. Aktualisieren Sie den Textknoten von "KATZ" auf "TIMBERLAKE".
  2. Vergleichen Sie das zweite Objekt { first: "Yehuda", last: "Katz" } mit der zweiten Zeile <li>Tom DALE</li> , einer weiteren 3 Operation
  3. Vergleichen Sie das zweite Objekt { first: "Tom", last: "Dale" } mit der zweiten Zeile <li>Godfrey CHAN</li> , einer weiteren 3 Operation
  4. Es gibt keine vierte Zeile im DOM. Fügen Sie daher eine neue ein
    3.1. Fügen Sie einen <li> -Knoten ein
    3.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 ( === ):

  1. Suchen Sie eine vorhandene Zeile, deren Daten mit dem ersten Objekt { 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 ein
    1.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")
  2. Suchen Sie eine vorhandene Zeile, deren Daten mit dem zweiten Objekt { 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 tun
  3. Ebenso nichts für Toms und Godfreys Reihen zu tun
  4. Entfernen Sie alle Zeilen mit nicht übereinstimmenden Objekten (keine, also in diesem Fall nichts zu tun)

Damit 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 Arguments key , wenn Sie {{#each}} .

Kollisionen 💥

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 ).

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] .

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 ein undefined 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:

  1. Es löst das Standardverhalten des Auswahlfelds aus: Ändern des aktivierten Status in true
  2. Es löst das Klickereignis aus, das vom Modifikator {{action}} abgefangen und erneut an die Methode makeChange gesendet wird
  3. Es ändert die Liste in [true, undefined, undefined, undefined, undefined] .
  4. Es aktualisiert das DOM.

Wie wird das DOM aktualisiert?

  1. Suchen Sie eine vorhandene Zeile, deren Daten mit dem ersten Objekt true übereinstimmen ( === ). Da nichts gefunden wurde, fügen Sie eine neue Zeile <input checked=true ...>0: true... ein (voranstellen)
  2. Suchen Sie eine vorhandene Zeile, deren Daten mit dem zweiten Objekt undefined übereinstimmen ( === ). Gefunden <input ...>0: ... (zuvor die ERSTE Zeile):
    2.1. Aktualisieren Sie den Textknoten {{idx}} auf 1
    2.2. Ansonsten hat sich, soweit Ember das beurteilen kann, in dieser Zeile nichts anderes geändert, nichts anderes zu tun
  3. Suchen Sie eine vorhandene Zeile, deren Daten mit dem dritten Objekt 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):
    3.1. Aktualisieren Sie den Textknoten {{idx}} auf 2
    3.2. Ansonsten hat sich, soweit Ember das beurteilen kann, in dieser Zeile nichts anderes geändert, nichts anderes zu tun
  4. Aktualisieren Sie in ähnlicher Weise die undefined-2 und undefined-3
  5. Entfernen Sie schließlich die nicht übereinstimmende Zeile 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?

@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:

  • Ziehen Sie ein Element zum Letzten
  • Schalterauswahl auf 'v2'

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.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen