Ember.js: # cada error de renderizado

Creado en 14 sept. 2018  ·  8Comentarios  ·  Fuente: emberjs/ember.js

Tengo una matriz de valores verdaderos / falsos / indefinidos que estoy representando como una lista de casillas de verificación.
Al cambiar un elemento de la matriz a o desde verdadero, la lista de casillas de verificación se vuelve a representar con la siguiente casilla de verificación (índice + 1) heredando el cambio junto con la casilla de verificación modificada.
Código:

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

Cuando uso {{#each range key="@index" as |value idx|}} funciona correctamente.

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

embereach

Bug Has Reproduction Rendering

Comentario más útil

Creo que sé lo que está pasando aquí. Es un desastre pero intentaré describirlo. A esto contribuyeron muchos casos extremos (error de usuario de la línea límite), y no estoy realmente seguro de qué es / no es un error, qué y cómo solucionarlos.

Mayor 🔑

Primero que nada, necesito describir lo que hace el parámetro key en {{#each}} . TL; DR está tratando de determinar cuándo y si tendría sentido reutilizar el DOM existente, en lugar de simplemente crear el DOM desde cero.

Para nuestro propósito, aceptemos como un hecho que "tocar DOM" (por ejemplo, actualizar el contenido de un nodo de texto, un atributo, agregar o eliminar contenido, etc.) es costoso y debe evitarse tanto como sea posible.

Centrémonos en una plantilla bastante simple:

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

Si this.names es ...

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

Entonces obtendrás ...

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

Hasta aquí todo bien.

Agregar un elemento a la lista

Ahora, ¿qué pasa si agregamos { first: "Andrew", last: "Timberlake" } a la lista? Esperaríamos que la plantilla produjera el siguiente DOM:

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

Pero cómo_?

La forma más ingenua de implementar el asistente {{#each}} borraría todo el contenido de la lista cada vez que cambia el contenido de la lista. Para hacer esto, necesitaría realizar _ al menos_ 23 operaciones:

  • Eliminar 3 <li> nodos
  • Inserte 4 <li> nodos
  • Inserte 12 nodos de texto (uno para el nombre, uno para el espacio intermedio y uno para el apellido, multiplicado por 4 filas)
  • Invocar el ayudante to-upper-case 4 veces

Esto parece ... muy innecesario y caro. _Sabemos_ que los primeros tres elementos no cambiaron, por lo que sería bueno si pudiéramos omitir el trabajo para esas filas.

🔑 @index

Una mejor implementación sería intentar reutilizar las filas existentes y no hacer actualizaciones innecesarias. Una idea sería simplemente hacer coincidir las filas con sus posiciones en las plantillas. Esto es esencialmente lo que hace key="@index" :

  1. Compare el primer objeto { first: "Yehuda", last: "Katz" } con la primera fila, <li>Yehuda KATZ</li> :
    1.1. "Yehuda" === "Yehuda", nada que hacer
    1.2. (el espacio no contiene datos dinámicos, por lo que no se necesita comparación)
    1.3. "Katz" === "Katz", dado que los ayudantes son "puros", sabemos que no tendremos que volver a invocar el ayudante to-upper-case y, por lo tanto, conocemos la salida de ese ayudante ("KATZ" ) _también_ no cambió, así que no hay nada que hacer aquí
  2. Del mismo modo, nada que hacer para las filas 2 y 3
  3. No hay una cuarta fila en el DOM, así que inserte una nueva
    3.1. Inserte un nodo <li>
    3.2. Insertar un nodo de texto ("Andrew")
    3.3. Insertar un nodo de texto (el espacio)
    3.4. Invocar el to-upper-case helper ("Timberlake" -> "TIMBERLAKE")
    3.5. Insertar un nodo de texto ("TIMBERLAKE")

Entonces, con esta implementación, redujimos el número total de operaciones de 23 a 5 (👋 agitando la mano sobre el costo de las comparaciones, pero para nuestro propósito, asumimos que son relativamente baratas en comparación con el resto). No está mal.

Agregar un elemento a la lista

Pero ahora, ¿qué pasaría si, en lugar de _anexar_ { first: "Andrew", last: "Timberlake" } a la lista, lo _prependemos_ en su lugar? Esperaríamos que la plantilla produjera el siguiente DOM:

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

Pero cómo_?

  1. Compare el primer objeto { first: "Andrew", last: "Timberlake" } con la primera fila, <li>Yehuda KATZ</li> :
    1.1. "Andrew"! == "Yehuda", actualiza el nodo de texto
    1.2. (el espacio no contiene datos dinámicos, por lo que no se necesita comparación)
    1.3. "Timberlake"! == "Katz", vuelve a invocar el ayudante to-upper-case
    1.4. Actualiza el nodo de texto de "KATZ" a "TIMBERLAKE"
  2. Compare el segundo objeto { first: "Yehuda", last: "Katz" } con la segunda fila, <li>Tom DALE</li> , otras 3 operaciones
  3. Compare el segundo objeto { first: "Tom", last: "Dale" } con la segunda fila, <li>Godfrey CHAN</li> , otras 3 operaciones
  4. No hay una cuarta fila en el DOM, así que inserte una nueva
    3.1. Inserte un nodo <li>
    3.2. Insertar un nodo de texto ("Godfrey")
    3.3. Insertar un nodo de texto (el espacio)
    3.4. Invocar el to-upper-case helper ("Chan" -> "CHAN")
    3.5. Insertar un nodo de texto ("CHAN")

Son 14 operaciones. ¡Ay!

🔑 @identidad

Eso parecía innecesario, porque conceptualmente, ya sea que estemos anteponiendo o agregando, todavía solo estamos cambiando (insertando) un solo objeto en la matriz. De manera óptima, deberíamos poder manejar este caso tan bien como lo hicimos en el escenario de adición.

Aquí es donde entra key="@identity" . En lugar de confiar en el _orden_ de los elementos en la matriz, usamos su identidad de objeto JavaScript ( === ):

  1. Busque una fila existente cuyos datos coincidan ( === ) con el primer objeto { first: "Andrew", last: "Timberlake" } . Como no se encontró nada, inserte (anteponga) una nueva fila:
    1.1. Inserte un nodo <li>
    1.2. Insertar un nodo de texto ("Andrew")
    1.3. Insertar un nodo de texto (el espacio)
    1.4. Invocar el to-upper-case helper ("Timberlake" -> "TIMBERLAKE")
    1.5. Insertar un nodo de texto ("TIMBERLAKE")
  2. Busque una fila existente cuyos datos coincidan ( === ) con el segundo objeto { first: "Yehuda", last: "Katz" } . Encontrado <li>Yehuda KATZ</li> :
    2.1. "Yehuda" === "Yehuda", nada que hacer
    2.2. (el espacio no contiene datos dinámicos, por lo que no se necesita comparación)
    2.3. "Katz" === "Katz", dado que los ayudantes son "puros", sabemos que no tendremos que volver a invocar el ayudante to-upper-case y, por lo tanto, conocemos la salida de ese ayudante ("KATZ" ) _también_ no cambió, así que no hay nada que hacer aquí
  3. Del mismo modo, nada que hacer por las filas de Tom y Godfrey
  4. Elimine todas las filas con objetos no coincidentes (ninguno, por lo que no hay nada que hacer en este caso)

Con eso, volvemos a las 5 operaciones óptimas.

Ampliar

Una vez más, se trata de hacer un gesto con la mano sobre las comparaciones y los costos de contabilidad. De hecho, esos tampoco son gratuitos y, en este ejemplo muy simple, puede que no valgan la pena. Pero imagine que la lista es grande y cada fila invoca un componente complicado (con muchos ayudantes, propiedades calculadas, subcomponentes, etc.). Imagínese el servicio de noticias de LinkedIn, por ejemplo. Si no hacemos coincidir las filas correctas con los datos correctos, los argumentos de sus componentes pueden potencialmente agitarse mucho y provocar muchas más actualizaciones DOM de las que podría esperar. También hay problemas para hacer coincidir los elementos DOM incorrectos y perder el estado del DOM, como la posición del cursor y el estado de selección de texto.

En general, el costo adicional de comparación y contabilidad vale la pena la mayor parte del tiempo en una aplicación del mundo real. Dado que key="@identity" es el valor predeterminado en Ember y funciona bien en casi todos los casos, normalmente no tendrá que preocuparse por configurar el argumento key cuando utilice {{#each}} .

Colisiones 💥

Pero espere, hay un problema. ¿Y este caso?

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

El problema aquí es que el mismo objeto _podría_ aparecer varias veces en la misma lista. Esto rompe nuestro ingenuo algoritmo @identity , específicamente la parte en la que dijimos "Encuentra una fila existente cuyos datos coincidan ( === ) ..."; esto solo funciona si la relación de datos a DOM es 1 : 1, que no es cierto en este caso. Esto puede parecer poco probable en la práctica, pero como marco, tenemos que manejarlo.

Para evitar esto, utilizamos una especie de enfoque híbrido para manejar estas colisiones. Internamente, la asignación de claves a DOM se parece a esto:

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

En su mayor parte, esto _es_ bastante raro, y cuando aparece, funciona Good Enough ™ la mayor parte del tiempo. Si, por alguna razón, esto no funciona, siempre puede usar una ruta de clave (o el mecanismo de clave aún más avanzado en RFC 321 ).

Volver a la "🐛"

Después de toda esa charla, ahora estamos listos para ver el escenario en Twiddle.

Básicamente, comenzamos con esta lista: [undefined, undefined, undefined, undefined, undefined] .

Nota no relacionada: Array(5) _no_ es lo mismo que [undefined, undefined, undefined, undefined, undefined] . Produce una "matriz perforada" que es algo que debe evitar en general. Sin embargo, no está relacionado con este error, porque al acceder a los "agujeros", de hecho, obtiene un undefined vuelta. Así que para nuestro propósito _muy limitado_ solamente, son lo mismo.

Como no especificamos la clave, Ember usa @identity por defecto. Además, dado que son colisiones, terminamos con algo como esto:

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

Ahora, digamos que hacemos clic en la primera casilla de verificación:

  1. Activa el comportamiento predeterminado del cuadro de selección: cambiar el estado marcado a verdadero
  2. Activa el evento click, que es interceptado por el modificador {{action}} y reenviado al método makeChange
  3. Cambia la lista a [true, undefined, undefined, undefined, undefined] .
  4. Actualiza el DOM.

¿Cómo se actualiza el DOM?

  1. Busque una fila existente cuyos datos coincidan ( === ) con el primer objeto true . Como no se encontró nada, inserte (anteponga) una nueva fila <input checked=true ...>0: true...
  2. Busque una fila existente cuyos datos coincidan ( === ) con el segundo objeto undefined . Encontrado <input ...>0: ... (anteriormente la PRIMERA fila):
    2.1. Actualice el nodo de texto {{idx}} a 1
    2.2. De lo contrario, por lo que Ember puede decir, nada más ha cambiado en esta fila, nada más que hacer.
  3. Busque una fila existente cuyos datos coincidan ( === ) con el tercer objeto undefined . Como esta es la segunda vez que vimos undefined , la clave interna es undefined-1 , por lo que encontramos <input ...>1: ... (anteriormente la SEGUNDA fila):
    3.1. Actualice el nodo de texto {{idx}} a 2
    3.2. De lo contrario, por lo que Ember puede decir, nada más ha cambiado en esta fila, nada más que hacer.
  4. De manera similar, actualice undefined-2 y undefined-3
  5. Finalmente, elimine la fila undefined-4 sin igual (ya que hay un undefined en la matriz después de la actualización)

Así que esto explica cómo obtuvimos el resultado que tenías en el twiddle. Esencialmente, todas las filas DOM se desplazaron hacia abajo en una, y se insertó una nueva en la parte superior, mientras que {{idx}} se actualiza para el resto.

La parte realmente inesperada es 2.2. Aunque la primera casilla de verificación (en la que se hizo clic) se desplazó una fila hacia abajo hasta la segunda posición, probablemente habría esperado que Ember en su propiedad checked haya cambiado a true , y dado que su valor límite no está definido, puede esperar que Ember lo cambie de nuevo a false , desmarcándolo.

Pero no es así como funciona. Como se mencionó al principio, acceder a DOM es caro. Esto incluye _lectura_ de DOM. Si, en cada actualización, tuviéramos que leer el último valor del DOM para nuestras comparaciones, prácticamente frustraría el propósito de nuestras optimizaciones. Por lo tanto, para evitar eso, recordamos el último valor que habíamos escrito en el DOM y comparamos el valor actual con el valor en caché sin tener que volver a leerlo desde el DOM. Solo cuando hay una diferencia, escribimos el nuevo valor en el DOM (y lo almacenamos en caché para la próxima vez). Este es el sentido en el que compartimos el mismo enfoque de "DOM virtual", pero solo lo hacemos en los nodos hoja, sin virtualizar el "árbol" de todo el DOM.

Entonces, TL; DR, "vincular" la propiedad checked (o la propiedad value de un campo de texto, etc.) no funciona realmente de la forma esperada. Imagínese si renderizó <div>{{this.name}}</div> y actualizó manualmente el textContent del elemento div usando jQuery o con el inspector de Chrome. No habrías esperado que Ember se diera cuenta de eso y actualizara this.name por ti. Esto es básicamente lo mismo: dado que la actualización de la propiedad checked ocurrió fuera de Ember (a través del comportamiento predeterminado del navegador para la casilla de verificación), Ember no va a saber nada de eso.

Por eso existe el asistente {{input}} . Tiene que registrar los oyentes de eventos relevantes en el elemento HTML subyacente y reflejar las operaciones en el cambio de propiedad apropiado, de modo que las partes interesadas (por ejemplo, la capa de representación) puedan ser notificadas.

No estoy seguro de dónde nos deja eso. Entiendo por qué esto es sorprendente, pero me inclino a decir que se trata de una serie de errores de usuario desafortunados. ¿Quizás deberíamos estar enfrascados en no vincular estas propiedades a los elementos de entrada?

Todos 8 comentarios

@andrewtimberlake parece que usar {{#each range key="@index" as |value idx|}} soluciona el problema.

Pero parece un error, el key es para un propósito diferente, https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor= cada

Creo que sé lo que está pasando aquí. Es un desastre pero intentaré describirlo. A esto contribuyeron muchos casos extremos (error de usuario de la línea límite), y no estoy realmente seguro de qué es / no es un error, qué y cómo solucionarlos.

Mayor 🔑

Primero que nada, necesito describir lo que hace el parámetro key en {{#each}} . TL; DR está tratando de determinar cuándo y si tendría sentido reutilizar el DOM existente, en lugar de simplemente crear el DOM desde cero.

Para nuestro propósito, aceptemos como un hecho que "tocar DOM" (por ejemplo, actualizar el contenido de un nodo de texto, un atributo, agregar o eliminar contenido, etc.) es costoso y debe evitarse tanto como sea posible.

Centrémonos en una plantilla bastante simple:

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

Si this.names es ...

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

Entonces obtendrás ...

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

Hasta aquí todo bien.

Agregar un elemento a la lista

Ahora, ¿qué pasa si agregamos { first: "Andrew", last: "Timberlake" } a la lista? Esperaríamos que la plantilla produjera el siguiente DOM:

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

Pero cómo_?

La forma más ingenua de implementar el asistente {{#each}} borraría todo el contenido de la lista cada vez que cambia el contenido de la lista. Para hacer esto, necesitaría realizar _ al menos_ 23 operaciones:

  • Eliminar 3 <li> nodos
  • Inserte 4 <li> nodos
  • Inserte 12 nodos de texto (uno para el nombre, uno para el espacio intermedio y uno para el apellido, multiplicado por 4 filas)
  • Invocar el ayudante to-upper-case 4 veces

Esto parece ... muy innecesario y caro. _Sabemos_ que los primeros tres elementos no cambiaron, por lo que sería bueno si pudiéramos omitir el trabajo para esas filas.

🔑 @index

Una mejor implementación sería intentar reutilizar las filas existentes y no hacer actualizaciones innecesarias. Una idea sería simplemente hacer coincidir las filas con sus posiciones en las plantillas. Esto es esencialmente lo que hace key="@index" :

  1. Compare el primer objeto { first: "Yehuda", last: "Katz" } con la primera fila, <li>Yehuda KATZ</li> :
    1.1. "Yehuda" === "Yehuda", nada que hacer
    1.2. (el espacio no contiene datos dinámicos, por lo que no se necesita comparación)
    1.3. "Katz" === "Katz", dado que los ayudantes son "puros", sabemos que no tendremos que volver a invocar el ayudante to-upper-case y, por lo tanto, conocemos la salida de ese ayudante ("KATZ" ) _también_ no cambió, así que no hay nada que hacer aquí
  2. Del mismo modo, nada que hacer para las filas 2 y 3
  3. No hay una cuarta fila en el DOM, así que inserte una nueva
    3.1. Inserte un nodo <li>
    3.2. Insertar un nodo de texto ("Andrew")
    3.3. Insertar un nodo de texto (el espacio)
    3.4. Invocar el to-upper-case helper ("Timberlake" -> "TIMBERLAKE")
    3.5. Insertar un nodo de texto ("TIMBERLAKE")

Entonces, con esta implementación, redujimos el número total de operaciones de 23 a 5 (👋 agitando la mano sobre el costo de las comparaciones, pero para nuestro propósito, asumimos que son relativamente baratas en comparación con el resto). No está mal.

Agregar un elemento a la lista

Pero ahora, ¿qué pasaría si, en lugar de _anexar_ { first: "Andrew", last: "Timberlake" } a la lista, lo _prependemos_ en su lugar? Esperaríamos que la plantilla produjera el siguiente DOM:

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

Pero cómo_?

  1. Compare el primer objeto { first: "Andrew", last: "Timberlake" } con la primera fila, <li>Yehuda KATZ</li> :
    1.1. "Andrew"! == "Yehuda", actualiza el nodo de texto
    1.2. (el espacio no contiene datos dinámicos, por lo que no se necesita comparación)
    1.3. "Timberlake"! == "Katz", vuelve a invocar el ayudante to-upper-case
    1.4. Actualiza el nodo de texto de "KATZ" a "TIMBERLAKE"
  2. Compare el segundo objeto { first: "Yehuda", last: "Katz" } con la segunda fila, <li>Tom DALE</li> , otras 3 operaciones
  3. Compare el segundo objeto { first: "Tom", last: "Dale" } con la segunda fila, <li>Godfrey CHAN</li> , otras 3 operaciones
  4. No hay una cuarta fila en el DOM, así que inserte una nueva
    3.1. Inserte un nodo <li>
    3.2. Insertar un nodo de texto ("Godfrey")
    3.3. Insertar un nodo de texto (el espacio)
    3.4. Invocar el to-upper-case helper ("Chan" -> "CHAN")
    3.5. Insertar un nodo de texto ("CHAN")

Son 14 operaciones. ¡Ay!

🔑 @identidad

Eso parecía innecesario, porque conceptualmente, ya sea que estemos anteponiendo o agregando, todavía solo estamos cambiando (insertando) un solo objeto en la matriz. De manera óptima, deberíamos poder manejar este caso tan bien como lo hicimos en el escenario de adición.

Aquí es donde entra key="@identity" . En lugar de confiar en el _orden_ de los elementos en la matriz, usamos su identidad de objeto JavaScript ( === ):

  1. Busque una fila existente cuyos datos coincidan ( === ) con el primer objeto { first: "Andrew", last: "Timberlake" } . Como no se encontró nada, inserte (anteponga) una nueva fila:
    1.1. Inserte un nodo <li>
    1.2. Insertar un nodo de texto ("Andrew")
    1.3. Insertar un nodo de texto (el espacio)
    1.4. Invocar el to-upper-case helper ("Timberlake" -> "TIMBERLAKE")
    1.5. Insertar un nodo de texto ("TIMBERLAKE")
  2. Busque una fila existente cuyos datos coincidan ( === ) con el segundo objeto { first: "Yehuda", last: "Katz" } . Encontrado <li>Yehuda KATZ</li> :
    2.1. "Yehuda" === "Yehuda", nada que hacer
    2.2. (el espacio no contiene datos dinámicos, por lo que no se necesita comparación)
    2.3. "Katz" === "Katz", dado que los ayudantes son "puros", sabemos que no tendremos que volver a invocar el ayudante to-upper-case y, por lo tanto, conocemos la salida de ese ayudante ("KATZ" ) _también_ no cambió, así que no hay nada que hacer aquí
  3. Del mismo modo, nada que hacer por las filas de Tom y Godfrey
  4. Elimine todas las filas con objetos no coincidentes (ninguno, por lo que no hay nada que hacer en este caso)

Con eso, volvemos a las 5 operaciones óptimas.

Ampliar

Una vez más, se trata de hacer un gesto con la mano sobre las comparaciones y los costos de contabilidad. De hecho, esos tampoco son gratuitos y, en este ejemplo muy simple, puede que no valgan la pena. Pero imagine que la lista es grande y cada fila invoca un componente complicado (con muchos ayudantes, propiedades calculadas, subcomponentes, etc.). Imagínese el servicio de noticias de LinkedIn, por ejemplo. Si no hacemos coincidir las filas correctas con los datos correctos, los argumentos de sus componentes pueden potencialmente agitarse mucho y provocar muchas más actualizaciones DOM de las que podría esperar. También hay problemas para hacer coincidir los elementos DOM incorrectos y perder el estado del DOM, como la posición del cursor y el estado de selección de texto.

En general, el costo adicional de comparación y contabilidad vale la pena la mayor parte del tiempo en una aplicación del mundo real. Dado que key="@identity" es el valor predeterminado en Ember y funciona bien en casi todos los casos, normalmente no tendrá que preocuparse por configurar el argumento key cuando utilice {{#each}} .

Colisiones 💥

Pero espere, hay un problema. ¿Y este caso?

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

El problema aquí es que el mismo objeto _podría_ aparecer varias veces en la misma lista. Esto rompe nuestro ingenuo algoritmo @identity , específicamente la parte en la que dijimos "Encuentra una fila existente cuyos datos coincidan ( === ) ..."; esto solo funciona si la relación de datos a DOM es 1 : 1, que no es cierto en este caso. Esto puede parecer poco probable en la práctica, pero como marco, tenemos que manejarlo.

Para evitar esto, utilizamos una especie de enfoque híbrido para manejar estas colisiones. Internamente, la asignación de claves a DOM se parece a esto:

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

En su mayor parte, esto _es_ bastante raro, y cuando aparece, funciona Good Enough ™ la mayor parte del tiempo. Si, por alguna razón, esto no funciona, siempre puede usar una ruta de clave (o el mecanismo de clave aún más avanzado en RFC 321 ).

Volver a la "🐛"

Después de toda esa charla, ahora estamos listos para ver el escenario en Twiddle.

Básicamente, comenzamos con esta lista: [undefined, undefined, undefined, undefined, undefined] .

Nota no relacionada: Array(5) _no_ es lo mismo que [undefined, undefined, undefined, undefined, undefined] . Produce una "matriz perforada" que es algo que debe evitar en general. Sin embargo, no está relacionado con este error, porque al acceder a los "agujeros", de hecho, obtiene un undefined vuelta. Así que para nuestro propósito _muy limitado_ solamente, son lo mismo.

Como no especificamos la clave, Ember usa @identity por defecto. Además, dado que son colisiones, terminamos con algo como esto:

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

Ahora, digamos que hacemos clic en la primera casilla de verificación:

  1. Activa el comportamiento predeterminado del cuadro de selección: cambiar el estado marcado a verdadero
  2. Activa el evento click, que es interceptado por el modificador {{action}} y reenviado al método makeChange
  3. Cambia la lista a [true, undefined, undefined, undefined, undefined] .
  4. Actualiza el DOM.

¿Cómo se actualiza el DOM?

  1. Busque una fila existente cuyos datos coincidan ( === ) con el primer objeto true . Como no se encontró nada, inserte (anteponga) una nueva fila <input checked=true ...>0: true...
  2. Busque una fila existente cuyos datos coincidan ( === ) con el segundo objeto undefined . Encontrado <input ...>0: ... (anteriormente la PRIMERA fila):
    2.1. Actualice el nodo de texto {{idx}} a 1
    2.2. De lo contrario, por lo que Ember puede decir, nada más ha cambiado en esta fila, nada más que hacer.
  3. Busque una fila existente cuyos datos coincidan ( === ) con el tercer objeto undefined . Como esta es la segunda vez que vimos undefined , la clave interna es undefined-1 , por lo que encontramos <input ...>1: ... (anteriormente la SEGUNDA fila):
    3.1. Actualice el nodo de texto {{idx}} a 2
    3.2. De lo contrario, por lo que Ember puede decir, nada más ha cambiado en esta fila, nada más que hacer.
  4. De manera similar, actualice undefined-2 y undefined-3
  5. Finalmente, elimine la fila undefined-4 sin igual (ya que hay un undefined en la matriz después de la actualización)

Así que esto explica cómo obtuvimos el resultado que tenías en el twiddle. Esencialmente, todas las filas DOM se desplazaron hacia abajo en una, y se insertó una nueva en la parte superior, mientras que {{idx}} se actualiza para el resto.

La parte realmente inesperada es 2.2. Aunque la primera casilla de verificación (en la que se hizo clic) se desplazó una fila hacia abajo hasta la segunda posición, probablemente habría esperado que Ember en su propiedad checked haya cambiado a true , y dado que su valor límite no está definido, puede esperar que Ember lo cambie de nuevo a false , desmarcándolo.

Pero no es así como funciona. Como se mencionó al principio, acceder a DOM es caro. Esto incluye _lectura_ de DOM. Si, en cada actualización, tuviéramos que leer el último valor del DOM para nuestras comparaciones, prácticamente frustraría el propósito de nuestras optimizaciones. Por lo tanto, para evitar eso, recordamos el último valor que habíamos escrito en el DOM y comparamos el valor actual con el valor en caché sin tener que volver a leerlo desde el DOM. Solo cuando hay una diferencia, escribimos el nuevo valor en el DOM (y lo almacenamos en caché para la próxima vez). Este es el sentido en el que compartimos el mismo enfoque de "DOM virtual", pero solo lo hacemos en los nodos hoja, sin virtualizar el "árbol" de todo el DOM.

Entonces, TL; DR, "vincular" la propiedad checked (o la propiedad value de un campo de texto, etc.) no funciona realmente de la forma esperada. Imagínese si renderizó <div>{{this.name}}</div> y actualizó manualmente el textContent del elemento div usando jQuery o con el inspector de Chrome. No habrías esperado que Ember se diera cuenta de eso y actualizara this.name por ti. Esto es básicamente lo mismo: dado que la actualización de la propiedad checked ocurrió fuera de Ember (a través del comportamiento predeterminado del navegador para la casilla de verificación), Ember no va a saber nada de eso.

Por eso existe el asistente {{input}} . Tiene que registrar los oyentes de eventos relevantes en el elemento HTML subyacente y reflejar las operaciones en el cambio de propiedad apropiado, de modo que las partes interesadas (por ejemplo, la capa de representación) puedan ser notificadas.

No estoy seguro de dónde nos deja eso. Entiendo por qué esto es sorprendente, pero me inclino a decir que se trata de una serie de errores de usuario desafortunados. ¿Quizás deberíamos estar enfrascados en no vincular estas propiedades a los elementos de entrada?

@chancancode : gracias por la increíble explicación. ¿Significa eso que nunca se debe usar <input ... > sino solo {{input ...}} para evitar errores como ese?

@ boris-petrov puede haber algunos casos limitados en los que sea aceptable ... como un campo de texto de solo lectura para "copiar esta URL en el portapapeles", o puede_ usar el elemento de entrada + {{action}} para interceptar el DOM y reflejan las actualizaciones de propiedades manualmente (que es lo que intentó hacer el twiddle, excepto que también se encontró con la colisión @identity ), pero sí, en algún momento, simplemente está reimplementando {{input}} y manejando todos los casos extremos que ya manejó por usted. Así que creo que es _probablemente_ justo decir que debería usar {{input}} mayoría de las veces, si no todas.

Sin embargo, eso todavía no habría "arreglado" este caso donde hay colisiones con las llaves. Consulte https://ember-twiddle.com/0f2369021128e2ae0c445155df5bb034?openFiles=templates.application.hbs%2C

Por eso dije, no estoy 100% seguro de qué hacer al respecto. Por un lado, estoy de acuerdo en que es sorprendente e inesperado, por otro lado, este tipo de colisión es bastante raro en aplicaciones reales y es por eso que el argumento "clave" es personalizable (este es un caso en el que la clave predeterminada "@identity" función no es Good Enough ™, por lo que existe esa función).

@chancancode : esto me recuerda otro problema que abrí hace algún tiempo . ¿Crees que hay algo parecido ahí? La respuesta que recibí allí (sobre la necesidad de usar replace lugar de set al configurar elementos de matriz) todavía me parece extraña.

@ boris-petrov no creo que esté relacionado

hola, usamos sortablejs para la lista arrastrable con ember. Por favor, consulte esta demostración para reproducir cada problema.

paso:

  • arrastra cualquier elemento para que dure
  • cambiar seleccione a 'v2'

puede ver que el elemento arrastrado permanece en el árbol dom.

pero, si arrastra el elemento a otra posición (no al último elemento), parece que funciona bien.

¿Fue útil esta página
0 / 5 - 0 calificaciones