NgFor不会使用Angular2中的Pipe更新数据


113

在这种情况下,我使用ngFor以下命令在视图中显示学生列表(数组):

<li *ngFor="#student of students">{{student.name}}</li>

每当我将其他学生添加到列表中时,它都会更新,这真是太好了。

然而,当我给它一个pipefilter由学生的名字,

<li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>

在我过滤学生姓名字段中输入内容之前,它不会更新列表。

这里是plnkr的链接。

Hello_world.html

<h1>Students:</h1>
<label for="newStudentName"></label>
<input type="text" name="newStudentName" placeholder="newStudentName" #newStudentElem>
<button (click)="addNewStudent(newStudentElem.value)">Add New Student</button>
<br>
<input type="text" placeholder="Search" #queryElem (keyup)="0">
<ul>
    <li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>
</ul>

sort_by_name_pipe.ts

import {Pipe} from 'angular2/core';

@Pipe({
    name: 'sortByName'
})
export class SortByNamePipe {

    transform(value, [queryString]) {
        // console.log(value, queryString);
        return value.filter((student) => new RegExp(queryString).test(student.name))
        // return value;
    }
}


14
添加pure:false在您的管道,并changeDetection: ChangeDetectionStrategy.OnPush在您的组件。
埃里克·马丁内斯

2
谢谢@EricMartinez。有用。但是你能解释一下吗?
Chu Son

2
另外,我建议不要.test()在您的过滤器功能中使用。这是因为,如果用户输入的字符串包含特殊含义的字符,例如:*+等,则代码将中断。我认为您应该将.includes()查询字符串与自定义函数一起使用或转义。
艾奇

6
添加pure:false并使管道处于有状态状态将解决此问题。无需修改ChangeDetectionStrategy。
像素位

2
对于任何阅读此书的人来说,Angular Pipes的文档都变得更好了,并且遍历了此处讨论的许多相同内容。看看这个。
0xcaff '16

Answers:


153

为了完全理解问题和可能的解决方案,我们需要讨论角度变化检测-用于管道和组件。

管道变化检测

无状态/纯管道

默认情况下,管道是无状态/纯管道。无状态/纯管道仅将输入数据转换为输出数据。他们什么都不记得,所以他们没有任何属性–只是一种transform()方法。因此,Angular可以优化无状态/纯管道的处理:如果输入不变,则无需在变更检测周期内执行管道。对于诸如{{power | exponentialStrength: factor}}powerfactor为输入。

对于这个问题,"#student of students | sortByName:queryElem.value"studentsqueryElem.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的答案。

但是,我们可以提高效率,并且正如我们将看到的那样,我们在管道实现中不需要array属性,并且不需要双重更改检测的解决方法。

组件变化检测

默认情况下,在每个浏览器事件中,Angular更改检测都会遍历每个组件以查看其是否更改-检查输入和模板(可能还有其他内容?)。

如果我们知道某个组件仅取决于其输入属性(和模板事件),并且输入属性是不可变的,则可以使用效率更高的onPush更改检测策略。使用此策略,仅在输入更改和模板事件触发时才检查组件,而不是检查每个浏览器事件。而且,显然,Expression ... has changed after it was checked使用此设置不会出现该错误。这是因为onPush直到再次对其进行“标记”(ChangeDetectorRef.markForCheck())才再次检查该组件。因此,模板绑定和有状态管道输出仅执行/评估一次。除非输入改变,否则无状态/纯管道仍然不会执行。因此,这里我们仍然需要一个有状态管道。

这是@EricMartinez建议的解决方案:带onPush更改检测的有状态管道。有关此解决方案,请参见@caffinatedmonkey的答案。

请注意,使用此解决方案,该transform()方法无需每次都返回相同的数组。我发现有些奇怪:没有状态的有状态管道。再想一想...有状态管道可能应该总是返回相同的数组。否则,它只能与onPush开发模式下的组件一起使用。


