为了充分理解问题和可能的解决方案,我们需要讨论 Angular 更改检测——针对管道和组件。
管道变化检测
无状态/纯管道
默认情况下,管道是无状态的/纯的。无状态/纯管道只是将输入数据转换为输出数据。他们什么都不记得,所以他们没有任何属性——只是一个transform()
方法。因此,Angular 可以优化无状态/纯管道的处理:如果它们的输入没有改变,则管道不需要在更改检测周期中执行。{{power | exponentialStrength: factor}}
对于管道,例如power
和factor
是输入。
对于这个问题,"#student of students | sortByName:queryElem.value"
和students
是queryElem.value
输入,管道sortByName
是无状态/纯的。 students
是一个数组(参考)。
- 添加学生时,数组引用不会改变 -
students
不会改变 - 因此不会执行无状态/纯管道。
- 当在过滤器输入中输入某些内容时,
queryElem.value
确实会发生变化,因此会执行无状态/纯管道。
解决数组问题的一种方法是在每次添加学生时更改数组引用——即,每次添加学生时创建一个新数组。我们可以这样做concat()
:
this.students = this.students.concat([{name: studentName}]);
尽管这可行,但我们的addNewStudent()
方法不应该仅仅因为我们使用管道就必须以某种方式实现。我们想用来push()
添加到我们的数组中。
有状态管道
有状态的管道有状态——它们通常有属性,而不仅仅是一个transform()
方法。即使他们的输入没有改变,他们也可能需要进行评估。当我们指定管道是有状态/非纯管道pure: false
时——那么每当 Angular 的更改检测系统检查组件的更改并且该组件使用有状态管道时,它将检查管道的输出,无论其输入是否已更改。
这听起来像我们想要的,尽管它的效率较低,因为我们希望管道在students
引用没有改变的情况下执行。如果我们只是让管道有状态,我们会得到一个错误:
EXCEPTION: Expression 'students | sortByName:queryElem.value in HelloWorld@7:6'
has changed after it was checked. Previous value: '[object Object],[object Object]'.
Current value: '[object Object],[object Object]' in [students | sortByName:queryElem.value
根据@drewmoore 的回答,“此错误仅在开发模式下发生(从 beta-0 开始默认启用)。如果您enableProdMode()
在引导应用程序时调用,则不会引发错误。” 状态的文档ApplicationRef.tick()
:
在开发模式下,tick() 还会执行第二个更改检测周期,以确保不会检测到进一步的更改。如果在第二个周期中发现了其他更改,则应用程序中的绑定会产生无法在单个更改检测过程中解决的副作用。在这种情况下,Angular 会抛出一个错误,因为 Angular 应用程序只能进行一次更改检测,在此期间必须完成所有更改检测。
在我们的场景中,我认为该错误是虚假/误导性的。我们有一个有状态的管道,每次调用时输出都会改变——它可能有副作用,这没关系。NgFor 在管道之后进行评估,因此它应该可以正常工作。
但是,我们不能在抛出这个错误的情况下进行真正的开发,因此一种解决方法是向管道实现添加一个数组属性(即状态)并始终返回该数组。有关此解决方案,请参阅@pixelbits 的答案。
但是,我们可以更有效率,正如我们将看到的,在管道实现中我们不需要数组属性,也不需要双重变化检测的解决方法。
组件变化检测
默认情况下,在每个浏览器事件中,Angular 更改检测都会检查每个组件是否发生更改——检查输入和模板(可能还有其他内容?)。
如果我们知道一个组件只依赖于它的输入属性(和模板事件),并且输入属性是不可变的,我们可以使用更有效的onPush
变化检测策略。使用这种策略,不是检查每个浏览器事件,而是仅在输入更改和模板事件触发时检查组件。而且,显然,我们不会Expression ... has changed after it was checked
在此设置中遇到该错误。这是因为onPush
组件在再次“标记”( ) 之前不会再次检查ChangeDetectorRef.markForCheck()
。因此模板绑定和有状态管道输出只执行/评估一次。除非它们的输入发生变化,否则无状态/纯管道仍然不会执行。所以我们在这里仍然需要一个有状态的管道。
这是@EricMartinez 建议的解决方案:带onPush
变更检测的有状态管道。有关此解决方案,请参阅@caffinatedmonkey 的答案。
请注意,使用此解决方案,该transform()
方法不需要每次都返回相同的数组。不过我觉得这有点奇怪:一个没有状态的有状态管道。再想一想……有状态的管道可能应该总是返回相同的数组。否则它只能与onPush
开发模式下的组件一起使用。
所以毕竟,我想我喜欢@Eric 和@pixelbits 答案的组合:返回相同数组引用的有状态管道,onPush
如果组件允许,则进行更改检测。由于有状态管道返回相同的数组引用,因此管道仍然可以与未配置的组件一起使用onPush
。
Plunker
这可能会成为 Angular 2 的习惯用法:如果数组正在输入管道,并且数组可能会改变(数组中的项目,即数组引用,而不是数组引用),我们需要使用有状态管道。