このシナリオでは、ngFor
を使用してビューに生徒のリスト(配列)を表示しています。
<li *ngFor="#student of students">{{student.name}}</li>
リストに他の学生を追加するたびに更新されるのは素晴らしいことです。
ただし、学生名でpipe
にfilter
を付けると、
<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;
}
}
問題と可能な解決策を完全に理解するために、パイプとコンポーネントのAngular変更検出を議論する必要があります。
デフォルトでは、パイプはステートレス/純粋です。ステートレス/純粋なパイプは、単に入力データを出力データに変換します。彼らは何も覚えていないので、プロパティはありません。ただのtransform()
メソッドです。したがって、Angularは、ステートレス/純粋なパイプの処理を最適化できます。入力が変更されない場合、変更検出サイクル中にパイプを実行する必要はありません。 {{power | exponentialStrength: factor}}
などのパイプの場合、power
およびfactor
は入力です。
この質問では、"#student of students | sortByName:queryElem.value"
、students
、およびqueryElem.value
は入力であり、パイプsortByName
はステートレス/純粋です。 students
は配列(参照)です。
students
は変更されません–したがって、ステートレス/純粋なパイプは実行されません。queryElem.value
が変更されるため、ステートレス/純粋なパイプが実行されます。配列の問題を修正する1つの方法は、生徒が追加されるたびに配列参照を変更することです。つまり、生徒が追加されるたびに新しい配列を作成します。 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 によると、「このエラーはdevモードでのみ発生します(ベータ0でデフォルトで有効になっています)。アプリをブートストラップするときにenableProdMode()
を呼び出すと、エラーはスローされません。 」 ApplicationRef.tick()
のドキュメント 状態:
開発モードでは、tick()は2回目の変更検出サイクルも実行して、変更が検出されないようにします。この2番目のサイクル中に追加の変更が検出されると、アプリ内のバインディングには、単一の変更検出パスでは解決できない副作用があります。この場合、Angularはエラーをスローします。これは、Angularアプリケーションが変更検出パスを1つしか持つことができず、その間にすべての変更検出を完了する必要があるためです。
このシナリオでは、エラーは偽り/誤解を招くと考えています。ステートフルパイプがあり、出力は呼び出されるたびに変化する可能性があります。副作用がある可能性がありますが、それは問題ありません。 NgForはパイプの後に評価されるため、正常に動作するはずです。
ただし、このエラーがスローされると実際には開発できないため、回避策の1つは、パイプ実装に配列プロパティ(つまり、状態)を追加し、常にその配列を返すことです。この解決策については、@ pixelbitsの回答をご覧ください。
ただし、より効率的にすることができます。また、後で説明するように、パイプの実装に配列プロパティは必要なく、二重変更検出の回避策も必要ありません。
デフォルトでは、すべてのブラウザイベントで、Angular変更の検出がすべてのコンポーネントを通過して、変更されたかどうかを確認します。入力とテンプレート(および他の要素)がチェックされます。
コンポーネントが入力プロパティ(およびテンプレートイベント)のみに依存し、入力プロパティが不変であることがわかっている場合は、より効率的なonPush
変更検出戦略を使用できます。この戦略では、すべてのブラウザーイベントをチェックする代わりに、入力が変更され、テンプレートイベントがトリガーされたときにのみコンポーネントがチェックされます。そして、どうやら、この設定ではExpression ... has changed after it was checked
エラーは発生しません。これは、onPush
コンポーネントが再度「マーク」されるまで(ChangeDetectorRef.markForCheck()
)チェックされないためです。そのため、テンプレートバインディングとステートフルパイプ出力は、一度だけ実行/評価されます。ステートレス/純粋なパイプは、入力が変更されない限り実行されません。したがって、ここでもステートフルパイプが必要です。
これは、@ EricMartinezが提案したソリューションです:onPush
変更検出を備えたステートフルパイプ。この解決策については、@ caffinatedmonkeyの回答をご覧ください。
このソリューションでは、transform()
メソッドは毎回同じ配列を返す必要がないことに注意してください。しかし、少し奇妙なことに気付きます。状態のないステートフルパイプです。さらに考えてみると...ステートフルパイプはおそらく常に同じ配列を返すはずです。それ以外の場合は、開発モードのonPush
コンポーネントでのみ使用できます。
ですから、結局のところ、@ Ericと@pixelbitsの回答の組み合わせが好きだと思います。同じ配列参照を返すステートフルパイプで、コンポーネントで許可されている場合はonPush
変更検出を使用します。ステートフルパイプは同じ配列参照を返すため、onPush
で構成されていないコンポーネントでもパイプを使用できます。
これはおそらくAngular 2のイディオムになります。配列がパイプを供給していて、配列が変更される可能性がある場合(配列内のアイテム、配列参照ではない)、ステートフルパイプを使用する必要があります。
Eric Martinezがコメントで指摘したように、Pipe
デコレーターにpure: false
を追加し、Component
デコレーターにchangeDetection: ChangeDetectionStrategy.OnPush
を追加すると問題が解決します。 これが動作するプランクです。ChangeDetectionStrategy.Always
への変更も機能します。その理由は次のとおりです。
パイプはデフォルトでステートレスです。
@Pipe
デコレータのpure
プロパティをfalse
に設定して、パイプがステートフルであることを宣言する必要があります。この設定により、Angularの変更検出システムは、入力が変更されたかどうかにかかわらず、サイクルごとにこのパイプの出力をチェックします。
ChangeDetectionStrategy
に関しては、デフォルトでは、すべてのバインディングが1サイクルごとにチェックされます。 pure: false
パイプが追加されると、パフォーマンス上の理由から、変更検出メソッドがCheckAlways
からCheckOnce
に変わると思います。 OnPush
を使用すると、入力プロパティが変更されたとき、またはイベントがトリガーされたときにのみ、コンポーネントのバインディングがチェックされます。 angular2
の重要な部分である変更検出器の詳細については、次のリンクをご覧ください。
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;
}
}
純粋なパイプと不純なパイプ
パイプには、純粋と不純の2つのカテゴリがあります。パイプはデフォルトでは純粋です。これまでに見たパイプはすべて純粋です。純粋なフラグをfalseに設定して、パイプを不純にします。 FlyingHeroesPipeを次のように不純にすることができます。
@Pipe({ name: 'flyingHeroesImpure', pure: false })
それを行う前に、純粋なパイプから始めて、純粋と不純の違いを理解してください。
純粋なパイプAngularは、入力値への純粋な変更を検出した場合にのみ、純粋なパイプを実行します。純粋な変更とは、プリミティブ入力値(String、Number、Boolean、Symbol)への変更、または変更されたオブジェクト参照(Date、Array、Function、Object)のいずれかです。
Angularは(複合)オブジェクト内の変更を無視します。入力月を変更したり、入力配列に追加したり、入力オブジェクトのプロパティを更新したりしても、純粋なパイプは呼び出されません。
これは制限的に見えるかもしれませんが、高速です。オブジェクト参照チェックは高速であるため、差分の詳細チェックよりもはるかに高速です。したがって、Angularは、パイプの実行とビューの更新の両方をスキップできるかどうかをすばやく判断できます。
このため、変更検出戦略を使用できる場合は、純粋なパイプが望ましいです。できない場合は、不純なパイプを使用できます。
パイプに追加のパラメーターを追加し、配列を変更した直後に変更します。純粋なパイプでもリストが更新されます
アイテムのアイテムをしましょう| pipe:param
この使用例では、データフィルタリングにtsファイルでパイプを使用しました。純粋なパイプを使用するよりも、パフォーマンスがはるかに優れています。次のようにtsで使用します。
import { YourPipeComponentName } from 'YourPipeComponentPath';
class YourService {
constructor(private pipe: YourPipeComponentName) {}
YourFunction(value) {
this.pipe.transform(value, 'pipeFilter');
}
}
回避策:コンストラクターでパイプを手動でインポートし、このパイプを使用して変換メソッドを呼び出します
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バージョンでも機能するはずです。