Ember.js: #each bug de re-renderização

Criado em 14 set. 2018  ·  8Comentários  ·  Fonte: emberjs/ember.js

Eu tenho uma matriz de valores verdadeiro / falso / indefinido que estou processando como uma lista de caixas de seleção.
Ao alterar um elemento da matriz para ou de verdadeiro, a lista de caixas de seleção é renderizada novamente com a seguinte caixa de seleção (índice + 1), herdando a alteração junto com a caixa de seleção alterada.
Código:

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

Quando eu uso {{#each range key="@index" as |value idx|}} ele funciona corretamente.

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

embereach

Bug Has Reproduction Rendering

Comentários muito úteis

Acho que sei o que está acontecendo aqui. É uma bagunça, mas vou tentar descrever. Muitos casos extremos (erro do usuário na fronteira) contribuíram para isso, e não tenho certeza do que é / não é um bug, o que e como consertar qualquer um deles.

Maior 🔑

Em primeiro lugar, preciso descrever o que o parâmetro key faz em {{#each}} . TL; DR está tentando determinar quando e se faria sentido reutilizar o DOM existente, em vez de apenas criar o DOM do zero.

Para nosso propósito, vamos aceitar que "tocar no DOM" (por exemplo, atualizar o conteúdo de um nó de texto, um atributo, adicionar ou remover conteúdo etc.) é caro e deve ser evitado tanto quanto possível.

Vamos nos concentrar em um modelo bastante simples:

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

Se this.names for ...

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

Então você vai conseguir ...

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

Por enquanto, tudo bem.

Anexando um item à lista

Agora, e se adicionarmos { first: "Andrew", last: "Timberlake" } à lista? Esperamos que o modelo produza o seguinte DOM:

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

Mas como_?

A maneira mais ingênua de implementar o auxiliar {{#each}} limparia todo o conteúdo da lista sempre que o conteúdo da lista mudasse. Para fazer isso, você precisaria realizar _pelo menos_ 23 operações:

  • Remova 3 <li> nodes
  • Insira 4 <li> nodes
  • Insira 12 nós de texto (um para o nome, um para o espaço entre e um para o sobrenome, vezes 4 linhas)
  • Invoque o ajudante to-upper-case 4 vezes

Isso parece ... muito desnecessário e caro. _Sabemos_ que os três primeiros itens não mudaram, então seria bom se pudéssemos simplesmente pular o trabalho para essas linhas.

🔑 @index

Uma implementação melhor seria tentar reutilizar as linhas existentes e não fazer nenhuma atualização desnecessária. Uma ideia seria simplesmente combinar as linhas com suas posições nos modelos. Isso é essencialmente o que key="@index" faz:

  1. Compare o primeiro objeto { first: "Yehuda", last: "Katz" } com a primeira linha, <li>Yehuda KATZ</li> :
    1.1. "Yehuda" === "Yehuda", nada a fazer
    1.2. (o espaço não contém dados dinâmicos, portanto, nenhuma comparação necessária)
    1.3. "Katz" === "Katz", uma vez que os ajudantes são "puros", sabemos que não teremos que invocar novamente o ajudante to-upper-case e, portanto, sabemos a saída desse ajudante ("KATZ" ) _também_ não mudou, então nada a fazer aqui
  2. Da mesma forma, nada a fazer para as linhas 2 e 3
  3. Não há uma quarta linha no DOM, então insira uma nova
    3.1. Insira um nó <li>
    3.2. Insira um nó de texto ("Andrew")
    3.3. Insira um nó de texto (o espaço)
    3.4. Invoque o to-upper-case helper ("Timberlake" -> "TIMBERLAKE")
    3,5. Insira um nó de texto ("TIMBERLAKE")

Portanto, com esta implementação, reduzimos o número total de operações de 23 para 5 (👋 acenando com o custo das comparações, mas para nosso propósito, estamos assumindo que elas são relativamente baratas em comparação com o resto). Não é ruim.

Incluindo um item na lista

Mas agora, o que aconteceria se, em vez de _appender_ { first: "Andrew", last: "Timberlake" } à lista, nós o _prendêssemos_? Esperamos que o modelo produza o seguinte DOM:

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

Mas como_?

  1. Compare o primeiro objeto { first: "Andrew", last: "Timberlake" } com a primeira linha, <li>Yehuda KATZ</li> :
    1.1. "Andrew"! == "Yehuda", atualize o nó de texto
    1.2. (o espaço não contém dados dinâmicos, portanto, nenhuma comparação necessária)
    1.3. "Timberlake"! == "Katz", invoque novamente o ajudante to-upper-case
    1.4. Atualize o nó de texto de "KATZ" para "TIMBERLAKE"
  2. Compare o segundo objeto { first: "Yehuda", last: "Katz" } com a segunda linha, <li>Tom DALE</li> , outra operação 3
  3. Compare o segundo objeto { first: "Tom", last: "Dale" } com a segunda linha, <li>Godfrey CHAN</li> , outra 3 operação
  4. Não há uma quarta linha no DOM, então insira uma nova
    3.1. Insira um nó <li>
    3.2. Insira um nó de texto ("Godfrey")
    3.3. Insira um nó de texto (o espaço)
    3.4. Invoque o ajudante to-upper-case ("Chan" -> "CHAN")
    3,5. Insira um nó de texto ("CHAN")

São 14 operações. Ai!

🔑 @identity

Isso parecia desnecessário, porque conceitualmente, quer estejamos prefixando ou acrescentando, ainda estamos apenas alterando (inserindo) um único objeto no array. Idealmente, devemos ser capazes de lidar com este caso tão bem como fizemos no cenário de acréscimo.

É aqui que entra key="@identity" . Em vez de depender da _ordem_ dos elementos no array, usamos sua identidade de objeto JavaScript ( === ):

  1. Encontre uma linha existente cujos dados correspondam ( === ) ao primeiro objeto { first: "Andrew", last: "Timberlake" } . Como nada foi encontrado, insira (prefixe) uma nova linha:
    1.1. Insira um nó <li>
    1.2. Insira um nó de texto ("Andrew")
    1.3. Insira um nó de texto (o espaço)
    1.4. Invoque o to-upper-case helper ("Timberlake" -> "TIMBERLAKE")
    1,5. Insira um nó de texto ("TIMBERLAKE")
  2. Encontre uma linha existente cujos dados correspondam ( === ) ao segundo objeto { first: "Yehuda", last: "Katz" } . Encontrado <li>Yehuda KATZ</li> :
    2.1. "Yehuda" === "Yehuda", nada a fazer
    2.2. (o espaço não contém dados dinâmicos, portanto, nenhuma comparação necessária)
    2.3. "Katz" === "Katz", uma vez que os ajudantes são "puros", sabemos que não teremos que invocar novamente o ajudante to-upper-case e, portanto, sabemos a saída desse ajudante ("KATZ" ) _também_ não mudou, então nada a fazer aqui
  3. Da mesma forma, nada a fazer para as brigas de Tom e Godfrey
  4. Remova todas as linhas com objetos sem correspondência (nenhum, portanto, nada a fazer neste caso)

Com isso, estamos de volta às 5 operações ideais.

Ampliando

Novamente, isso é 👋 acenando as comparações e os custos de contabilidade. Na verdade, eles também não são gratuitos e, neste exemplo muito simples, podem não valer a pena. Mas imagine que a lista é grande e cada linha invoca um componente complicado (com muitos auxiliares, propriedades computadas, subcomponentes, etc). Imagine o feed de notícias do LinkedIn, por exemplo. Se não combinarmos as linhas corretas com os dados corretos, os argumentos dos seus componentes podem se alterar muito e causar muito mais atualizações de DOM do que você poderia esperar. Também há problemas em combinar os elementos DOM errados e perder o estado DOM, como a posição do cursor e o estado de seleção de texto.

No geral, o custo extra de comparação e contabilidade vale facilmente a maior parte do tempo em um aplicativo do mundo real. Visto que key="@identity" é o padrão no Ember e funciona bem para quase todos os casos, você normalmente não precisa se preocupar em definir o argumento key ao usar {{#each}} .

Colisões 💥

Mas espere, há um problema. Que tal 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
];

O problema aqui é que o mesmo objeto _pode_ aparecer várias vezes na mesma lista. Isso quebra nosso algoritmo @identity ingênuo, especificamente a parte em que dissemos "Encontre uma linha existente cujos dados correspondam ( === ) ..." - isso só funciona se os dados para o relacionamento DOM forem 1 : 1, o que não é verdade neste caso. Isso pode parecer improvável na prática, mas como estrutura, temos que lidar com isso.

Para evitar isso, usamos uma abordagem híbrida para lidar com essas colisões. Internamente, o mapeamento de chaves para DOM se parece com isto:

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

Na maioria das vezes, isso _é_ muito raro e, quando surge, funciona Good Enough ™ na maioria das vezes. Se, por algum motivo, isso não funcionar, você sempre pode usar um caminho de chave (ou o mecanismo de codificação ainda mais avançado no RFC 321 ).

Voltar para o "🐛"

Depois de toda essa conversa, agora estamos prontos para examinar o cenário do Twiddle.

Essencialmente, começamos com esta lista: [undefined, undefined, undefined, undefined, undefined] .

Nota não relacionada: Array(5) _não_ é a mesma coisa que [undefined, undefined, undefined, undefined, undefined] . Ele produz uma "matriz furada" que é algo que você deve evitar em geral. No entanto, não está relacionado a este bug, porque ao acessar os "buracos" você realmente recebe undefined volta. Portanto, para o nosso propósito _muito estreito_ apenas, eles são os mesmos.

Como não especificamos a chave, o Ember usa @identity por padrão. Além disso, como são colisões, acabamos com algo assim:

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

Agora, digamos que clicamos na primeira caixa de seleção:

  1. Ele aciona o comportamento padrão da caixa de seleção: alterar o estado marcado para verdadeiro
  2. Ele aciona o evento click, que é interceptado pelo modificador {{action}} e reenviado para o método makeChange
  3. Ele muda a lista para [true, undefined, undefined, undefined, undefined] .
  4. Ele atualiza o DOM.

Como o DOM é atualizado?

  1. Encontre uma linha existente cujos dados correspondam ( === ) ao primeiro objeto true . Uma vez que nada foi encontrado, insira (prefixe) uma nova linha <input checked=true ...>0: true...
  2. Encontre uma linha existente cujos dados correspondam ( === ) ao segundo objeto undefined . Encontrado <input ...>0: ... (anteriormente a PRIMEIRA linha):
    2.1. Atualize o nó de texto {{idx}} para 1
    2.2. Do contrário, pelo que Ember sabe, nada mais mudou nesta linha, nada mais a fazer
  3. Encontre uma linha existente cujos dados correspondam ( === ) ao terceiro objeto undefined . Como esta é a segunda vez que vimos undefined , a chave interna é undefined-1 , então encontramos <input ...>1: ... (anteriormente a SEGUNDA linha):
    3.1. Atualize o nó de texto {{idx}} para 2
    3.2. Do contrário, pelo que Ember sabe, nada mais mudou nesta linha, nada mais a fazer
  4. Da mesma forma, atualize undefined-2 e undefined-3
  5. Finalmente, remova a linha undefined-4 (uma vez que há um undefined na matriz após a atualização)

Isso explica como obtivemos a saída que você obteve no twiddle. Essencialmente, todas as linhas do DOM foram reduzidas em um, e um novo foi inserido no topo, enquanto {{idx}} é atualizado para o resto.

A parte realmente inesperada é 2.2. Mesmo que a primeira caixa de seleção (aquela que foi clicada) tenha sido deslocada para baixo em uma linha para a segunda posição, você provavelmente esperaria que Ember sua propriedade checked mudou para true , visto que seu valor limite é indefinido, você pode esperar que o Ember altere-o de volta para false , desmarcando-o.

Mas não é assim que funciona. Como mencionado no início, acessar o DOM é caro. Isso inclui _reading_ de DOM. Se, em cada atualização, tivéssemos que ler o valor mais recente do DOM para nossas comparações, isso praticamente anularia o propósito de nossas otimizações. Portanto, para evitar isso, lembramos o último valor que gravamos no DOM e comparamos o valor atual com o valor armazenado em cache sem ter que lê-lo de volta no DOM. Somente quando há uma diferença, gravamos o novo valor no DOM (e o armazenamos em cache da próxima vez). Este é o sentido em que compartilhamos a mesma abordagem de "DOM virtual", mas apenas o fazemos nos nós folha, não virtualizando a "árvore" de todo o DOM.

Portanto, TL; DR, "vincular" a propriedade checked (ou a propriedade value de um campo de texto, etc) realmente não funciona da maneira que você espera. Imagine se você renderizou <div>{{this.name}}</div> e atualizou manualmente o textContent do elemento div usando jQuery ou com o inspetor de cromo. Você não esperaria que o Ember notasse isso e atualizasse this.name para você. Basicamente, é a mesma coisa: como a atualização da propriedade checked aconteceu fora do Ember (por meio do comportamento padrão do navegador para a caixa de seleção), o Ember não saberá disso.

É por isso que o ajudante {{input}} existe. Ele deve registrar os ouvintes de eventos relevantes no elemento HTML subjacente e refletir as operações na mudança de propriedade apropriada, para que as partes interessadas (por exemplo, a camada de renderização) possam ser notificadas.

Não tenho certeza de onde isso nos deixa. Eu entendo por que isso é surpreendente, mas estou inclinado a dizer que essa é uma série de erros de usuários infelizes. Talvez devêssemos evitar vincular essas propriedades aos elementos de entrada?

Todos 8 comentários

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

Mas parece um bug, o key tem um propósito diferente, https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor= cada

Acho que sei o que está acontecendo aqui. É uma bagunça, mas vou tentar descrever. Muitos casos extremos (erro do usuário na fronteira) contribuíram para isso, e não tenho certeza do que é / não é um bug, o que e como consertar qualquer um deles.

Maior 🔑

Em primeiro lugar, preciso descrever o que o parâmetro key faz em {{#each}} . TL; DR está tentando determinar quando e se faria sentido reutilizar o DOM existente, em vez de apenas criar o DOM do zero.

Para nosso propósito, vamos aceitar que "tocar no DOM" (por exemplo, atualizar o conteúdo de um nó de texto, um atributo, adicionar ou remover conteúdo etc.) é caro e deve ser evitado tanto quanto possível.

Vamos nos concentrar em um modelo bastante simples:

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

Se this.names for ...

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

Então você vai conseguir ...

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

Por enquanto, tudo bem.

Anexando um item à lista

Agora, e se adicionarmos { first: "Andrew", last: "Timberlake" } à lista? Esperamos que o modelo produza o seguinte DOM:

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

Mas como_?

A maneira mais ingênua de implementar o auxiliar {{#each}} limparia todo o conteúdo da lista sempre que o conteúdo da lista mudasse. Para fazer isso, você precisaria realizar _pelo menos_ 23 operações:

  • Remova 3 <li> nodes
  • Insira 4 <li> nodes
  • Insira 12 nós de texto (um para o nome, um para o espaço entre e um para o sobrenome, vezes 4 linhas)
  • Invoque o ajudante to-upper-case 4 vezes

Isso parece ... muito desnecessário e caro. _Sabemos_ que os três primeiros itens não mudaram, então seria bom se pudéssemos simplesmente pular o trabalho para essas linhas.

🔑 @index

Uma implementação melhor seria tentar reutilizar as linhas existentes e não fazer nenhuma atualização desnecessária. Uma ideia seria simplesmente combinar as linhas com suas posições nos modelos. Isso é essencialmente o que key="@index" faz:

  1. Compare o primeiro objeto { first: "Yehuda", last: "Katz" } com a primeira linha, <li>Yehuda KATZ</li> :
    1.1. "Yehuda" === "Yehuda", nada a fazer
    1.2. (o espaço não contém dados dinâmicos, portanto, nenhuma comparação necessária)
    1.3. "Katz" === "Katz", uma vez que os ajudantes são "puros", sabemos que não teremos que invocar novamente o ajudante to-upper-case e, portanto, sabemos a saída desse ajudante ("KATZ" ) _também_ não mudou, então nada a fazer aqui
  2. Da mesma forma, nada a fazer para as linhas 2 e 3
  3. Não há uma quarta linha no DOM, então insira uma nova
    3.1. Insira um nó <li>
    3.2. Insira um nó de texto ("Andrew")
    3.3. Insira um nó de texto (o espaço)
    3.4. Invoque o to-upper-case helper ("Timberlake" -> "TIMBERLAKE")
    3,5. Insira um nó de texto ("TIMBERLAKE")

Portanto, com esta implementação, reduzimos o número total de operações de 23 para 5 (👋 acenando com o custo das comparações, mas para nosso propósito, estamos assumindo que elas são relativamente baratas em comparação com o resto). Não é ruim.

Incluindo um item na lista

Mas agora, o que aconteceria se, em vez de _appender_ { first: "Andrew", last: "Timberlake" } à lista, nós o _prendêssemos_? Esperamos que o modelo produza o seguinte DOM:

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

Mas como_?

  1. Compare o primeiro objeto { first: "Andrew", last: "Timberlake" } com a primeira linha, <li>Yehuda KATZ</li> :
    1.1. "Andrew"! == "Yehuda", atualize o nó de texto
    1.2. (o espaço não contém dados dinâmicos, portanto, nenhuma comparação necessária)
    1.3. "Timberlake"! == "Katz", invoque novamente o ajudante to-upper-case
    1.4. Atualize o nó de texto de "KATZ" para "TIMBERLAKE"
  2. Compare o segundo objeto { first: "Yehuda", last: "Katz" } com a segunda linha, <li>Tom DALE</li> , outra operação 3
  3. Compare o segundo objeto { first: "Tom", last: "Dale" } com a segunda linha, <li>Godfrey CHAN</li> , outra 3 operação
  4. Não há uma quarta linha no DOM, então insira uma nova
    3.1. Insira um nó <li>
    3.2. Insira um nó de texto ("Godfrey")
    3.3. Insira um nó de texto (o espaço)
    3.4. Invoque o ajudante to-upper-case ("Chan" -> "CHAN")
    3,5. Insira um nó de texto ("CHAN")

São 14 operações. Ai!

🔑 @identity

Isso parecia desnecessário, porque conceitualmente, quer estejamos prefixando ou acrescentando, ainda estamos apenas alterando (inserindo) um único objeto no array. Idealmente, devemos ser capazes de lidar com este caso tão bem como fizemos no cenário de acréscimo.

É aqui que entra key="@identity" . Em vez de depender da _ordem_ dos elementos no array, usamos sua identidade de objeto JavaScript ( === ):

  1. Encontre uma linha existente cujos dados correspondam ( === ) ao primeiro objeto { first: "Andrew", last: "Timberlake" } . Como nada foi encontrado, insira (prefixe) uma nova linha:
    1.1. Insira um nó <li>
    1.2. Insira um nó de texto ("Andrew")
    1.3. Insira um nó de texto (o espaço)
    1.4. Invoque o to-upper-case helper ("Timberlake" -> "TIMBERLAKE")
    1,5. Insira um nó de texto ("TIMBERLAKE")
  2. Encontre uma linha existente cujos dados correspondam ( === ) ao segundo objeto { first: "Yehuda", last: "Katz" } . Encontrado <li>Yehuda KATZ</li> :
    2.1. "Yehuda" === "Yehuda", nada a fazer
    2.2. (o espaço não contém dados dinâmicos, portanto, nenhuma comparação necessária)
    2.3. "Katz" === "Katz", uma vez que os ajudantes são "puros", sabemos que não teremos que invocar novamente o ajudante to-upper-case e, portanto, sabemos a saída desse ajudante ("KATZ" ) _também_ não mudou, então nada a fazer aqui
  3. Da mesma forma, nada a fazer para as brigas de Tom e Godfrey
  4. Remova todas as linhas com objetos sem correspondência (nenhum, portanto, nada a fazer neste caso)

Com isso, estamos de volta às 5 operações ideais.

Ampliando

Novamente, isso é 👋 acenando as comparações e os custos de contabilidade. Na verdade, eles também não são gratuitos e, neste exemplo muito simples, podem não valer a pena. Mas imagine que a lista é grande e cada linha invoca um componente complicado (com muitos auxiliares, propriedades computadas, subcomponentes, etc). Imagine o feed de notícias do LinkedIn, por exemplo. Se não combinarmos as linhas corretas com os dados corretos, os argumentos dos seus componentes podem se alterar muito e causar muito mais atualizações de DOM do que você poderia esperar. Também há problemas em combinar os elementos DOM errados e perder o estado DOM, como a posição do cursor e o estado de seleção de texto.

No geral, o custo extra de comparação e contabilidade vale facilmente a maior parte do tempo em um aplicativo do mundo real. Visto que key="@identity" é o padrão no Ember e funciona bem para quase todos os casos, você normalmente não precisa se preocupar em definir o argumento key ao usar {{#each}} .

Colisões 💥

Mas espere, há um problema. Que tal 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
];

O problema aqui é que o mesmo objeto _pode_ aparecer várias vezes na mesma lista. Isso quebra nosso algoritmo @identity ingênuo, especificamente a parte em que dissemos "Encontre uma linha existente cujos dados correspondam ( === ) ..." - isso só funciona se os dados para o relacionamento DOM forem 1 : 1, o que não é verdade neste caso. Isso pode parecer improvável na prática, mas como estrutura, temos que lidar com isso.

Para evitar isso, usamos uma abordagem híbrida para lidar com essas colisões. Internamente, o mapeamento de chaves para DOM se parece com isto:

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

Na maioria das vezes, isso _é_ muito raro e, quando surge, funciona Good Enough ™ na maioria das vezes. Se, por algum motivo, isso não funcionar, você sempre pode usar um caminho de chave (ou o mecanismo de codificação ainda mais avançado no RFC 321 ).

Voltar para o "🐛"

Depois de toda essa conversa, agora estamos prontos para examinar o cenário do Twiddle.

Essencialmente, começamos com esta lista: [undefined, undefined, undefined, undefined, undefined] .

Nota não relacionada: Array(5) _não_ é a mesma coisa que [undefined, undefined, undefined, undefined, undefined] . Ele produz uma "matriz furada" que é algo que você deve evitar em geral. No entanto, não está relacionado a este bug, porque ao acessar os "buracos" você realmente recebe undefined volta. Portanto, para o nosso propósito _muito estreito_ apenas, eles são os mesmos.

Como não especificamos a chave, o Ember usa @identity por padrão. Além disso, como são colisões, acabamos com algo assim:

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

Agora, digamos que clicamos na primeira caixa de seleção:

  1. Ele aciona o comportamento padrão da caixa de seleção: alterar o estado marcado para verdadeiro
  2. Ele aciona o evento click, que é interceptado pelo modificador {{action}} e reenviado para o método makeChange
  3. Ele muda a lista para [true, undefined, undefined, undefined, undefined] .
  4. Ele atualiza o DOM.

Como o DOM é atualizado?

  1. Encontre uma linha existente cujos dados correspondam ( === ) ao primeiro objeto true . Uma vez que nada foi encontrado, insira (prefixe) uma nova linha <input checked=true ...>0: true...
  2. Encontre uma linha existente cujos dados correspondam ( === ) ao segundo objeto undefined . Encontrado <input ...>0: ... (anteriormente a PRIMEIRA linha):
    2.1. Atualize o nó de texto {{idx}} para 1
    2.2. Do contrário, pelo que Ember sabe, nada mais mudou nesta linha, nada mais a fazer
  3. Encontre uma linha existente cujos dados correspondam ( === ) ao terceiro objeto undefined . Como esta é a segunda vez que vimos undefined , a chave interna é undefined-1 , então encontramos <input ...>1: ... (anteriormente a SEGUNDA linha):
    3.1. Atualize o nó de texto {{idx}} para 2
    3.2. Do contrário, pelo que Ember sabe, nada mais mudou nesta linha, nada mais a fazer
  4. Da mesma forma, atualize undefined-2 e undefined-3
  5. Finalmente, remova a linha undefined-4 (uma vez que há um undefined na matriz após a atualização)

Isso explica como obtivemos a saída que você obteve no twiddle. Essencialmente, todas as linhas do DOM foram reduzidas em um, e um novo foi inserido no topo, enquanto {{idx}} é atualizado para o resto.

A parte realmente inesperada é 2.2. Mesmo que a primeira caixa de seleção (aquela que foi clicada) tenha sido deslocada para baixo em uma linha para a segunda posição, você provavelmente esperaria que Ember sua propriedade checked mudou para true , visto que seu valor limite é indefinido, você pode esperar que o Ember altere-o de volta para false , desmarcando-o.

Mas não é assim que funciona. Como mencionado no início, acessar o DOM é caro. Isso inclui _reading_ de DOM. Se, em cada atualização, tivéssemos que ler o valor mais recente do DOM para nossas comparações, isso praticamente anularia o propósito de nossas otimizações. Portanto, para evitar isso, lembramos o último valor que gravamos no DOM e comparamos o valor atual com o valor armazenado em cache sem ter que lê-lo de volta no DOM. Somente quando há uma diferença, gravamos o novo valor no DOM (e o armazenamos em cache da próxima vez). Este é o sentido em que compartilhamos a mesma abordagem de "DOM virtual", mas apenas o fazemos nos nós folha, não virtualizando a "árvore" de todo o DOM.

Portanto, TL; DR, "vincular" a propriedade checked (ou a propriedade value de um campo de texto, etc) realmente não funciona da maneira que você espera. Imagine se você renderizou <div>{{this.name}}</div> e atualizou manualmente o textContent do elemento div usando jQuery ou com o inspetor de cromo. Você não esperaria que o Ember notasse isso e atualizasse this.name para você. Basicamente, é a mesma coisa: como a atualização da propriedade checked aconteceu fora do Ember (por meio do comportamento padrão do navegador para a caixa de seleção), o Ember não saberá disso.

É por isso que o ajudante {{input}} existe. Ele deve registrar os ouvintes de eventos relevantes no elemento HTML subjacente e refletir as operações na mudança de propriedade apropriada, para que as partes interessadas (por exemplo, a camada de renderização) possam ser notificadas.

Não tenho certeza de onde isso nos deixa. Eu entendo por que isso é surpreendente, mas estou inclinado a dizer que essa é uma série de erros de usuários infelizes. Talvez devêssemos evitar vincular essas propriedades aos elementos de entrada?

@chancancode - obrigado pela explicação incrível. Isso significa que <input ... > nunca deve ser usado, mas apenas {{input ...}} , a fim de evitar todos os erros como esse?

@ boris-petrov pode haver alguns casos limitados em que é aceitável .. como um campo de texto somente leitura para "copiar este url para sua área de transferência", ou você _pode_ usar o elemento de entrada + {{action}} para interceptar o Evento DOM e refletem as atualizações de propriedade manualmente (que é o que o twiddle tentou fazer, exceto que também gerou a colisão de @identity ), mas sim, em algum momento você está apenas reimplementando {{input}} e lidar com todos os casos extremos que já tratou para você. Então eu acho que é _provavelmente_ justo dizer que você deve apenas usar {{input}} maior parte, senão todo, do tempo.

No entanto, isso ainda não teria "consertado" este caso em que há colisões com as chaves. Consulte https://ember-twiddle.com/0f2369021128e2ae0c445155df5bb034?openFiles=templates.application.hbs%2C

É por isso que eu disse que não estou 100% certo do que fazer a respeito. Por um lado, concordo que é surpreendente e inesperado, por outro lado, esse tipo de colisão é muito raro em aplicativos reais e é por isso que o argumento "chave" é personalizável (este é um caso em que a chave "@identity" padrão não é Good Enough ™, é por isso que esse recurso existe).

@chancancode - isso me lembra de outro problema que abri há algum tempo . Você acha que há algo semelhante aí? A resposta que recebi lá (sobre a necessidade de usar replace vez de set ao definir elementos de array) ainda parece estranha para mim.

@ boris-petrov não acho que esteja relacionado

oi, usamos sortablejs para lista arrastável com ember. pls verifique esta demonstração para reproduzir cada problema.

degrau:

  • arraste qualquer item para o último
  • mudar a seleção para 'v2'

você pode ver o item arrastado ficar na árvore dom.

mas, se arrastar o item para outra posição (não o último item), parece que funciona bem.

Esta página foi útil?
0 / 5 - 0 avaliações