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
@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.
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.
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:
<li>
nœuds<li>
nœudsto-upper-case
4 foisCela 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.
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"
:
{ first: "Yehuda", last: "Katz" }
avec la première ligne, <li>Yehuda KATZ</li>
:to-upper-case
, et donc nous connaissons la sortie de cet helper ("KATZ" ) _aussi_ n'a pas changé, donc rien à faire ici<li>
to-upper-case
("Timberlake" -> "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.
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_?
{ first: "Andrew", last: "Timberlake" }
avec la première ligne, <li>Yehuda KATZ</li>
:to-upper-case
{ first: "Yehuda", last: "Katz" }
avec la deuxième ligne, <li>Tom DALE</li>
, une autre opération 3{ first: "Tom", last: "Dale" }
avec la deuxième ligne, <li>Godfrey CHAN</li>
, encore 3 opérations<li>
to-upper-case
("Chan" -> "CHAN")Cela fait 14 opérations. Aie!
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 ( ===
):
===
) au premier objet { first: "Andrew", last: "Timberlake" }
. Puisque rien n'a été trouvé, insérez (préférez) une nouvelle ligne:<li>
to-upper-case
("Timberlake" -> "TIMBERLAKE")===
) au deuxième objet { first: "Yehuda", last: "Katz" }
. Trouvé <li>Yehuda KATZ</li>
:to-upper-case
, et donc nous connaissons la sortie de cet helper ("KATZ" ) _aussi_ n'a pas changé, donc rien à faire iciSur ce, nous revenons aux 5 opérations optimales.
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}}
.
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 ).
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 unundefined
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:
{{action}}
et redistribué vers la méthode makeChange
[true, undefined, undefined, undefined, undefined]
.Comment le DOM est-il mis à jour?
===
) au premier objet true
. Puisque rien n'a été trouvé, insérez (préfixez) une nouvelle ligne <input checked=true ...>0: true...
===
) au deuxième objet undefined
. Trouvé <input ...>0: ...
(auparavant la PREMIÈRE ligne):{{idx}}
en 1
===
) 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):{{idx}}
en 2
undefined-2
et undefined-3
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?
Code pertinent:
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 - 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:
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.
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:
Si
this.names
est ...Ensuite, vous obtiendrez ...
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: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:<li>
nœuds<li>
nœudsto-upper-case
4 foisCela 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"
:{ 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 ici3.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:Mais comment_?
{ 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"
{ first: "Yehuda", last: "Katz" }
avec la deuxième ligne,<li>Tom DALE</li>
, une autre opération 3{ first: "Tom", last: "Dale" }
avec la deuxième ligne,<li>Godfrey CHAN</li>
, encore 3 opérations3.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 (===
):===
) 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")
===
) 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 iciSur 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'argumentkey
lorsque vous utilisez{{#each}}
.Collisions 💥
Mais attendez, il y a un problème. Et cette affaire?
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:
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]
.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:Maintenant, disons que nous cliquons sur la première case à cocher:
{{action}}
et redistribué vers la méthodemakeChange
[true, undefined, undefined, undefined, undefined]
.Comment le DOM est-il mis à jour?
===
) au premier objettrue
. Puisque rien n'a été trouvé, insérez (préfixez) une nouvelle ligne<input checked=true ...>0: true...
===
) au deuxième objetundefined
. Trouvé<input ...>0: ...
(auparavant la PREMIÈRE ligne):2.1. Mettez à jour le nœud de texte
{{idx}}
en1
2.2. Sinon, pour autant qu'Ember puisse le dire, rien d'autre n'a changé dans cette rangée, rien d'autre à faire
===
) au troisième objetundefined
. Puisque c'est la deuxième fois que nous voyonsundefined
, la clé interne estundefined-1
, nous avons donc trouvé<input ...>1: ...
(auparavant la SECONDE ligne):3.1. Mettez à jour le nœud de texte
{{idx}}
en2
3.2. Sinon, pour autant qu'Ember puisse le dire, rien d'autre n'a changé dans cette rangée, rien d'autre à faire
undefined-2
etundefined-3
undefined-4
rangée (car il y a un moinsundefined
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
entrue
, et puisque sa valeur liée n'est pas définie, vous pouvez vous attendre à ce qu'Ember la remette enfalse
, 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 letextContent
de l'élémentdiv
utilisantjQuery
ou avec l'inspecteur de chrome. Vous ne vous attendiez pas à ce qu'Ember le remarque et mettethis.name
jourchecked
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?