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

NgFor doesn't update data with Pipe in Angular2

在这种情况下,我用 ngFor:

向视图显示学生列表(数组)
<li *ngFor="#student of students">{{student.name}}</li>

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

但是,当我按学生姓名给它 pipefilter 时,

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

直到我在过滤学生姓名字段中输入内容后,它才会更新列表。

这是 link 到 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;
    }
}

正如 Eric Martinez 在评论中指出的那样,将 pure: false 添加到您的 Pipe 装饰器并将 changeDetection: ChangeDetectionStrategy.OnPush 添加到您的 Component 装饰器将解决您的问题。 Here is a working plunkr. 更改为 ChangeDetectionStrategy.Always 也有效。原因如下。

According to the angular2 guide on pipes:

Pipes are stateless by default. We must declare a pipe to be stateful by setting the pure property of the @Pipe decorator to false. This setting tells Angular’s change detection system to check the output of this pipe each cycle, whether its input has changed or not.

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

Demo Plunkr

您不需要更改 ChangeDetectionStrategy。实现有状态的 Pipe 足以让一切正常工作。

这是一个有状态的管道(没有进行其他更改):

@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;
  }
}

要完全理解问题和可能的解决方案,我们需要讨论 Angular 变化检测——管道和组件。

管道变化检测

Stateless/pure 管道

默认情况下,管道是 stateless/pure。 Stateless/pure 管道只是将输入数据转换为输出数据。他们什么都不记得,所以他们没有任何属性——只有一个 transform() 方法。 Angular 因此可以优化 stateless/pure 管道的处理:如果它们的输入没有改变,则管道不需要在变化检测周期中执行。对于像 {{power | exponentialStrength: factor}} 这样的管道,powerfactor 是输入。

对于这道题,"#student of students | sortByName:queryElem.value"studentsqueryElem.value是输入,管道sortByName是stateless/pure。 students 是一个数组(参考)。

  • 添加学生时,数组 reference 不会改变 – students 不会改变 – 因此 stateless/pure 管道不会执行.
  • 当在过滤器输入中输入内容时,queryElem.value 会发生变化,因此会执行 stateless/pure 管道。

解决数组问题的一种方法是每次添加学生时更改数组引用——即,每次添加学生时创建一个新数组。我们可以用 concat():

this.students = this.students.concat([{name: studentName}]);

虽然这可行,但我们的 addNewStudent() 方法不应该仅仅因为我们使用管道就必须以某种方式实现。我们想使用 push() 添加到我们的数组中。

状态管道

有状态管道具有状态——它们通常具有属性,而不仅仅是 transform() 方法。即使他们的输入没有改变,他们也可能需要被评估。当我们指定一个管道是 stateful/non-pure – 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

根据, "this error only happens in dev mode (which is enabled by default as of beta-0). If you call enableProdMode() when bootstrapping the app, the error won't get thrown." The docs for ApplicationRef.tick()状态:

In development mode, tick() also performs a second change detection cycle to ensure that no further changes are detected. If additional changes are picked up during this second cycle, bindings in the app have side-effects that cannot be resolved in a single change detection pass. In this case, Angular throws an error, since an Angular application can only have one change detection pass during which all change detection must complete.

在我们的场景中,我认为错误是 bogus/misleading。我们有一个有状态的管道,每次调用时输出都会改变——它可以有 side-effects,这没关系。 NgFor 在管道之后进行评估,因此它应该可以正常工作。

但是,抛出这个错误我们无法真正进行开发,因此一种解决方法是将数组 属性(即状态)添加到管道实现中,并始终 return 该数组.请参阅@pixelbits 对此解决方案的回答。

然而,我们可以更有效率,正如我们将看到的,我们在管道实现中不需要数组 属性,我们也不需要双重变化检测的解决方法.

组件变化检测

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

如果我们知道一个组件只依赖于它的输入属性(和模板事件),并且输入属性是不可变的,我们就可以使用更有效的 onPush 变化检测策略。使用这种策略,不是检查每个浏览器事件,而是仅在输入更改和模板事件触发时检查组件。而且,显然,我们不会通过此设置得到 Expression ... has changed after it was checked 错误。这是因为在再次“标记”(ChangeDetectorRef.markForCheck()) 之前不会再次检查 onPush 组件。所以模板绑定和有状态管道输出只有一次 executed/evaluated。 Stateless/pure 管道仍然不会执行,除非它们的输入发生变化。所以我们这里还是需要一个有状态的管道。

这是@EricMartinez 建议的解决方案:带有 onPush 变化检测的有状态管道。请参阅@caffinatedmonkey 对此解决方案的回答。

请注意,使用此解决方案,transform() 方法不需要每次都 return 相同的数组。不过我觉得这有点奇怪:没有状态的有状态管道。再考虑一下......有状态管道可能应该始终 return 相同的数组。否则它只能在开发模式下与 onPush 组件一起使用。


所以毕竟,我想我喜欢@Eric 和@pixelbits 的答案的组合:return 相同数组引用的有状态管道,如果组件允许,带有 onPush 变化检测它。由于有状态管道 return 是相同的数组引用,因此管道仍可用于未配置 onPush.

的组件

Plunker

这可能会成为一个Angular 2的习语:如果一个数组正在给管道供水,并且数组可能会改变(数组中的项目,而不是数组引用),我们需要使用一个有状态的管道。

来自angular documentation

纯净与不纯净的管道

有两种管道:纯管道和不纯管道。管道默认是纯的。到目前为止,您看到的每根管道都是纯净的。通过将管道的纯标志设置为假,可以使管道不纯。您可以像这样使 FlyingHeroesPipe 不纯:

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

在这样做之前,先了解纯和不纯的区别,从纯管道开始。

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

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

这似乎有限制,但速度也很快。对象引用检查速度很快——比深入检查差异快得多——因此 Angular 可以快速确定它是否可以跳过管道执行和视图更新。

因此,如果您可以接受更改检测策略,则最好使用纯管道。不行的时候可以用不纯的管道。

解决方法:在构造函数中手动导入管道并使用此管道调用转换方法

constructor(
private searchFilter : TableFilterPipe) { }

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

其实你甚至不需要管道

而不是pure:false。可以通过 this.students = Object.assign([], NEW_ARRAY); 深拷贝替换组件中的值其中 NEW_ARRAY 是修改后的数组。

它适用于 angular 6,应该也适用于其他 angular 版本。

给pipe添加额外参数,array改变后马上改变,即使是纯pipe,list也会刷新

让项目的项目 | pipe:param

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

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

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

创建不纯的管道会降低性能。 所以不要创建不纯的管道,而是通过在数据更改时创建数据副本来更改数据变量的引用,并在原始数据变量中重新分配副本的引用。

            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();
            } 

现在没有必要把事情搞得太复杂了!

使管道不纯不会导致新版本 Angular 中的开发模式出现任何错误。我猜当前接受的答案中提到的错误与 this issue 有关,该错误在 5.5(!)年前(发布此问题后不久)得到解决。

据我了解,Angular 现在使用 IterableDiffer 来检测不纯管道返回的数组中的变化,就像它检测出现在模板中的普通数组一样 (当使用默认的变化检测策略时), 所以当数组引用发生变化时它不会认为这是一个问题,前提是它的内容没有。这意味着如果我们每次都生成一个新数组不会有任何错误,所以我们可以让管道不纯,这样就可以了。

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'sortByName',
  pure: false
})
export class SortByNamePipe implements PipeTransform {
  transform(value, queryString) {
    return value.filter(student => new RegExp(queryString).test(student.name));
  }
}