Ember.js: #each bug de re-rendu

Créé le 14 sept. 2018  ·  8Commentaires  ·  Source: emberjs/ember.js

J'ai un tableau de valeurs vrai / faux / indéfini que je rend comme une liste de cases à cocher.
Lors du changement d'un élément de tableau vers ou depuis true, la liste des cases à cocher est de nouveau rendue avec la case à cocher suivante (index + 1) héritant de la modification avec la case à cocher modifiée.
Code:

{{#each range as |value idx|}}
  <label><input type="checkbox" checked={{value}} {{action makeChange idx on="change"}}>{{idx}}: {{value}}</label><br/>
{{/each}}

Lorsque j'utilise {{#each range key="@index" as |value idx|}} cela fonctionne correctement.

Twiddle: https://ember-twiddle.com/6d63548f35f99da19cee9f58fb64db59

embereach

Bug Has Reproduction Rendering

Commentaire le plus utile

Je pense que je sais ce qui se passe ici. C'est un 🍝 de gâchis mais je vais essayer de le décrire. Un grand nombre de cas extrêmes (erreur utilisateur à la frontière) ont contribué à cela, et je ne suis pas vraiment sûr de ce qui est / n'est pas un bogue, de quoi et comment résoudre l'un d'entre eux.

Majeur 🔑

Tout d'abord, je dois décrire ce que fait le paramètre key dans {{#each}} . TL; DR il essaie de déterminer quand et s'il serait judicieux de réutiliser le DOM existant, plutôt que de simplement créer le DOM à partir de zéro.

Pour notre propos, acceptons comme acquis que "toucher DOM" (par exemple mettre à jour le contenu d'un nœud de texte, d'un attribut, ajouter ou supprimer du contenu, etc.) est coûteux et doit être évité autant que possible.

Concentrons-nous sur un modèle assez simple:

<ul>
  {{#each this.names as |name|}}
    <li>{{name.first}} {{to-upper-case name.last}}</li>
  {{/each}}
</ul>

Si this.names est ...

[
  { first: "Yehuda", last: "Katz" },
  { first: "Tom", last: "Dale" },
  { first: "Godfrey", last: "Chan" }
]

Ensuite, vous obtiendrez ...

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

Jusqu'ici tout va bien.

Ajouter un élément à la liste

Et si nous ajoutions { first: "Andrew", last: "Timberlake" } à la liste? Nous nous attendons à ce que le modèle produise le DOM suivant:

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

Mais comment_?

La manière la plus naïve d'implémenter l'assistant {{#each}} effacerait tout le contenu de la liste à chaque fois que le contenu de la liste change. Pour ce faire, vous devez effectuer au moins 23 opérations:

  • Supprimer 3 <li> nœuds
  • Insérer 4 <li> nœuds
  • Insérez 12 nœuds de texte (un pour le prénom, un pour l'espace entre et un pour le nom, multiplié par 4 lignes)
  • Invoquez l' to-upper-case 4 fois

Cela semble ... très inutile et coûteux. Nous _ savons_ que les trois premiers éléments n'ont pas changé, donc ce serait bien si nous pouvions simplement sauter le travail pour ces lignes.

🔑 @index

Une meilleure implémentation serait d'essayer de réutiliser les lignes existantes et de ne pas faire de mises à jour inutiles. Une idée serait de simplement faire correspondre les lignes avec leurs positions dans les modèles. C'est essentiellement ce que fait key="@index" :

  1. Comparez le premier objet { first: "Yehuda", last: "Katz" } avec la première ligne, <li>Yehuda KATZ</li> :
    1.1. "Yehuda" === "Yehuda", rien à faire
    1.2. (l'espace ne contient pas de données dynamiques donc aucune comparaison n'est nécessaire)
    1.3. "Katz" === "Katz", puisque les helpers sont "purs", nous savons que nous n'aurons pas à ré-invoquer le helper to-upper-case , et donc nous connaissons la sortie de cet helper ("KATZ" ) _aussi_ n'a pas changé, donc rien à faire ici
  2. De même, rien à faire pour les rangées 2 et 3
  3. Il n'y a pas de quatrième ligne dans le DOM, alors insérez-en une nouvelle
    3.1. Insérer un nœud <li>
    3.2. Insérer un nœud de texte ("Andrew")
    3.3. Insérer un nœud de texte (l'espace)
    3.4. Invoquez l' to-upper-case ("Timberlake" -> "TIMBERLAKE")
    3.5. Insérer un nœud de texte ("TIMBERLAKE")

Donc, avec cette implémentation, nous avons réduit le nombre total d'opérations de 23 à 5 (👋 agitant la main sur le coût des comparaisons, mais pour notre propos, nous supposons qu'elles sont relativement bon marché par rapport au reste). Pas mal.

Pré-ajouter un élément à la liste

Mais maintenant, que se passerait-il si, au lieu de _ajouter_ { first: "Andrew", last: "Timberlake" } à la liste, nous la _préparions_ à la place? Nous nous attendons à ce que le modèle produise le DOM suivant:

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

Mais comment_?

  1. Comparez le premier objet { first: "Andrew", last: "Timberlake" } avec la première ligne, <li>Yehuda KATZ</li> :
    1.1. "Andrew"! == "Yehuda", mettre à jour le nœud de texte
    1.2. (l'espace ne contient pas de données dynamiques donc aucune comparaison n'est nécessaire)
    1.3. "Timberlake"! == "Katz", réinvoquez l' to-upper-case
    1.4. Mettez à jour le nœud de texte de "KATZ" à "TIMBERLAKE"
  2. Comparez le deuxième objet { first: "Yehuda", last: "Katz" } avec la deuxième ligne, <li>Tom DALE</li> , une autre opération 3
  3. Comparez le deuxième objet { first: "Tom", last: "Dale" } avec la deuxième ligne, <li>Godfrey CHAN</li> , encore 3 opérations
  4. Il n'y a pas de quatrième ligne dans le DOM, alors insérez-en une nouvelle
    3.1. Insérer un nœud <li>
    3.2. Insérer un nœud de texte ("Godfrey")
    3.3. Insérer un nœud de texte (l'espace)
    3.4. Appelez l'assistant to-upper-case ("Chan" -> "CHAN")
    3.5. Insérer un nœud de texte ("CHAN")

Cela fait 14 opérations. Aie!

🔑 @identité

Cela semblait inutile, car conceptuellement, que nous ajoutions ou ajoutions, nous ne changeons (insérons) qu'un seul objet dans le tableau. De manière optimale, nous devrions être en mesure de gérer ce cas aussi bien que nous l'avons fait dans le scénario d'ajout.

C'est là que key="@identity" entre en jeu. Au lieu de nous fier à l'ordre des éléments du tableau, nous utilisons leur identité d'objet JavaScript ( === ):

  1. Trouvez une ligne existante dont les données correspondent ( === ) au premier objet { first: "Andrew", last: "Timberlake" } . Puisque rien n'a été trouvé, insérez (préférez) une nouvelle ligne:
    1.1. Insérer un nœud <li>
    1.2. Insérer un nœud de texte ("Andrew")
    1.3. Insérer un nœud de texte (l'espace)
    1.4. Appelez l'assistant to-upper-case ("Timberlake" -> "TIMBERLAKE")
    1.5. Insérer un nœud de texte ("TIMBERLAKE")
  2. Trouvez une ligne existante dont les données correspondent ( === ) au deuxième objet { first: "Yehuda", last: "Katz" } . Trouvé <li>Yehuda KATZ</li> :
    2.1. "Yehuda" === "Yehuda", rien à faire
    2.2. (l'espace ne contient pas de données dynamiques donc aucune comparaison n'est nécessaire)
    2.3. "Katz" === "Katz", puisque les helpers sont "purs", nous savons que nous n'aurons pas à ré-invoquer le helper to-upper-case , et donc nous connaissons la sortie de cet helper ("KATZ" ) _aussi_ n'a pas changé, donc rien à faire ici
  3. De même, rien à faire pour les rangs de Tom et Godfrey
  4. Supprimez toutes les lignes avec des objets sans correspondance (aucun, donc rien à faire dans ce cas)

Sur ce, nous revenons aux 5 opérations optimales.

Augmenter

Encore une fois, c'est agiter la main sur les comparaisons et les coûts de comptabilité. En effet, ceux-ci ne sont pas gratuits non plus et dans cet exemple très simple, ils n'en valent peut-être pas la peine. Mais imaginez que la liste est grande et que chaque ligne appelle un composant compliqué (avec beaucoup d'aides, des propriétés calculées, des sous-composants, etc.). Imaginez le fil d'actualité LinkedIn, par exemple. Si nous ne faisons pas correspondre les bonnes lignes avec les bonnes données, les arguments de vos composants peuvent potentiellement générer beaucoup plus de mises à jour DOM que vous ne le pensez autrement. Il y a aussi des problèmes avec la correspondance des mauvais éléments DOM et la perte de l'état du DOM, comme la position du curseur et l'état de sélection de texte.

Dans l'ensemble, le coût supplémentaire de comparaison et de comptabilité vaut facilement la plupart du temps dans une application du monde réel. Puisque le key="@identity" est la valeur par défaut dans Ember et qu'il fonctionne bien dans presque tous les cas, vous n'aurez généralement pas à vous soucier de définir l'argument key lorsque vous utilisez {{#each}} .

Collisions 💥

Mais attendez, il y a un problème. Et cette affaire?

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

Le problème ici est que le même objet _pourrait_ apparaître plusieurs fois dans la même liste. Cela casse notre algorithme naïf @identity , en particulier la partie où nous avons dit "Trouver une ligne existante dont les données correspondent ( === ) ..." - cela ne fonctionne que si la relation entre les données et le DOM est 1 : 1, ce qui n'est pas vrai dans ce cas. Cela peut sembler peu probable en pratique, mais en tant que cadre, nous devons le gérer.

Pour éviter cela, nous utilisons une sorte d'approche hybride pour gérer ces collisions. En interne, le mappage des clés vers le DOM ressemble à ceci:

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

Pour la plupart, ce _is_ est assez rare, et quand cela arrive, cela fonctionne Good Enough ™ la plupart du temps. Si, pour une raison quelconque, cela ne fonctionne pas, vous pouvez toujours utiliser un chemin de clé (ou le mécanisme de saisie encore plus avancé de la RFC 321 ).

Retour au "🐛"

Après tout ce discours, nous sommes maintenant prêts à examiner le scénario du Twiddle.

Essentiellement, nous avons commencé avec cette liste: [undefined, undefined, undefined, undefined, undefined] .

Remarque non liée: Array(5) n'est _pas_ la même chose que [undefined, undefined, undefined, undefined, undefined] . Il produit un "tableau troué", ce que vous devez éviter en général. Cependant, il n'est pas lié à ce bogue, car en accédant aux "trous" vous obtenez en effet un undefined retour. Donc, pour notre objectif _très étroit_ seulement, ils sont les mêmes.

Comme nous n'avons pas spécifié la clé, Ember utilise @identity par défaut. De plus, comme ce sont des collisions, nous nous sommes retrouvés avec quelque chose comme ceci:

"undefined" => <input ...> 0: ...,
"undefined-1" => <input ...> 1: ...,
"undefined-2" => <input ...> 2: ...,
"undefined-3" => <input ...> 3: ...,
"undefined-4" => <input ...> 4: ...

Maintenant, disons que nous cliquons sur la première case à cocher:

  1. Il déclenche le comportement par défaut de la boîte de sélection: changer l'état coché en vrai
  2. Il déclenche l'événement click, qui est intercepté par le modificateur {{action}} et redistribué vers la méthode makeChange
  3. Cela change la liste en [true, undefined, undefined, undefined, undefined] .
  4. Il met à jour le DOM.

Comment le DOM est-il mis à jour?

  1. Trouvez une ligne existante dont les données correspondent ( === ) au premier objet true . Puisque rien n'a été trouvé, insérez (préfixez) une nouvelle ligne <input checked=true ...>0: true...
  2. Trouvez une ligne existante dont les données correspondent ( === ) au deuxième objet undefined . Trouvé <input ...>0: ... (auparavant la PREMIÈRE ligne):
    2.1. Mettez à jour le nœud de texte {{idx}} en 1
    2.2. Sinon, pour autant qu'Ember puisse le dire, rien d'autre n'a changé dans cette rangée, rien d'autre à faire
  3. Trouvez une ligne existante dont les données correspondent ( === ) au troisième objet undefined . Puisque c'est la deuxième fois que nous voyons undefined , la clé interne est undefined-1 , nous avons donc trouvé <input ...>1: ... (auparavant la SECONDE ligne):
    3.1. Mettez à jour le nœud de texte {{idx}} en 2
    3.2. Sinon, pour autant qu'Ember puisse le dire, rien d'autre n'a changé dans cette rangée, rien d'autre à faire
  4. De même, mettez à jour les undefined-2 et undefined-3
  5. Enfin, retirez - la sans équivalent undefined-4 rangée (car il y a un moins undefined dans le tableau après la mise à jour)

Cela explique donc comment nous avons obtenu la sortie que vous aviez dans le twiddle. Essentiellement, toutes les lignes DOM ont été décalées d'une unité, et une nouvelle a été insérée en haut, tandis que le {{idx}} est mis à jour pour le reste.

La partie vraiment inattendue est 2.2. Même si la première case à cocher (celle sur laquelle on a cliqué) a été décalée d'une ligne vers la deuxième position, vous vous seriez probablement attendus à ce qu'Ember passe à sa propriété checked en true , et puisque sa valeur liée n'est pas définie, vous pouvez vous attendre à ce qu'Ember la remette en false , en la décochant.

Mais ce n'est pas ainsi que cela fonctionne. Comme mentionné au début, accéder au DOM est coûteux. Cela inclut la _lecture_ de DOM. Si, à chaque mise à jour, nous devions lire la dernière valeur du DOM pour nos comparaisons, cela irait à l'encontre de l'objectif de nos optimisations. Par conséquent, pour éviter cela, nous nous sommes souvenus de la dernière valeur que nous avions écrite dans le DOM, et comparons la valeur actuelle à la valeur mise en cache sans avoir à la relire depuis le DOM. Ce n'est qu'en cas de différence que nous écrivons la nouvelle valeur dans le DOM (et la mettons en cache pour la prochaine fois). C'est dans ce sens que nous partageons en quelque sorte la même approche «DOM virtuel», mais nous ne le faisons qu'aux nœuds feuilles, sans virtualiser «l'arborescence» de l'ensemble du DOM.

Ainsi, TL; DR, "lier" la propriété checked (ou la propriété value d'un champ de texte, etc.) ne fonctionne pas vraiment comme vous le souhaitez. Imaginez si vous avez rendu <div>{{this.name}}</div> et que vous avez mis à jour manuellement le textContent de l'élément div utilisant jQuery ou avec l'inspecteur de chrome. Vous ne vous attendiez pas à ce qu'Ember le remarque et mette this.name jour checked s'est produite en dehors d'Ember (via le comportement par défaut du navigateur pour la case à cocher), Ember ne le saura pas.

C'est pourquoi l'assistant {{input}} existe. Il doit enregistrer les écouteurs d'événements pertinents sur l'élément HTML sous-jacent et refléter les opérations dans le changement de propriété approprié, afin que les parties intéressées (par exemple la couche de rendu) puissent être notifiées.

Je ne sais pas trop où cela nous laisse. Je comprends pourquoi cela est surprenant, mais j'ai tendance à dire qu'il s'agit d'une série d'erreurs utilisateur malheureuses. Peut-être devrions-nous ne pas lier ces propriétés aux éléments d'entrée?

Tous les 8 commentaires

@andrewtimberlake semble utiliser le {{#each range key="@index" as |value idx|}} contourner le problème.

Mais cela semble être un bogue, le key un but différent, https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor= chaque

Je pense que je sais ce qui se passe ici. C'est un 🍝 de gâchis mais je vais essayer de le décrire. Un grand nombre de cas extrêmes (erreur utilisateur à la frontière) ont contribué à cela, et je ne suis pas vraiment sûr de ce qui est / n'est pas un bogue, de quoi et comment résoudre l'un d'entre eux.

Majeur 🔑

Tout d'abord, je dois décrire ce que fait le paramètre key dans {{#each}} . TL; DR il essaie de déterminer quand et s'il serait judicieux de réutiliser le DOM existant, plutôt que de simplement créer le DOM à partir de zéro.

Pour notre propos, acceptons comme acquis que "toucher DOM" (par exemple mettre à jour le contenu d'un nœud de texte, d'un attribut, ajouter ou supprimer du contenu, etc.) est coûteux et doit être évité autant que possible.

Concentrons-nous sur un modèle assez simple:

<ul>
  {{#each this.names as |name|}}
    <li>{{name.first}} {{to-upper-case name.last}}</li>
  {{/each}}
</ul>

Si this.names est ...

[
  { first: "Yehuda", last: "Katz" },
  { first: "Tom", last: "Dale" },
  { first: "Godfrey", last: "Chan" }
]

Ensuite, vous obtiendrez ...

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

Jusqu'ici tout va bien.

Ajouter un élément à la liste

Et si nous ajoutions { first: "Andrew", last: "Timberlake" } à la liste? Nous nous attendons à ce que le modèle produise le DOM suivant:

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

Mais comment_?

La manière la plus naïve d'implémenter l'assistant {{#each}} effacerait tout le contenu de la liste à chaque fois que le contenu de la liste change. Pour ce faire, vous devez effectuer au moins 23 opérations:

  • Supprimer 3 <li> nœuds
  • Insérer 4 <li> nœuds
  • Insérez 12 nœuds de texte (un pour le prénom, un pour l'espace entre et un pour le nom, multiplié par 4 lignes)
  • Invoquez l' to-upper-case 4 fois

Cela semble ... très inutile et coûteux. Nous _ savons_ que les trois premiers éléments n'ont pas changé, donc ce serait bien si nous pouvions simplement sauter le travail pour ces lignes.

🔑 @index

Une meilleure implémentation serait d'essayer de réutiliser les lignes existantes et de ne pas faire de mises à jour inutiles. Une idée serait de simplement faire correspondre les lignes avec leurs positions dans les modèles. C'est essentiellement ce que fait key="@index" :

  1. Comparez le premier objet { first: "Yehuda", last: "Katz" } avec la première ligne, <li>Yehuda KATZ</li> :
    1.1. "Yehuda" === "Yehuda", rien à faire
    1.2. (l'espace ne contient pas de données dynamiques donc aucune comparaison n'est nécessaire)
    1.3. "Katz" === "Katz", puisque les helpers sont "purs", nous savons que nous n'aurons pas à ré-invoquer le helper to-upper-case , et donc nous connaissons la sortie de cet helper ("KATZ" ) _aussi_ n'a pas changé, donc rien à faire ici
  2. De même, rien à faire pour les rangées 2 et 3
  3. Il n'y a pas de quatrième ligne dans le DOM, alors insérez-en une nouvelle
    3.1. Insérer un nœud <li>
    3.2. Insérer un nœud de texte ("Andrew")
    3.3. Insérer un nœud de texte (l'espace)
    3.4. Invoquez l' to-upper-case ("Timberlake" -> "TIMBERLAKE")
    3.5. Insérer un nœud de texte ("TIMBERLAKE")

Donc, avec cette implémentation, nous avons réduit le nombre total d'opérations de 23 à 5 (👋 agitant la main sur le coût des comparaisons, mais pour notre propos, nous supposons qu'elles sont relativement bon marché par rapport au reste). Pas mal.

Pré-ajouter un élément à la liste

Mais maintenant, que se passerait-il si, au lieu de _ajouter_ { first: "Andrew", last: "Timberlake" } à la liste, nous la _préparions_ à la place? Nous nous attendons à ce que le modèle produise le DOM suivant:

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

Mais comment_?

  1. Comparez le premier objet { first: "Andrew", last: "Timberlake" } avec la première ligne, <li>Yehuda KATZ</li> :
    1.1. "Andrew"! == "Yehuda", mettre à jour le nœud de texte
    1.2. (l'espace ne contient pas de données dynamiques donc aucune comparaison n'est nécessaire)
    1.3. "Timberlake"! == "Katz", réinvoquez l' to-upper-case
    1.4. Mettez à jour le nœud de texte de "KATZ" à "TIMBERLAKE"
  2. Comparez le deuxième objet { first: "Yehuda", last: "Katz" } avec la deuxième ligne, <li>Tom DALE</li> , une autre opération 3
  3. Comparez le deuxième objet { first: "Tom", last: "Dale" } avec la deuxième ligne, <li>Godfrey CHAN</li> , encore 3 opérations
  4. Il n'y a pas de quatrième ligne dans le DOM, alors insérez-en une nouvelle
    3.1. Insérer un nœud <li>
    3.2. Insérer un nœud de texte ("Godfrey")
    3.3. Insérer un nœud de texte (l'espace)
    3.4. Appelez l'assistant to-upper-case ("Chan" -> "CHAN")
    3.5. Insérer un nœud de texte ("CHAN")

Cela fait 14 opérations. Aie!

🔑 @identité

Cela semblait inutile, car conceptuellement, que nous ajoutions ou ajoutions, nous ne changeons (insérons) qu'un seul objet dans le tableau. De manière optimale, nous devrions être en mesure de gérer ce cas aussi bien que nous l'avons fait dans le scénario d'ajout.

C'est là que key="@identity" entre en jeu. Au lieu de nous fier à l'ordre des éléments du tableau, nous utilisons leur identité d'objet JavaScript ( === ):

  1. Trouvez une ligne existante dont les données correspondent ( === ) au premier objet { first: "Andrew", last: "Timberlake" } . Puisque rien n'a été trouvé, insérez (préférez) une nouvelle ligne:
    1.1. Insérer un nœud <li>
    1.2. Insérer un nœud de texte ("Andrew")
    1.3. Insérer un nœud de texte (l'espace)
    1.4. Appelez l'assistant to-upper-case ("Timberlake" -> "TIMBERLAKE")
    1.5. Insérer un nœud de texte ("TIMBERLAKE")
  2. Trouvez une ligne existante dont les données correspondent ( === ) au deuxième objet { first: "Yehuda", last: "Katz" } . Trouvé <li>Yehuda KATZ</li> :
    2.1. "Yehuda" === "Yehuda", rien à faire
    2.2. (l'espace ne contient pas de données dynamiques donc aucune comparaison n'est nécessaire)
    2.3. "Katz" === "Katz", puisque les helpers sont "purs", nous savons que nous n'aurons pas à ré-invoquer le helper to-upper-case , et donc nous connaissons la sortie de cet helper ("KATZ" ) _aussi_ n'a pas changé, donc rien à faire ici
  3. De même, rien à faire pour les rangs de Tom et Godfrey
  4. Supprimez toutes les lignes avec des objets sans correspondance (aucun, donc rien à faire dans ce cas)

Sur ce, nous revenons aux 5 opérations optimales.

Augmenter

Encore une fois, c'est agiter la main sur les comparaisons et les coûts de comptabilité. En effet, ceux-ci ne sont pas gratuits non plus et dans cet exemple très simple, ils n'en valent peut-être pas la peine. Mais imaginez que la liste est grande et que chaque ligne appelle un composant compliqué (avec beaucoup d'aides, des propriétés calculées, des sous-composants, etc.). Imaginez le fil d'actualité LinkedIn, par exemple. Si nous ne faisons pas correspondre les bonnes lignes avec les bonnes données, les arguments de vos composants peuvent potentiellement générer beaucoup plus de mises à jour DOM que vous ne le pensez autrement. Il y a aussi des problèmes avec la correspondance des mauvais éléments DOM et la perte de l'état du DOM, comme la position du curseur et l'état de sélection de texte.

Dans l'ensemble, le coût supplémentaire de comparaison et de comptabilité vaut facilement la plupart du temps dans une application du monde réel. Puisque le key="@identity" est la valeur par défaut dans Ember et qu'il fonctionne bien dans presque tous les cas, vous n'aurez généralement pas à vous soucier de définir l'argument key lorsque vous utilisez {{#each}} .

Collisions 💥

Mais attendez, il y a un problème. Et cette affaire?

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

Le problème ici est que le même objet _pourrait_ apparaître plusieurs fois dans la même liste. Cela casse notre algorithme naïf @identity , en particulier la partie où nous avons dit "Trouver une ligne existante dont les données correspondent ( === ) ..." - cela ne fonctionne que si la relation entre les données et le DOM est 1 : 1, ce qui n'est pas vrai dans ce cas. Cela peut sembler peu probable en pratique, mais en tant que cadre, nous devons le gérer.

Pour éviter cela, nous utilisons une sorte d'approche hybride pour gérer ces collisions. En interne, le mappage des clés vers le DOM ressemble à ceci:

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

Pour la plupart, ce _is_ est assez rare, et quand cela arrive, cela fonctionne Good Enough ™ la plupart du temps. Si, pour une raison quelconque, cela ne fonctionne pas, vous pouvez toujours utiliser un chemin de clé (ou le mécanisme de saisie encore plus avancé de la RFC 321 ).

Retour au "🐛"

Après tout ce discours, nous sommes maintenant prêts à examiner le scénario du Twiddle.

Essentiellement, nous avons commencé avec cette liste: [undefined, undefined, undefined, undefined, undefined] .

Remarque non liée: Array(5) n'est _pas_ la même chose que [undefined, undefined, undefined, undefined, undefined] . Il produit un "tableau troué", ce que vous devez éviter en général. Cependant, il n'est pas lié à ce bogue, car en accédant aux "trous" vous obtenez en effet un undefined retour. Donc, pour notre objectif _très étroit_ seulement, ils sont les mêmes.

Comme nous n'avons pas spécifié la clé, Ember utilise @identity par défaut. De plus, comme ce sont des collisions, nous nous sommes retrouvés avec quelque chose comme ceci:

"undefined" => <input ...> 0: ...,
"undefined-1" => <input ...> 1: ...,
"undefined-2" => <input ...> 2: ...,
"undefined-3" => <input ...> 3: ...,
"undefined-4" => <input ...> 4: ...

Maintenant, disons que nous cliquons sur la première case à cocher:

  1. Il déclenche le comportement par défaut de la boîte de sélection: changer l'état coché en vrai
  2. Il déclenche l'événement click, qui est intercepté par le modificateur {{action}} et redistribué vers la méthode makeChange
  3. Cela change la liste en [true, undefined, undefined, undefined, undefined] .
  4. Il met à jour le DOM.

Comment le DOM est-il mis à jour?

  1. Trouvez une ligne existante dont les données correspondent ( === ) au premier objet true . Puisque rien n'a été trouvé, insérez (préfixez) une nouvelle ligne <input checked=true ...>0: true...
  2. Trouvez une ligne existante dont les données correspondent ( === ) au deuxième objet undefined . Trouvé <input ...>0: ... (auparavant la PREMIÈRE ligne):
    2.1. Mettez à jour le nœud de texte {{idx}} en 1
    2.2. Sinon, pour autant qu'Ember puisse le dire, rien d'autre n'a changé dans cette rangée, rien d'autre à faire
  3. Trouvez une ligne existante dont les données correspondent ( === ) au troisième objet undefined . Puisque c'est la deuxième fois que nous voyons undefined , la clé interne est undefined-1 , nous avons donc trouvé <input ...>1: ... (auparavant la SECONDE ligne):
    3.1. Mettez à jour le nœud de texte {{idx}} en 2
    3.2. Sinon, pour autant qu'Ember puisse le dire, rien d'autre n'a changé dans cette rangée, rien d'autre à faire
  4. De même, mettez à jour les undefined-2 et undefined-3
  5. Enfin, retirez - la sans équivalent undefined-4 rangée (car il y a un moins undefined dans le tableau après la mise à jour)

Cela explique donc comment nous avons obtenu la sortie que vous aviez dans le twiddle. Essentiellement, toutes les lignes DOM ont été décalées d'une unité, et une nouvelle a été insérée en haut, tandis que le {{idx}} est mis à jour pour le reste.

La partie vraiment inattendue est 2.2. Même si la première case à cocher (celle sur laquelle on a cliqué) a été décalée d'une ligne vers la deuxième position, vous vous seriez probablement attendus à ce qu'Ember passe à sa propriété checked en true , et puisque sa valeur liée n'est pas définie, vous pouvez vous attendre à ce qu'Ember la remette en false , en la décochant.

Mais ce n'est pas ainsi que cela fonctionne. Comme mentionné au début, accéder au DOM est coûteux. Cela inclut la _lecture_ de DOM. Si, à chaque mise à jour, nous devions lire la dernière valeur du DOM pour nos comparaisons, cela irait à l'encontre de l'objectif de nos optimisations. Par conséquent, pour éviter cela, nous nous sommes souvenus de la dernière valeur que nous avions écrite dans le DOM, et comparons la valeur actuelle à la valeur mise en cache sans avoir à la relire depuis le DOM. Ce n'est qu'en cas de différence que nous écrivons la nouvelle valeur dans le DOM (et la mettons en cache pour la prochaine fois). C'est dans ce sens que nous partageons en quelque sorte la même approche «DOM virtuel», mais nous ne le faisons qu'aux nœuds feuilles, sans virtualiser «l'arborescence» de l'ensemble du DOM.

Ainsi, TL; DR, "lier" la propriété checked (ou la propriété value d'un champ de texte, etc.) ne fonctionne pas vraiment comme vous le souhaitez. Imaginez si vous avez rendu <div>{{this.name}}</div> et que vous avez mis à jour manuellement le textContent de l'élément div utilisant jQuery ou avec l'inspecteur de chrome. Vous ne vous attendiez pas à ce qu'Ember le remarque et mette this.name jour checked s'est produite en dehors d'Ember (via le comportement par défaut du navigateur pour la case à cocher), Ember ne le saura pas.

C'est pourquoi l'assistant {{input}} existe. Il doit enregistrer les écouteurs d'événements pertinents sur l'élément HTML sous-jacent et refléter les opérations dans le changement de propriété approprié, afin que les parties intéressées (par exemple la couche de rendu) puissent être notifiées.

Je ne sais pas trop où cela nous laisse. Je comprends pourquoi cela est surprenant, mais j'ai tendance à dire qu'il s'agit d'une série d'erreurs utilisateur malheureuses. Peut-être devrions-nous ne pas lier ces propriétés aux éléments d'entrée?

@chancancode - merci pour l'explication incroyable. Cela signifie-t-il que <input ... > ne doit jamais être utilisé mais seulement {{input ...}} afin d'éviter toutes les erreurs de ce genre?

@ boris-petrov, il peut y avoir des cas limités où cela est acceptable .. comme un champ de texte en lecture seule pour le genre de chose "copier cette URL dans votre presse-papiers", ou vous _pouvez_ utiliser l'élément d'entrée + {{action}} pour intercepter le DOM et refléter les mises à jour de propriété manuellement (ce que le twiddle a essayé de faire, sauf qu'il a également rencontré la collision @identity ), mais oui, à un moment donné, vous ne faites que ré-implémenter {{input}} et gérer tous les cas de bord qu'il a déjà traités pour vous. Donc, je pense qu'il est probablement juste de dire que vous devriez simplement utiliser {{input}} plupart, sinon la totalité, du temps.

Cependant, cela n'aurait toujours pas «corrigé» ce cas où il y a des collisions avec les clés. Voir https://ember-twiddle.com/0f2369021128e2ae0c445155df5bb034?openFiles=templates.application.hbs%2C

C'est pourquoi j'ai dit, je ne sais pas à 100% quoi faire à ce sujet. D'une part je conviens que c'est surprenant et inattendu, d'autre part, ce genre de collision est assez rare dans les vraies applications et c'est pourquoi l'argument "clé" est personnalisable (c'est un cas où la clé par défaut "@identity" n'est pas Good Enough ™, c'est pourquoi cette fonction existe).

@chancancode - cela me rappelle un autre problème que j'ai ouvert il y a quelque temps . Pensez-vous qu'il y a quelque chose de similaire là-bas? La réponse que j'y ai reçue (à propos de la nécessité d'utiliser replace au lieu de set lors de la définition des éléments du tableau) me semble encore étrange.

@ boris-petrov je ne pense pas que ce soit lié

salut, nous utilisons sortablejs pour une liste déplaçable avec braise. Veuillez vérifier cette démo pour reproduire chaque problème.

étape:

  • faites glisser un élément pour durer
  • basculer sélectionner sur 'v2'

vous pouvez voir l'élément glissé rester dans l'arborescence dom.

mais, si vous faites glisser l'élément vers une autre position (pas le dernier élément), il semble que cela fonctionne bien.

Cette page vous a été utile?
0 / 5 - 0 notes