Ember.js: # каждая ошибка повторной визуализации

Созданный на 14 сент. 2018  ·  8Комментарии  ·  Источник: emberjs/ember.js

У меня есть массив значений true / false / undefined, который я отображаю как список флажков.
При изменении элемента массива на значение true или наоборот, список флажков перерисовывается со следующим флажком (индекс + 1), наследующим изменение вместе с измененным флажком.
Код:

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

Когда я использую {{#each range key="@index" as |value idx|}} он работает правильно.

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

embereach

Bug Has Reproduction Rendering

Самый полезный комментарий

Думаю, я знаю, что здесь происходит. Это беспорядка, но я постараюсь это описать. Этому способствовало множество крайних случаев (пользовательская ошибка пограничной линии), и я не совсем уверен, что является / не является ошибкой, что и как исправить любую из этих ошибок.

Майор 🔑

Прежде всего, мне нужно описать, что делает параметр key в {{#each}} . TL; DR он пытается определить, когда и имеет ли смысл повторно использовать существующую DOM, а не просто создавать DOM с нуля.

Для нашей цели давайте примем как данность, что «прикосновение к DOM» (например, обновление содержимого текстового узла, атрибута, добавление или удаление содержимого и т. Д.) Является дорогостоящим и его следует избегать по мере возможности.

Давайте сосредоточимся на довольно простом шаблоне:

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

Если this.names - это ...

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

Тогда вы получите ...

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

Все идет нормально.

Добавление элемента в список

А что, если мы добавим в список { first: "Andrew", last: "Timberlake" } ? Мы ожидаем, что шаблон создаст следующую модель DOM:

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

Но как_?

Самый наивный способ реализовать помощник {{#each}} очищать все содержимое списка каждый раз, когда содержимое списка изменяется. Для этого вам потребуется выполнить не менее 23 операций:

  • Удалить 3 <li> узлов
  • Вставить 4 узла <li>
  • Вставьте 12 текстовых узлов (один для имени, один для промежутка между ними и один для фамилии, умноженный на 4 строки)
  • Вызов помощника to-upper-case 4 раза

Это кажется ... очень ненужным и дорогим. Мы _ знаем_, что первые три элемента не изменились, поэтому было бы неплохо, если бы мы могли просто пропустить работу с этими строками.

🔑 @index

Лучшей реализацией было бы попытаться повторно использовать существующие строки и не делать ненужных обновлений. Одна из идей - просто сопоставить строки с их положением в шаблонах. По сути, это то, что делает key="@index" :

  1. Сравните первый объект { first: "Yehuda", last: "Katz" } с первой строкой <li>Yehuda KATZ</li> :
    1.1. "Иегуда" === "Иегуда", нечего делать
    1.2. (пространство не содержит динамических данных, поэтому сравнение не требуется)
    1.3. «Katz» === «Katz», поскольку помощники «чистые», мы знаем, что нам не придется повторно вызывать помощник to-upper-case , и поэтому нам известен вывод этого помощника («KATZ» ) _тоже_ не изменилось, так что делать здесь нечего
  2. Аналогично ничего не делать для строк 2 и 3.
  3. В DOM нет четвертой строки, поэтому вставьте новую
    3.1. Вставить узел <li>
    3.2. Вставить текстовый узел («Андрей»)
    3.3. Вставить текстовый узел (пробел)
    3.4. Вызов помощника to-upper-case ("Тимберлейк" -> "ТИМБЕРЛЕЙК")
    3.5. Вставить текстовый узел ("ТИМБЕРЛЕЙК")

Таким образом, с помощью этой реализации мы сократили общее количество операций с 23 до 5 (размахивая руками над стоимостью сравнений, но для нашей цели мы предполагаем, что они относительно дешевы по сравнению с остальными). Неплохо.

Добавление элемента в список

Но теперь, что произойдет, если вместо _appnding_ { first: "Andrew", last: "Timberlake" } в список мы _prepended_ вместо этого? Мы ожидаем, что шаблон создаст следующую модель DOM:

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

Но как_?

  1. Сравните первый объект { first: "Andrew", last: "Timberlake" } с первой строкой <li>Yehuda KATZ</li> :
    1.1. «Эндрю»! == «Иегуда», обновите текстовый узел
    1.2. (пространство не содержит динамических данных, поэтому сравнение не требуется)
    1.3. "Тимберлейк"! == "Кац", повторно вызовите помощника to-upper-case
    1.4. Обновите текстовый узел с "KATZ" на "TIMBERLAKE".
  2. Сравните второй объект { first: "Yehuda", last: "Katz" } со второй строкой, <li>Tom DALE</li> , еще 3 операции
  3. Сравните второй объект { first: "Tom", last: "Dale" } со второй строкой, <li>Godfrey CHAN</li> , еще 3 операции
  4. В DOM нет четвертой строки, поэтому вставьте новую
    3.1. Вставить узел <li>
    3.2. Вставить текстовый узел («Годфри»)
    3.3. Вставить текстовый узел (пробел)
    3.4. Вызов помощника to-upper-case ("Чан" -> "ЧАН")
    3.5. Вставить текстовый узел («ЧАН»)

Это 14 операций. Ой!

🔑 @identity

Это казалось ненужным, потому что концептуально, независимо от того, добавляем мы или добавляем, мы все еще изменяем (вставляем) только один объект в массив. Оптимально, мы должны иметь возможность обрабатывать этот случай так же хорошо, как и в сценарии добавления.

Здесь на помощь приходит key="@identity" . Вместо того, чтобы полагаться на _order_ элементов в массиве, мы используем их идентификатор объекта JavaScript ( === ):

  1. Найдите существующую строку, данные которой соответствуют ( === ) первому объекту { first: "Andrew", last: "Timberlake" } . Поскольку ничего не найдено, вставьте (добавьте) новую строку:
    1.1. Вставить узел <li>
    1.2. Вставить текстовый узел («Андрей»)
    1.3. Вставить текстовый узел (пробел)
    1.4. Вызов помощника to-upper-case ("Тимберлейк" -> "ТИМБЕРЛЕЙК")
    1.5. Вставить текстовый узел ("ТИМБЕРЛЕЙК")
  2. Найдите существующую строку, данные которой соответствуют ( === ) второму объекту { first: "Yehuda", last: "Katz" } . Найдено <li>Yehuda KATZ</li> :
    2.1. "Иегуда" === "Иегуда", нечего делать
    2.2. (пространство не содержит динамических данных, поэтому сравнение не требуется)
    2.3. «Katz» === «Katz», поскольку помощники «чистые», мы знаем, что нам не придется повторно вызывать помощник to-upper-case , и поэтому нам известен вывод этого помощника («KATZ» ) _тоже_ не изменилось, так что делать здесь нечего
  3. Точно так же нечего делать с рядами Тома и Годфри.
  4. Удалите все строки с несовпадающими объектами (нет, в этом случае делать нечего)

На этом мы вернулись к оптимальным 5 операциям.

Увеличение масштаба

Опять же, это размахивая рукой над сравнениями и бухгалтерскими расходами. В самом деле, они тоже не бесплатны, и в этом очень простом примере они могут не того стоить. Но представьте, что список большой, и каждая строка вызывает сложный компонент (с множеством помощников, вычисляемых свойств, подкомпонентов и т. Д.). Представьте, например, новостную ленту LinkedIn. Если мы не сопоставим правильные строки с правильными данными, аргументы ваших компонентов могут потенциально сильно сбиваться и вызывать гораздо больше обновлений DOM, чем вы могли бы ожидать. Также существуют проблемы с сопоставлением неправильных элементов DOM и потерей состояния DOM, такого как положение курсора и состояние выделения текста.

В целом, в реальном приложении затраты на дополнительное сравнение и бухгалтерский учет в большинстве случаев окупаются. Поскольку key="@identity" является значением по умолчанию в Ember и хорошо работает почти во всех случаях, вам обычно не придется беспокоиться об установке аргумента key при использовании {{#each}} .

Столкновения 💥

Но подождите, есть проблема. Что насчет этого дела?

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

Проблема здесь в том, что один и тот же объект _ мог_ появляться несколько раз в одном списке. Это нарушает наш наивный алгоритм @identity , в частности ту часть, где мы сказали: «Найдите существующую строку, данные которой совпадают ( === ) ...» - это работает только в том случае, если отношение данных к DOM равно 1 : 1, что в данном случае неверно. На практике это может показаться маловероятным, но мы должны с этим справиться.

Чтобы избежать этого, мы используем своего рода гибридный подход для обработки этих коллизий. Внутренне сопоставление ключей к DOM выглядит примерно так:

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

По большей части это встречается довольно редко, а когда все же возникает, большую часть времени это работает достаточно хорошо ™. Если по какой-то причине это не сработает, вы всегда можете использовать путь ключа (или даже более продвинутый механизм ввода ключей в RFC 321 ).

Вернуться к "🐛"

После всего этого разговора мы готовы взглянуть на сценарий в Twiddle.

По сути, мы начали с этого списка: [undefined, undefined, undefined, undefined, undefined] .

Несвязанное примечание: Array(5) _не_ то же самое, что [undefined, undefined, undefined, undefined, undefined] . Он создает "дырявый массив", которого в целом следует избегать. Однако это не связано с этой ошибкой, потому что при доступе к «дырам» вы действительно получаете undefined обратно. Так что только для нашей _ очень узкой_ цели они одинаковы.

Поскольку мы не указали ключ, Ember по умолчанию использует @identity . Далее, поскольку это столкновения, мы получили примерно следующее:

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

Теперь, допустим, мы установили первый флажок:

  1. Он запускает поведение поля выбора по умолчанию: изменение отмеченного состояния на true
  2. Он запускает событие click, которое перехватывается модификатором {{action}} и повторно отправляется методу makeChange
  3. Он изменяет список на [true, undefined, undefined, undefined, undefined] .
  4. Он обновляет DOM.

Как обновляется DOM?

  1. Найдите существующую строку, данные которой соответствуют ( === ) первому объекту true . Поскольку ничего не найдено, вставьте (добавьте) новую строку <input checked=true ...>0: true...
  2. Найдите существующую строку, данные которой соответствуют ( === ) второму объекту undefined . Найдено <input ...>0: ... (ранее ПЕРВАЯ строка):
    2.1. Обновите текстовый узел {{idx}} до 1
    2.2. В остальном, насколько может судить Эмбер, в этом ряду больше ничего не изменилось, больше нечего делать.
  3. Найдите существующую строку, данные которой соответствуют ( === ) третьему объекту undefined . Поскольку это второй раз, когда мы видим undefined , внутренний ключ - undefined-1 , поэтому мы нашли <input ...>1: ... (ранее ВТОРАЯ строка):
    3.1. Обновите текстовый узел {{idx}} до 2
    3.2. В остальном, насколько может судить Эмбер, в этом ряду больше ничего не изменилось, больше нечего делать.
  4. Аналогичным образом обновите undefined-2 и undefined-3
  5. Наконец, удалите несопоставленную строку undefined-4 (поскольку после обновления в массиве на одну undefined меньше)

Это объясняет, как мы получили результат, который вы получили в твиддле. По сути, все строки DOM смещены вниз на одну, и новая была вставлена ​​вверху, а {{idx}} обновляется для остальных.

Действительно неожиданная часть - 2.2. Несмотря на то, что первый флажок (тот, который был нажат) был сдвинут на одну строку вниз во вторую позицию, вы, вероятно, ожидали, что Ember, его свойство checked изменилось на true , и поскольку его связанное значение не определено, вы можете ожидать, что Ember вернет его обратно на false , сняв таким образом отметку.

Но это не так. Как упоминалось в начале, доступ к DOM стоит дорого. Это включает _reading_ из DOM. Если бы при каждом обновлении нам приходилось считывать последнее значение из DOM для наших сравнений, это в значительной степени нарушило бы цель наших оптимизаций. Поэтому, чтобы этого избежать, мы запомнили последнее значение, которое мы записали в DOM, и сравнили текущее значение с кэшированным значением, не считывая его обратно из DOM. Только когда есть разница, мы записываем новое значение в DOM (и кэшируем его в следующий раз). В этом смысле мы как бы разделяем тот же подход «виртуального DOM», но мы делаем это только на конечных узлах, не виртуализируя «древовидность» всего DOM.

Итак, TL; DR, «привязка» свойства checked (или свойства value текстового поля и т. Д.) На самом деле не работает так, как вы ожидаете. Представьте, что вы визуализировали <div>{{this.name}}</div> и вручную обновили textContent элемента div используя jQuery или с помощью инспектора Chrome. Вы не ожидали, что Ember заметит это и обновит за вас this.name . По сути, это то же самое: поскольку обновление свойства checked произошло за пределами Ember (через поведение браузера по умолчанию для флажка), Ember не узнает об этом.

Вот почему существует помощник {{input}} . Он должен зарегистрировать соответствующие прослушиватели событий в базовом элементе HTML и отразить операции в соответствующем изменении свойства, чтобы заинтересованные стороны (например, уровень визуализации) могли быть уведомлены.

Я не уверен, к чему это нас приведет. Я понимаю, почему это удивительно, но я склонен сказать, что это серия досадных ошибок пользователя. Возможно, нам следует избегать привязки этих свойств к элементам ввода?

Все 8 Комментарий

@andrewtimberlake похоже, что использование {{#each range key="@index" as |value idx|}} решает проблему.

Но похоже на ошибку, key предназначен для другой цели, https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor= каждый

Думаю, я знаю, что здесь происходит. Это беспорядка, но я постараюсь это описать. Этому способствовало множество крайних случаев (пользовательская ошибка пограничной линии), и я не совсем уверен, что является / не является ошибкой, что и как исправить любую из этих ошибок.

Майор 🔑

Прежде всего, мне нужно описать, что делает параметр key в {{#each}} . TL; DR он пытается определить, когда и имеет ли смысл повторно использовать существующую DOM, а не просто создавать DOM с нуля.

Для нашей цели давайте примем как данность, что «прикосновение к DOM» (например, обновление содержимого текстового узла, атрибута, добавление или удаление содержимого и т. Д.) Является дорогостоящим и его следует избегать по мере возможности.

Давайте сосредоточимся на довольно простом шаблоне:

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

Если this.names - это ...

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

Тогда вы получите ...

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

Все идет нормально.

Добавление элемента в список

А что, если мы добавим в список { first: "Andrew", last: "Timberlake" } ? Мы ожидаем, что шаблон создаст следующую модель DOM:

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

Но как_?

Самый наивный способ реализовать помощник {{#each}} очищать все содержимое списка каждый раз, когда содержимое списка изменяется. Для этого вам потребуется выполнить не менее 23 операций:

  • Удалить 3 <li> узлов
  • Вставить 4 узла <li>
  • Вставьте 12 текстовых узлов (один для имени, один для промежутка между ними и один для фамилии, умноженный на 4 строки)
  • Вызов помощника to-upper-case 4 раза

Это кажется ... очень ненужным и дорогим. Мы _ знаем_, что первые три элемента не изменились, поэтому было бы неплохо, если бы мы могли просто пропустить работу с этими строками.

🔑 @index

Лучшей реализацией было бы попытаться повторно использовать существующие строки и не делать ненужных обновлений. Одна из идей - просто сопоставить строки с их положением в шаблонах. По сути, это то, что делает key="@index" :

  1. Сравните первый объект { first: "Yehuda", last: "Katz" } с первой строкой <li>Yehuda KATZ</li> :
    1.1. "Иегуда" === "Иегуда", нечего делать
    1.2. (пространство не содержит динамических данных, поэтому сравнение не требуется)
    1.3. «Katz» === «Katz», поскольку помощники «чистые», мы знаем, что нам не придется повторно вызывать помощник to-upper-case , и поэтому нам известен вывод этого помощника («KATZ» ) _тоже_ не изменилось, так что делать здесь нечего
  2. Аналогично ничего не делать для строк 2 и 3.
  3. В DOM нет четвертой строки, поэтому вставьте новую
    3.1. Вставить узел <li>
    3.2. Вставить текстовый узел («Андрей»)
    3.3. Вставить текстовый узел (пробел)
    3.4. Вызов помощника to-upper-case ("Тимберлейк" -> "ТИМБЕРЛЕЙК")
    3.5. Вставить текстовый узел ("ТИМБЕРЛЕЙК")

Таким образом, с помощью этой реализации мы сократили общее количество операций с 23 до 5 (размахивая руками над стоимостью сравнений, но для нашей цели мы предполагаем, что они относительно дешевы по сравнению с остальными). Неплохо.

Добавление элемента в список

Но теперь, что произойдет, если вместо _appnding_ { first: "Andrew", last: "Timberlake" } в список мы _prepended_ вместо этого? Мы ожидаем, что шаблон создаст следующую модель DOM:

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

Но как_?

  1. Сравните первый объект { first: "Andrew", last: "Timberlake" } с первой строкой <li>Yehuda KATZ</li> :
    1.1. «Эндрю»! == «Иегуда», обновите текстовый узел
    1.2. (пространство не содержит динамических данных, поэтому сравнение не требуется)
    1.3. "Тимберлейк"! == "Кац", повторно вызовите помощника to-upper-case
    1.4. Обновите текстовый узел с "KATZ" на "TIMBERLAKE".
  2. Сравните второй объект { first: "Yehuda", last: "Katz" } со второй строкой, <li>Tom DALE</li> , еще 3 операции
  3. Сравните второй объект { first: "Tom", last: "Dale" } со второй строкой, <li>Godfrey CHAN</li> , еще 3 операции
  4. В DOM нет четвертой строки, поэтому вставьте новую
    3.1. Вставить узел <li>
    3.2. Вставить текстовый узел («Годфри»)
    3.3. Вставить текстовый узел (пробел)
    3.4. Вызов помощника to-upper-case ("Чан" -> "ЧАН")
    3.5. Вставить текстовый узел («ЧАН»)

Это 14 операций. Ой!

🔑 @identity

Это казалось ненужным, потому что концептуально, независимо от того, добавляем мы или добавляем, мы все еще изменяем (вставляем) только один объект в массив. Оптимально, мы должны иметь возможность обрабатывать этот случай так же хорошо, как и в сценарии добавления.

Здесь на помощь приходит key="@identity" . Вместо того, чтобы полагаться на _order_ элементов в массиве, мы используем их идентификатор объекта JavaScript ( === ):

  1. Найдите существующую строку, данные которой соответствуют ( === ) первому объекту { first: "Andrew", last: "Timberlake" } . Поскольку ничего не найдено, вставьте (добавьте) новую строку:
    1.1. Вставить узел <li>
    1.2. Вставить текстовый узел («Андрей»)
    1.3. Вставить текстовый узел (пробел)
    1.4. Вызов помощника to-upper-case ("Тимберлейк" -> "ТИМБЕРЛЕЙК")
    1.5. Вставить текстовый узел ("ТИМБЕРЛЕЙК")
  2. Найдите существующую строку, данные которой соответствуют ( === ) второму объекту { first: "Yehuda", last: "Katz" } . Найдено <li>Yehuda KATZ</li> :
    2.1. "Иегуда" === "Иегуда", нечего делать
    2.2. (пространство не содержит динамических данных, поэтому сравнение не требуется)
    2.3. «Katz» === «Katz», поскольку помощники «чистые», мы знаем, что нам не придется повторно вызывать помощник to-upper-case , и поэтому нам известен вывод этого помощника («KATZ» ) _тоже_ не изменилось, так что делать здесь нечего
  3. Точно так же нечего делать с рядами Тома и Годфри.
  4. Удалите все строки с несовпадающими объектами (нет, в этом случае делать нечего)

На этом мы вернулись к оптимальным 5 операциям.

Увеличение масштаба

Опять же, это размахивая рукой над сравнениями и бухгалтерскими расходами. В самом деле, они тоже не бесплатны, и в этом очень простом примере они могут не того стоить. Но представьте, что список большой, и каждая строка вызывает сложный компонент (с множеством помощников, вычисляемых свойств, подкомпонентов и т. Д.). Представьте, например, новостную ленту LinkedIn. Если мы не сопоставим правильные строки с правильными данными, аргументы ваших компонентов могут потенциально сильно сбиваться и вызывать гораздо больше обновлений DOM, чем вы могли бы ожидать. Также существуют проблемы с сопоставлением неправильных элементов DOM и потерей состояния DOM, такого как положение курсора и состояние выделения текста.

В целом, в реальном приложении затраты на дополнительное сравнение и бухгалтерский учет в большинстве случаев окупаются. Поскольку key="@identity" является значением по умолчанию в Ember и хорошо работает почти во всех случаях, вам обычно не придется беспокоиться об установке аргумента key при использовании {{#each}} .

Столкновения 💥

Но подождите, есть проблема. Что насчет этого дела?

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

Проблема здесь в том, что один и тот же объект _ мог_ появляться несколько раз в одном списке. Это нарушает наш наивный алгоритм @identity , в частности ту часть, где мы сказали: «Найдите существующую строку, данные которой совпадают ( === ) ...» - это работает только в том случае, если отношение данных к DOM равно 1 : 1, что в данном случае неверно. На практике это может показаться маловероятным, но мы должны с этим справиться.

Чтобы избежать этого, мы используем своего рода гибридный подход для обработки этих коллизий. Внутренне сопоставление ключей к DOM выглядит примерно так:

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

По большей части это встречается довольно редко, а когда все же возникает, большую часть времени это работает достаточно хорошо ™. Если по какой-то причине это не сработает, вы всегда можете использовать путь ключа (или даже более продвинутый механизм ввода ключей в RFC 321 ).

Вернуться к "🐛"

После всего этого разговора мы готовы взглянуть на сценарий в Twiddle.

По сути, мы начали с этого списка: [undefined, undefined, undefined, undefined, undefined] .

Несвязанное примечание: Array(5) _не_ то же самое, что [undefined, undefined, undefined, undefined, undefined] . Он создает "дырявый массив", которого в целом следует избегать. Однако это не связано с этой ошибкой, потому что при доступе к «дырам» вы действительно получаете undefined обратно. Так что только для нашей _ очень узкой_ цели они одинаковы.

Поскольку мы не указали ключ, Ember по умолчанию использует @identity . Далее, поскольку это столкновения, мы получили примерно следующее:

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

Теперь, допустим, мы установили первый флажок:

  1. Он запускает поведение поля выбора по умолчанию: изменение отмеченного состояния на true
  2. Он запускает событие click, которое перехватывается модификатором {{action}} и повторно отправляется методу makeChange
  3. Он изменяет список на [true, undefined, undefined, undefined, undefined] .
  4. Он обновляет DOM.

Как обновляется DOM?

  1. Найдите существующую строку, данные которой соответствуют ( === ) первому объекту true . Поскольку ничего не найдено, вставьте (добавьте) новую строку <input checked=true ...>0: true...
  2. Найдите существующую строку, данные которой соответствуют ( === ) второму объекту undefined . Найдено <input ...>0: ... (ранее ПЕРВАЯ строка):
    2.1. Обновите текстовый узел {{idx}} до 1
    2.2. В остальном, насколько может судить Эмбер, в этом ряду больше ничего не изменилось, больше нечего делать.
  3. Найдите существующую строку, данные которой соответствуют ( === ) третьему объекту undefined . Поскольку это второй раз, когда мы видим undefined , внутренний ключ - undefined-1 , поэтому мы нашли <input ...>1: ... (ранее ВТОРАЯ строка):
    3.1. Обновите текстовый узел {{idx}} до 2
    3.2. В остальном, насколько может судить Эмбер, в этом ряду больше ничего не изменилось, больше нечего делать.
  4. Аналогичным образом обновите undefined-2 и undefined-3
  5. Наконец, удалите несопоставленную строку undefined-4 (поскольку после обновления в массиве на одну undefined меньше)

Это объясняет, как мы получили результат, который вы получили в твиддле. По сути, все строки DOM смещены вниз на одну, и новая была вставлена ​​вверху, а {{idx}} обновляется для остальных.

Действительно неожиданная часть - 2.2. Несмотря на то, что первый флажок (тот, который был нажат) был сдвинут на одну строку вниз во вторую позицию, вы, вероятно, ожидали, что Ember, его свойство checked изменилось на true , и поскольку его связанное значение не определено, вы можете ожидать, что Ember вернет его обратно на false , сняв таким образом отметку.

Но это не так. Как упоминалось в начале, доступ к DOM стоит дорого. Это включает _reading_ из DOM. Если бы при каждом обновлении нам приходилось считывать последнее значение из DOM для наших сравнений, это в значительной степени нарушило бы цель наших оптимизаций. Поэтому, чтобы этого избежать, мы запомнили последнее значение, которое мы записали в DOM, и сравнили текущее значение с кэшированным значением, не считывая его обратно из DOM. Только когда есть разница, мы записываем новое значение в DOM (и кэшируем его в следующий раз). В этом смысле мы как бы разделяем тот же подход «виртуального DOM», но мы делаем это только на конечных узлах, не виртуализируя «древовидность» всего DOM.

Итак, TL; DR, «привязка» свойства checked (или свойства value текстового поля и т. Д.) На самом деле не работает так, как вы ожидаете. Представьте, что вы визуализировали <div>{{this.name}}</div> и вручную обновили textContent элемента div используя jQuery или с помощью инспектора Chrome. Вы не ожидали, что Ember заметит это и обновит за вас this.name . По сути, это то же самое: поскольку обновление свойства checked произошло за пределами Ember (через поведение браузера по умолчанию для флажка), Ember не узнает об этом.

Вот почему существует помощник {{input}} . Он должен зарегистрировать соответствующие прослушиватели событий в базовом элементе HTML и отразить операции в соответствующем изменении свойства, чтобы заинтересованные стороны (например, уровень визуализации) могли быть уведомлены.

Я не уверен, к чему это нас приведет. Я понимаю, почему это удивительно, но я склонен сказать, что это серия досадных ошибок пользователя. Возможно, нам следует избегать привязки этих свойств к элементам ввода?

@chancancode - спасибо за прекрасное объяснение. Означает ли это, что никогда нельзя использовать <input ... > а использовать только {{input ...}} , чтобы предотвратить все подобные ошибки?

@ boris-petrov могут быть некоторые ограниченные случаи, когда это приемлемо ... например, текстовое поле только для чтения для типа "скопировать этот URL в буфер обмена", или вы _ можете_ использовать элемент ввода + {{action}} для перехвата DOM и отразите обновления свойств вручную (это то, что пытался сделать твиддл, за исключением того, что он также столкнулся с столкновением @identity ), но да, в какой-то момент вы просто повторно реализуете {{input}} и обрабатывать все крайние случаи, которые он уже обработал для вас. Поэтому я думаю, что _ вероятно_ справедливо сказать, что вам следует просто использовать {{input}} большую часть, если не все время.

Однако это все равно не «исправило» этот случай, когда есть столкновения с ключами. См. Https://ember-twiddle.com/0f2369021128e2ae0c445155df5bb034?openFiles=templates.application.hbs%2C

Вот почему я сказал: я на 100% не уверен, что с этим делать. С одной стороны, я согласен, что это удивительно и неожиданно, с другой стороны, такой вид коллизий довольно редко встречается в реальных приложениях, и именно поэтому аргумент «ключ» можно настраивать (это тот случай, когда по умолчанию используется ключ «@identity» функция не является Good Enough ™, поэтому она существует).

@chancancode - это напоминает мне о другом выпуске, который я открыл некоторое время назад . Как вы думаете, там есть что-то подобное? Полученный там ответ (о необходимости использовать replace вместо set при установке элементов массива) мне до сих пор кажется странным.

@ boris-petrov Я не думаю, что это связано

привет, мы используем sortablejs для перетаскиваемого списка с помощью ember. Пожалуйста, проверьте эту демонстрацию, чтобы воспроизвести каждую проблему.

шаг:

  • перетащите любой элемент на последний
  • переключите выбор на 'v2'

вы можете видеть, что перетаскиваемый элемент остается в дереве доменов.

но, если перетащить элемент в другую позицию (не в последний элемент), кажется, что это работает хорошо.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги