【问题标题】:NgFor doesn't update data with Pipe in Angular2NgFor 不会在 Angular2 中使用 Pipe 更新数据
【发布时间】:2016-03-31 02:43:21
【问题描述】:

在这种情况下,我使用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;
    }
}

【问题讨论】:

  • 在你的管道中添加pure:false,在你的组件中添加changeDetection: ChangeDetectionStrategy.OnPush
  • 谢谢@EricMartinez。有用。但是你能解释一下吗?
  • 另外,我建议不要在您的过滤功能中使用.test()。这是因为,如果用户输入一个包含特殊含义字符的字符串,例如:*+ 等,您的代码将会中断。我认为您应该使用.includes() 或使用自定义函数转义查询字符串。
  • 添加 pure:false 并使您的管道有状态将解决此问题。不需要修改 ChangeDetectionStrategy。
  • 对于任何阅读本文的人来说,Angular Pipes 的文档已经变得更好,并且涵盖了这里讨论的许多相同内容。 Check it out.

标签: angular angular2-template angular2-directives angular-pipe


【解决方案1】:

为了全面了解问题和可能的解决方案,我们需要讨论 Angular 更改检测——针对管道和组件。

管道变化检测

无状态/纯管道

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

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

  • 添加学生后,数组 reference 不会改变 - 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's answer,“此错误仅发生在开发模式下(从 beta-0 开始默认启用)。如果您在引导应用程序时调用enableProdMode(),则不会引发错误。” docs for 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 的惯用语:如果一个数组正在提供一个管道,并且该数组可能会改变(数组中的项目,即不是数组引用),我们需要使用一个有状态的管道。

【讨论】:

  • 感谢您对问题的详细解释。奇怪的是,数组的变化检测是作为标识而不是相等比较来实现的。用 Observable 可以解决这个问题吗?
  • @MartinNowak,如果你问数组是否是 Observable,并且管道是无状态的......我不知道,我没有尝试过。
  • 另一种解决方案是纯管道,其中输出数组使用事件监听器跟踪输入数组的变化。
  • 我刚刚对你的 Plunker 进行了分叉,它在没有 onPush 更改检测 plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview 的情况下对我有用
  • 在较新版本的 Angular 中,使管道不纯不会导致开发模式出现任何错误,因此现在无需过度复杂化。详情请参考my answer
【解决方案2】:

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

According to the angular2 guide on pipes:

管道默认是无状态的。我们必须通过将@Pipe 装饰器的pure 属性设置为false 来声明管道是有状态的。这个设置告诉 Angular 的变化检测系统在每个周期检查这个管道的输出,不管它的输入是否发生了变化。

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

【讨论】:

  • “当添加pure: false 管道时,我相信更改检测方法会从 CheckAlways 更改为 CheckOnce”——这与您从文档中引用的内容不一致。我的理解如下:默认情况下,每个周期都会检查无状态管道的输入。如果有变化,则调用管道的transform() 方法来(重新)生成输出。每个周期都会调用有状态管道的transform() 方法,并检查其输出是否有变化。另请参阅stackoverflow.com/a/34477111/215945
  • @MarkRajcok,哎呀,你是对的,但如果是这样,为什么更改更改检测策略会起作用?
  • 好问题。看起来,当更改检测策略更改为OnPush(即,组件被标记为“不可变”)时,有状态管道输出不必“稳定”——即,transform() 方法似乎是只运行一次(可能对组件的所有更改检测只运行一次)。对比@pixelbits 的答案,transform() 方法被多次调用,并且它必须稳定(根据 pixelbits 的other answer),因此需要使用相同的数组引用。
  • @MarkRajcok 如果你说的是对的,就效率而言,这可能是更好的方法,在这个特殊的用例中,因为transform 仅在推送新数据时调用,而不是多次调用直到输出稳定,对吧?
  • 可能,请参阅我冗长的回答。我希望有更多关于 Angular 2 中的变更检测的文档。
【解决方案3】:

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

【讨论】:

  • 我在没有将 changeDetection 更改为 onPush 的情况下收到此错误:[plnkr.co/edit/j1VUdgJxYr6yx8WgzCId?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]
  • 你能举例说明为什么需要将值存储到 SortByNamePipe 中的局部变量,然后在变换函数 @pixelbits 中返回它吗?我还注意到 Angular 使用 changeDetetion.OnPush 循环通过变换函数两次,没有它则循环 4 次
  • @ChuSon,您可能正在查看以前版本的日志。我相信,我们不是返回一个值数组,而是返回一个值数组的 reference,这可以检测到变化。 Pixelbits,你的回答更有意义。
  • 为了其他读者的利益,关于上面提到的cmets中的ChuSon错误,以及需要使用局部变量another question was created,并由pixelbits回答。
【解决方案4】:

来自angular documentation

纯和不纯的管道

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

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

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

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

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

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

因此,当您可以接受更改检测策略时,最好使用纯管道。做不到的时候可以用不纯的管子。

【讨论】:

  • 答案是正确的,但发帖时多花点功夫就好了
【解决方案5】:

而不是做纯:假。您可以通过 this.students = Object.assign([], NEW_ARRAY); 深复制和替换组件中的值其中 NEW_ARRAY 是修改后的数组。

它适用于 Angular 6,也适用于其他 Angular 版本。

【讨论】:

    【解决方案6】:

    现在没有必要把事情复杂化了!

    在较新版本的 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));
      }
    }
    

    【讨论】:

      【解决方案7】:

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

      constructor(
      private searchFilter : TableFilterPipe) { }
      
      onChange() {
         this.data = this.searchFilter.transform(this.sourceData, this.searchText)}
      

      其实你甚至不需要管道

      【讨论】:

        【解决方案8】:

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

        let item of items |管道:参数

        【讨论】:

          【解决方案9】:

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

          import { YourPipeComponentName } from 'YourPipeComponentPath';
          
          class YourService {
          
            constructor(private pipe: YourPipeComponentName) {}
          
            YourFunction(value) {
              this.pipe.transform(value, 'pipeFilter');
            }
          }
          

          【讨论】:

            【解决方案10】:

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

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2016-09-01
              • 2016-09-24
              • 1970-01-01
              • 1970-01-01
              • 2018-09-22
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多