所以毕竟,我认为我喜欢@Eric和@pixelbits的答案的组合:有状态管道,该管道返回相同的数组引用,并onPush在组件允许的情况下进行更改检测。由于有状态管道返回相同的数组引用,因此该管道仍可与未配置的组件一起使用onPush

Plunker

这可能会成为Angular 2惯用语:如果数组正在馈送管道,并且该数组可能会更改(数组中的项,而不是数组引用),则需要使用有状态的管道。


感谢您对问题的详细解释。尽管将数组的更改检测实现为标识而不是相等比较,但这很奇怪。可以通过Observable解决此问题吗?
马丁·诺瓦克

@MartinNowak,如果您要问数组是否是可观察的数组,并且管道是无状态的...我不知道,我还没有尝试过。
Mark Rajcok '16

另一种解决方案是纯管道,其中输出数组使用事件侦听器跟踪输入数组中的更改。
Tuupertunut

我刚刚派出了您的Plunker,它对我有用,无需onPush更改检测plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview
Dan

27

正如埃里克·马丁内斯在评论中指出,添加pure: false到您的Pipe装饰,并changeDetection: ChangeDetectionStrategy.OnPush以你的Component装饰将解决您的问题。这是一个正在工作的朋克。更改为ChangeDetectionStrategy.Always,也可以。这就是为什么。

根据管道上的angular2指南

默认情况下,管道是无状态的。我们必须通过将装饰器的pure属性设置为来声明管道是有状态的。此设置告诉Angular的更改检测系统在每个循环中检查此管道的输出,无论其输入是否已更改。@Pipefalse

至于ChangeDetectionStrategy,默认情况下,每个绑定都会检查所有绑定。当pure: false添加管道时CheckAlwaysCheckOnce出于性能原因,我认为更改检测方法从更改为。使用OnPush,仅当输入属性更改或触发事件时才检查Component的绑定。有关变更检测器(的重要组成部分)的更多信息angular2,请查看以下链接:


pure: false添加管道后,我相信更改检测方法将从CheckAlways更改为CheckOnce” –这与您在文档中引用的内容不一致。我的理解如下:默认情况下,每个周期都会检查无状态管道的输入。如果有更改,transform()则调用管道的方法以(重新)生成输出。transform()每个周期都会调用有状态管道的方法,并检查其输出是否有变化。另请参阅stackoverflow.com/a/34477111/215945
Mark Rajcok 2015年

@MarkRajcok,糟糕,您是对的,但是如果是这样,为什么更改更改检测策略有效?
0xcaff 2015年

1
好问题。似乎将更改检测策略更改为OnPush(即,组件标记为“不可变”)时,有状态管道输出不必“稳定”-即,似乎该transform()方法仅运行一次(可能全部运行)。组件的更改检测仅运行一次)。与@pixelbits的答案相反,该transform()方法被多次调用,并且必须稳定(根据pixelbits的其他答案),因此需要使用相同的数组引用。
Mark Rajcok 2015年

@MarkRajcok如果您说的是正确的,那么就效率而言,这可能是更好的方法,在这种特定的用例中,因为transform仅当推送新数据时才调用它,而不是多次推送直到输出稳定,对吗?
0xcaff 2015年

1
可能要看我冗长的答案。我希望有关于改变检测角2.更多的文档
马克Rajcok

22

Demo Plunkr

您无需更改ChangeDetectionStrategy。实施有状态的管道足以使一切正常。

这是一个有状态管道(未进行其他任何更改):

@Pipe({
  name: 'sortByName',
  pure: false
})
export class SortByNamePipe {
  tmp = [];
  transform (value, [queryString]) {
    this.tmp.length = 0;
    // console.log(value, queryString);
    var arr = value.filter((student)=>new RegExp(queryString).test(student.name));
    for (var i =0; i < arr.length; ++i) {
        this.tmp.push(arr[i]);
     }

    return this.tmp;
  }
}

