我有一个true / false / undefined值数组,我将它们渲染为复选框列表。
在将数组元素更改为true或从true更改为数组元素时,将使用以下(index + 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|}}
它可以正常工作。
扭蛋: https ://ember-twiddle.com/6d63548f35f99da19cee9f58fb64db59
@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个操作:
<li>
节点<li>
节点to-upper-case
助手4次这似乎...非常不必要和昂贵。 我们知道前三个项目没有改变,所以如果我们只跳过那些行的工作,那就太好了。
更好的实现方式是尝试重用现有行,并且不执行任何不必要的更新。 一种想法是简单地将行与其在模板中的位置进行匹配。 这实际上是key="@index"
作用:
{ first: "Yehuda", last: "Katz" }
与第一行<li>Yehuda KATZ</li>
:to-upper-case
助手,因此我们知道该助手的输出(“ KATZ” )_also_没变,所以这里无事可做<li>
节点to-upper-case
助手(“ Timberlake”->“ TIMBERLAKE”)因此,通过此实现,我们将操作总数从23个减少到了5个(比较费时费力,但是出于我们的目的,我们假设它们与其余的相比是相对便宜的)。 不错。
但是,现在,如果我们而不是将{ first: "Andrew", last: "Timberlake" }
附加到列表中,而是将其添加到列表中,将会发生什么情况呢? 我们希望模板产生以下DOM:
<ul>
<li>Andrew TIMBERLAKE</li>
<li>Yehuda KATZ</li>
<li>Tom DALE</li>
<li>Godfrey CHAN</li>
</ul>
但是_如何_?
{ first: "Andrew", last: "Timberlake" }
与第一行<li>Yehuda KATZ</li>
:to-upper-case
帮助器{ first: "Yehuda", last: "Katz" }
与第二行<li>Tom DALE</li>
,另外3个操作{ first: "Tom", last: "Dale" }
与第二行<li>Godfrey CHAN</li>
,另外3个操作<li>
节点to-upper-case
助手(“ Chan”->“ CHAN”)那是14次操作。 哎哟!
这似乎没有必要,因为从概念上讲,无论我们是在前面还是在后面,我们仍然只更改(插入)数组中的单个对象。 最佳地,我们应该能够像在追加场景中一样处理这种情况。
这是key="@identity"
进入的地方。我们不用依赖数组中元素的_order_,而是使用它们的JavaScript对象标识( ===
):
{ first: "Andrew", last: "Timberlake" }
匹配( ===
)的现有行。 由于未找到任何内容,因此插入(添加)新行:<li>
节点to-upper-case
助手(“ Timberlake”->“ TIMBERLAKE”){ first: "Yehuda", last: "Katz" }
匹配( ===
)的现有行。 找到<li>Yehuda KATZ</li>
:to-upper-case
助手,因此我们知道该助手的输出(“ KATZ” )_also_没变,所以这里无事可做这样,我们回到了最佳的5种操作。
同样,这是比较和簿记成本的👋手。 确实,它们也不是免费的,在这个非常简单的示例中,它们可能不值得。 但是,想象一下这个列表很大,并且每一行都调用一个复杂的组件(具有很多帮助器,计算属性,子组件等)。 例如,想象一下LinkedIn新闻提要。 如果我们没有用正确的数据来匹配正确的行,则组件的参数可能会产生很大的混乱,并导致DOM更新超出您的预期。 匹配错误的DOM元素并失去DOM状态(例如光标位置和文本选择状态)也存在问题。
总体而言,在现实世界中的大多数时间里,额外的比较和簿记成本很容易就值得。 由于key="@identity"
是Ember中的默认设置,并且在几乎所有情况下都能正常使用,因此在使用{{#each}}
时,通常不必担心设置key
参数。
但是,等等,这是一个问题。 那这种情况呢?
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
];
这里的问题是同一对象_could_在同一列表中出现多次。 这打破了我们朴素的@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>
在大多数情况下,这种情况很少见,当它出现时,大多数时候都可以使用Good Enough™。 如果由于某种原因这不起作用,则可以始终使用密钥路径(或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: ...
现在,假设我们单击第一个复选框:
{{action}}
修饰符拦截并重新分配给makeChange
方法[true, undefined, undefined, undefined, undefined]
。DOM如何更新?
true
匹配( ===
)的现有行。 由于未找到任何内容,因此插入(添加)新行<input checked=true ...>0: true...
undefined
匹配( ===
)的现有行。 找到<input ...>0: ...
(以前是FIRST行):{{idx}}
文本节点更新为1
undefined
匹配( ===
)的现有行。 由于这是我们第二次看到undefined
,内部键是undefined-1
,因此我们找到了<input ...>1: ...
(以前是SECOND行):{{idx}}
文本节点更新为2
undefined-2
和undefined-3
undefined-4
行(因为更新后数组中的undefined
少因此,这说明了我们如何获得旋转中的输出。 基本上所有DOM行都向下移动了一个,并在顶部插入了一个新行,而其余的{{idx}}
被更新了。
真正出乎意料的部分是2.2。 即使第一个复选框(被单击的复选框)向下移动了一行到第二个位置,您也可能希望将Ember的checked
属性更改为true
,并且由于其绑定值是不确定的,因此您可能希望Ember将其更改回false
,从而取消选中它。
但这不是它的工作方式。 如开头所述,访问DOM的成本很高。 这包括DOM中的_reading_。 如果在每次更新时我们都必须从DOM中读取最新值以进行比较,那么这将很不利于优化的目的。 因此,为了避免这种情况,我们记住了我们写入DOM的最后一个值,并将当前值与缓存的值进行比较,而不必从DOM中读取回去。 仅当存在差异时,我们才将新值写入DOM(并在下一次缓存它)。 从某种意义上说,我们有点相同的“虚拟DOM”方法,但是我们只在叶节点执行此操作,而不是虚拟整个DOM的“树形”。
因此,TL; DR“绑定” checked
属性(或文本字段的value
属性,等等)并不能像您期望的那样工作。 想象一下,如果您渲染了<div>{{this.name}}</div>
并使用jQuery
或chrome检验器手动更新了textContent
div
元素的jQuery
。 您不会期望Ember注意到这一点并为您更新this.name
。 这基本上是同一件事:由于对checked
属性的更新发生在Ember之外(通过浏览器的复选框默认行为),因此Ember不会知道这一点。
这就是{{input}}
助手存在的原因。 它必须在基础HTML元素上注册相关的事件侦听器,并将操作反映到适当的属性更改中,以便可以通知感兴趣的各方(例如,呈现层)。
我不确定那会给我们留下什么。 我理解为什么这令人惊讶,但是我倾向于说这是一系列不幸的用户错误。 也许我们应该反对在输入元素上绑定这些属性?
相关代码:
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-感谢您的精彩解释。 这是否意味着不应使用<input ... >
而只能使用{{input ...}}
来防止此类错误?
@ boris-petrov可能在某些有限的情况下是可以接受的。.例如,用于“将此URL复制到剪贴板”的只读文本字段,或者您可以_can_使用输入元素+ {{action}}
来拦截DOM事件并手动反映属性更新(这是该方法的尝试,除了它还会遇到@identity
冲突),但是是的,在某些时候,您只是在重新实现{{input}}
并处理它已经为您处理的所有边缘情况。 因此,我认为您应该最多使用{{input}}
,即使不是全部,也是合理的。
但是,在与键发生冲突的情况下,仍然无法“解决”这种情况。 参见https://ember-twiddle.com/0f2369021128e2ae0c445155df5bb034?openFiles=templates.application.hbs%2C
这就是为什么我说,我100%不知道该怎么办。 一方面,我同意这是令人惊讶且出乎意料的,另一方面,这种冲突在实际应用中很少见,这就是为什么“ key”参数是可自定义的(这是默认“ @identity”键的情况)功能不是Good Enough™,这就是该功能存在的原因)。
@chancancode-这使我想起了我replace
而不是set
)对我来说仍然很奇怪。
@ boris-petrov我不认为这是相关的
嗨,我们将sortablejs用于ember的可拖动列表。 请检查此演示以重现每个问题。
步:
您可以看到拖动的项目留在dom树中。
但是,如果将项目拖到其他位置(而不是最后一个项目),则似乎效果很好。
最有用的评论
我想我知道这是怎么回事。 这真是一团糟,但我会尽力描述。 许多边缘情况(边界线用户错误)导致了这种情况,我不确定是什么/不是错误,什么以及如何修复这些错误。
🔑
首先,我需要描述
key
参数在{{#each}}
。 TL; DR试图确定何时以及是否应该重用现有DOM,而不是仅仅从头开始创建DOM。出于我们的目的,让我们接受一个“接触DOM”(例如,更新文本节点的内容,属性,添加或删除内容等)是昂贵的,应尽可能避免。
让我们关注一个相当简单的模板:
如果
this.names
是...然后你会得到...
到现在为止还挺好。
将项目追加到列表
现在,如果我们将
{ first: "Andrew", last: "Timberlake" }
追加到列表中怎么办? 我们希望模板产生以下DOM:但是_如何_?
实现
{{#each}}
助手的最幼稚的方法是每次列表内容更改时都清除列表的所有内容。 为此,您至少需要执行23个操作:<li>
节点<li>
节点to-upper-case
助手4次这似乎...非常不必要和昂贵。 我们知道前三个项目没有改变,所以如果我们只跳过那些行的工作,那就太好了。
index @index
更好的实现方式是尝试重用现有行,并且不执行任何不必要的更新。 一种想法是简单地将行与其在模板中的位置进行匹配。 这实际上是
key="@index"
作用:{ first: "Yehuda", last: "Katz" }
与第一行<li>Yehuda KATZ</li>
:1.1。 “ Yehuda” ===“ Yehuda”,无关
1.2。 (该空间不包含动态数据,因此无需进行比较)
1.3。 “ Katz” ===“ Katz”,由于助手是“纯”的,我们知道我们不必重新调用
to-upper-case
助手,因此我们知道该助手的输出(“ KATZ” )_also_没变,所以这里无事可做3.1。 插入
<li>
节点3.2。 插入一个文本节点(“ Andrew”)
3.3。 插入文本节点(空格)
3.4。 调用
to-upper-case
助手(“ Timberlake”->“ TIMBERLAKE”)3.5。 插入文本节点(“ TIMBERLAKE”)
因此,通过此实现,我们将操作总数从23个减少到了5个(比较费时费力,但是出于我们的目的,我们假设它们与其余的相比是相对便宜的)。 不错。
在列表前添加项目
但是,现在,如果我们而不是将
{ first: "Andrew", last: "Timberlake" }
附加到列表中,而是将其添加到列表中,将会发生什么情况呢? 我们希望模板产生以下DOM:但是_如何_?
{ first: "Andrew", last: "Timberlake" }
与第一行<li>Yehuda KATZ</li>
:1.1。 “ Andrew”!==“ Yehuda”,更新文本节点
1.2。 (该空间不包含动态数据,因此无需进行比较)
1.3。 “ Timberlake”!==“ Katz”,重新调用
to-upper-case
帮助器1.4。 将文本节点从“ KATZ”更新为“ TIMBERLAKE”
{ first: "Yehuda", last: "Katz" }
与第二行<li>Tom DALE</li>
,另外3个操作{ first: "Tom", last: "Dale" }
与第二行<li>Godfrey CHAN</li>
,另外3个操作3.1。 插入
<li>
节点3.2。 插入一个文本节点(“ Godfrey”)
3.3。 插入文本节点(空格)
3.4。 调用
to-upper-case
助手(“ Chan”->“ CHAN”)3.5。 插入文本节点(“ CHAN”)
那是14次操作。 哎哟!
ident @身份
这似乎没有必要,因为从概念上讲,无论我们是在前面还是在后面,我们仍然只更改(插入)数组中的单个对象。 最佳地,我们应该能够像在追加场景中一样处理这种情况。
这是
key="@identity"
进入的地方。我们不用依赖数组中元素的_order_,而是使用它们的JavaScript对象标识(===
):{ first: "Andrew", last: "Timberlake" }
匹配(===
)的现有行。 由于未找到任何内容,因此插入(添加)新行:1.1。 插入
<li>
节点1.2。 插入一个文本节点(“ Andrew”)
1.3。 插入文本节点(空格)
1.4。 调用
to-upper-case
助手(“ Timberlake”->“ TIMBERLAKE”)1.5。 插入文本节点(“ TIMBERLAKE”)
{ first: "Yehuda", last: "Katz" }
匹配(===
)的现有行。 找到<li>Yehuda KATZ</li>
:2.1。 “ Yehuda” ===“ Yehuda”,无关
2.2。 (该空间不包含动态数据,因此无需进行比较)
2.3。 “ Katz” ===“ Katz”,由于助手是“纯”的,我们知道我们不必重新调用
to-upper-case
助手,因此我们知道该助手的输出(“ KATZ” )_also_没变,所以这里无事可做这样,我们回到了最佳的5种操作。
扩大
同样,这是比较和簿记成本的👋手。 确实,它们也不是免费的,在这个非常简单的示例中,它们可能不值得。 但是,想象一下这个列表很大,并且每一行都调用一个复杂的组件(具有很多帮助器,计算属性,子组件等)。 例如,想象一下LinkedIn新闻提要。 如果我们没有用正确的数据来匹配正确的行,则组件的参数可能会产生很大的混乱,并导致DOM更新超出您的预期。 匹配错误的DOM元素并失去DOM状态(例如光标位置和文本选择状态)也存在问题。
总体而言,在现实世界中的大多数时间里,额外的比较和簿记成本很容易就值得。 由于
key="@identity"
是Ember中的默认设置,并且在几乎所有情况下都能正常使用,因此在使用{{#each}}
时,通常不必担心设置key
参数。碰撞💥
但是,等等,这是一个问题。 那这种情况呢?
这里的问题是同一对象_could_在同一列表中出现多次。 这打破了我们朴素的
@identity
算法,特别是我们所说的“查找数据匹配的现有行(===
)...”的部分–仅在数据与DOM关系为1的情况下有效:1,在这种情况下不正确。 实际上这似乎不太可能,但是作为一个框架,我们必须处理它。为了避免这种情况,我们使用一种混合方法来处理这些冲突。 在内部,键到DOM的映射如下所示:
在大多数情况下,这种情况很少见,当它出现时,大多数时候都可以使用Good Enough™。 如果由于某种原因这不起作用,则可以始终使用密钥路径(或RFC 321中甚至更高级的密钥机制)。
回到“🐛”
讨论完所有内容之后,我们现在就可以看看Twiddle中的情况。
基本上,我们从以下列表开始:
[undefined, undefined, undefined, undefined, undefined]
。由于我们未指定密钥,因此Ember默认使用
@identity
。 此外,由于它们是碰撞,所以我们最终得到这样的结果:现在,假设我们单击第一个复选框:
{{action}}
修饰符拦截并重新分配给makeChange
方法[true, undefined, undefined, undefined, undefined]
。DOM如何更新?
true
匹配(===
)的现有行。 由于未找到任何内容,因此插入(添加)新行<input checked=true ...>0: true...
undefined
匹配(===
)的现有行。 找到<input ...>0: ...
(以前是FIRST行):2.1。 将
{{idx}}
文本节点更新为1
2.2。 否则,就Ember所知,此行中没有其他更改,没有其他操作
undefined
匹配(===
)的现有行。 由于这是我们第二次看到undefined
,内部键是undefined-1
,因此我们找到了<input ...>1: ...
(以前是SECOND行):3.1。 将
{{idx}}
文本节点更新为2
3.2。 否则,就Ember所知,此行中没有其他更改,没有其他操作
undefined-2
和undefined-3
undefined-4
行(因为更新后数组中的undefined
少因此,这说明了我们如何获得旋转中的输出。 基本上所有DOM行都向下移动了一个,并在顶部插入了一个新行,而其余的
{{idx}}
被更新了。真正出乎意料的部分是2.2。 即使第一个复选框(被单击的复选框)向下移动了一行到第二个位置,您也可能希望将Ember的
checked
属性更改为true
,并且由于其绑定值是不确定的,因此您可能希望Ember将其更改回false
,从而取消选中它。但这不是它的工作方式。 如开头所述,访问DOM的成本很高。 这包括DOM中的_reading_。 如果在每次更新时我们都必须从DOM中读取最新值以进行比较,那么这将很不利于优化的目的。 因此,为了避免这种情况,我们记住了我们写入DOM的最后一个值,并将当前值与缓存的值进行比较,而不必从DOM中读取回去。 仅当存在差异时,我们才将新值写入DOM(并在下一次缓存它)。 从某种意义上说,我们有点相同的“虚拟DOM”方法,但是我们只在叶节点执行此操作,而不是虚拟整个DOM的“树形”。
因此,TL; DR“绑定”
checked
属性(或文本字段的value
属性,等等)并不能像您期望的那样工作。 想象一下,如果您渲染了<div>{{this.name}}</div>
并使用jQuery
或chrome检验器手动更新了textContent
div
元素的jQuery
。 您不会期望Ember注意到这一点并为您更新this.name
。 这基本上是同一件事:由于对checked
属性的更新发生在Ember之外(通过浏览器的复选框默认行为),因此Ember不会知道这一点。这就是
{{input}}
助手存在的原因。 它必须在基础HTML元素上注册相关的事件侦听器,并将操作反映到适当的属性更改中,以便可以通知感兴趣的各方(例如,呈现层)。我不确定那会给我们留下什么。 我理解为什么这令人惊讶,但是我倾向于说这是一系列不幸的用户错误。 也许我们应该反对在输入元素上绑定这些属性?