我在没有将changeDectection更改为onPush的情况下收到此错误:[ plnkr.co/edit/j1VUdgJx​​Yr6yx8WgzCId?p=preview] ( Plnkr)Expression 'students | sortByName:queryElem.value in HelloWorld@7:6' has changed after it was checked. Previous value: '[object Object],[object Object],[object Object]'. Current value: '[object Object],[object Object],[object Object]' in [students | sortByName:queryElem.value in HelloWorld@7:6]
Chu Son

1
您能否举例说明为什么需要将值存储到SortByNamePipe中的局部变量,然后在转换函数@pixelbits中将其返回?我还注意到,Angular通过transform函数两次使用changeDetetion.OnPush进行了循环,而在没有它的情况下进行了4次
Chu Son

@ChuSon,您可能正在查看以前版本的日志。我相信,不是返回值数组,而是返回对值数组的引用,该引用可以检测到变化。像素位,您的答案更有意义。
0xcaff 2015年

为了其他读者的利益,关于上面注释中提到的ChuSon错误以及使用局部变量的需要,创建了另一个问题,并由pixelbits回答。
Mark Rajcok 2015年

19

角度文档

纯净和不纯净的管道

管道有两类:纯管道和不纯管道。默认情况下,管道是纯管道。到目前为止,您所看到的每条管道都是纯净的。通过将管道的纯标志设置为false,可以使管道不纯。您可以像这样使FlyingHeroesPipe不纯:

@Pipe({ name: 'flyingHeroesImpure', pure: false })

在此之前,请先从纯管道开始,了解纯净与不纯净之间的区别。

纯管道Angular仅在检测到输入值的纯变化时才执行纯管道。纯粹的更改是对原始输入值(字符串,数字,布尔值,符号)的更改,或者是对象引用(日期,数组,函数,对象)的更改。

Angular忽略(复合)对象内的更改。如果更改输入月份,添加到输入数组或更新输入对象属性,它将不会调用纯管道。

这似乎很严格,但速度很快。对象引用检查非常快速(比对差异的深入检查要快得多),因此Angular可以快速确定是否可以跳过管道执行和视图更新。

因此,当您可以使用变更检测策略时,最好使用纯管道。如果不能,则可以使用不纯的管道。


答案是正确的,但在发布时需要多做些努力
Stefan

2

而不是执行pure:false。您可以使用this.students = Object.assign([],NEW_ARRAY);复制并替换组件中的值。其中NEW_ARRAY是修改后的数组。

它适用于角度6,也应适用于其他角度版本。


0

解决方法:在构造函数中手动导入Pipe并使用此Pipe调用transform方法

constructor(
private searchFilter : TableFilterPipe) { }

onChange() {
   this.data = this.searchFilter.transform(this.sourceData, this.searchText)}

其实你甚至不需要管道


0

添加到管道额外参数,并在数组更改后立即更改它,即使使用纯管道,列表也会刷新

出租项目| 管道:参数


0

在这种情况下,我使用ts文件中的Pipe进行数据过滤。与使用纯管道相比,它在性能上要好得多。在ts中这样使用:

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

  YourFunction(value) {
    this.pipe.transform(value, 'pipeFilter');
  }
}

0

创建不纯净的管道会导致性能损失。因此,不要创建不纯管道,而是在数据更改时通过创建数据副本来更改数据变量的引用,并在原始数据变量中重新分配副本的引用。

            emp=[];
            empid:number;
            name:string;
            city:string;
            salary:number;
            gender:string;
            dob:string;
            experience:number;

            add(){
              const temp=[...this.emps];
              const e={empid:this.empid,name:this.name,gender:this.gender,city:this.city,salary:this.salary,dob:this.dob,experience:this.experience};
              temp.push(e); 
              this.emps =temp;
              //this.reset();
            } 
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.