この記事では、Angular での変更検出について説明します。一定の参考値があるので、困っている友達が参考になれば幸いです。
元のリンク:
https://blog.angularin Depth.com/everything-you-need-to-know-about-change-detection- in-angular-8006c51d206f
この記事は、原文を読みながら著者が翻訳したものです。間違いや間違った表現が多数あります。原文を読むことをお勧めします。 。 選ぶ。
私と同じように Angular の変更検出をより深く理解したい場合は、ソース コードにアクセスすることが間違いなく必須の選択です。インターネット上には関連情報が少なすぎるためです。ほとんどの記事では、各コンポーネントに独自の変更検出器があることを指摘するだけで、それ以上は触れず、不変オブジェクトの使用と変更検出戦略に焦点を当てています。この記事の目的は、不変オブジェクトの使用がなぜ機能するのかを説明することです。変更検出戦略は検出にどのような影響を与えますか?同時に、この記事では、さまざまなシナリオに対応するパフォーマンスの最適化方法を提案することもできます。
この記事は 2 つの部分で構成されており、最初の部分はより技術的な内容であり、ソース コードへの参照が数多くあります。主に、最下位レベルでの変更検出の仕組みについて詳しく説明します。 Angular-4.0.1 に基づいています。 2.4.1 以降のバージョンの変更検出戦略は大幅に変更されていることに注意してください。以前のバージョンの変更検出に興味がある場合は、この回答を読んでください。
関連チュートリアルの推奨事項: "angular チュートリアル "
2 番目のパートでは、主にアプリケーションで変更検出を使用する方法について説明します。このパートは Angular2 でも同じです。 AngularのパブリックAPIは変わっていないからです。
Angular のドキュメントでは、Angular アプリケーションがコンポーネント ツリーであると全体を通して言及されています。しかし、Angular の最下層は実際には低レベルの抽象化、つまりビューを使用します。ビュー ビューとコンポーネントの関係は単純です。ビューはコンポーネントに関連付けられ、その逆も同様です。各ビューは、そのコンポーネント プロパティに関連するコンポーネント インスタンスへの参照を保持します。属性の検出や DOM の更新などのすべての操作はビュー上で実行されます。したがって、コンポーネントはビューの上位レベルの記述であるため、Angular アプリケーションをビュー ツリーとして記述する方が技術的にはより正確です。関連するビューは、ソース コードで次のように説明されています:
ビューは、アプリケーション UI の基本的な構成要素であり、一緒に作成および破棄される要素の最小のグループです。
View はアプリケーション インターフェイスを構成する最小単位であり、一緒に作成および破棄される一連の要素の組み合わせです。
ビュー内の要素のプロパティは変更できますが、ビュー内の要素の構造 (数と順序) は変更できません。要素の構造を変更するには、挿入、移動、またはViewContainerRef を介してネストされたビューを削除します。各ビューには多くのビュー コンテナを含めることができます。
ビュー内の要素のプロパティは変更できますが、ビュー内の要素の数と順序は変更できません。これを変更したい場合は、VireContainerRef を通じて挿入、移動、削除の操作を実行する必要があります。各ビューには複数のビュー コンテナが含まれます。
この記事では、コンポーネントとコンポーネント ビューの概念は互換性があります。
インターネット上の多くの記事では、ここで説明するビューが変更検出オブジェクトまたは ChangeDetectorRef として使用されていることに注意してください。実際、Angular には変更検出用の個別のオブジェクトはなく、すべての変更検出はビュー上で直接実行されます。
export interface ViewData { def: ViewDefinition; root: RootData; renderer: Renderer2; // index of component provider / anchor. parentNodeDef: NodeDef|null; parent: ViewData|null; viewContainerParent: ViewData|null; component: any; context: any; // Attention: Never loop over this, as this will // create a polymorphic usage site. // Instead: Always loop over ViewDefinition.nodes, // and call the right accessor (e.g. `elementData`) based on // the NodeType. nodes: {[key: number]: NodeData}; state: ViewState; oldValues: any[]; disposables: DisposableFn[]|null; }
各ビューには独自の状態があります。これらの状態の値に基づいて、Angular は次のことを決定します。このビューと他のすべてのビューを使用するには、サブビューで変更検出を実行します。ビューには多くのステータス値がありますが、この記事では、次の 4 つのステータス値が最も重要です:
// Bitmask of states export const enum ViewState { FirstCheck = 1 << 0, ChecksEnabled = 1 << 1, Errored = 1 << 2, Destroyed = 1 << 3 }
CheckedEnabled
値が false## の場合# またはビュー
Errored または
Destroyed 状態の場合、このビューの変更検出は実行されません。デフォルトでは、
ChangeDetectionStrategy.onPush が使用されない限り、すべてのビューは
CheckEnabled で初期化されます。 onPush については後ほど説明します。これらの状態は組み合わせることができ、たとえば、ビューには FirstCheck メンバーと CheckEnabled メンバーの両方を含めることができます。
针对操作视图,Angular中有一些封装出的高级概念,详见这里。一个概念是ViewRef。他的_view属性囊括了组件视图,同时它还有一个方法detectChanges
。当一个异步事件触发时,Angular从他的最顶层的ViewRef开始触发变更检测,然后对子视图继续进行变更检测。
ChangeDectionRef
可以被注入到组件的构造函数中。这个类的定义如下:
export declare abstract class ChangeDetectorRef { abstract checkNoChanges(): void; abstract detach(): void; abstract detectChanges(): void; abstract markForCheck(): void; abstract reattach(): void; } export abstract class ViewRef extends ChangeDetectorRef { /** * Destroys the view and all of the data structures associated with it. */ abstract destroy(): void; abstract get destroyed(): boolean; abstract onDestroy(callback: Function): any }
负责对视图运行变更检测的主要逻辑属于checkAndUpdateView方法。他的大部分功能都是对子组件视图进行操作。从宿主组件开始,这个方法被递归调用作用于每一个组件。这意味着当递归树展开时,在下一次调用这个方法时子组件会成为父组件。
当在某个特定视图上开始触发这个方法时,以下操作会依次发生:
如果这是视图的第一次检测,将ViewState.firstCheck设置为true,否则为false;
检查并更新子组件/指令的输入属性-checkAndUpdateDirectiveInline
更新子视图的变更检测状态(属于变更检测策略实现的一部分)
对内嵌视图运行变更检测(重复列表中的步骤)
如果绑定的值发生变化,调用子组件的onChanges生命周期钩子;
调用子组件的OnInit和DoCheck两个生命周期钩子(OnInit只在第一次变更检测时调用)
在子组件视图上更新ContentChildren列表-checkAndUpdateQuery
调用子组件的AfterContentInit和AfterContentChecked(前者只在第一次检测时调用)-callProviderLifecycles
如果当前视图组件上的属性发生变化,更新DOM
对子视图执行变更检测-callViewAction
更新当前视图组件的ViewChildren列表-checkAndUpdateQuery
调用子组件的AfterViewInit和AfterViewChecked-callProviderLifecycles
对当前视图禁用检测
在以上操作中有几点需要注意
假设我们现在有一棵组件树:
在上面的讲解中我们得知了每个组件都和一个组件视图相关联。每个视图都使用ViewState.checksEnabled初始化了。这意味着当Angular开始变更检测时,整棵组件树上的所有组件都会被检测;
假设此时我们需要禁用AComponent和它的子组件的变更检测,我们只要将它的ViewState.checksEnabled设置为false就行。这听起来很容易,但是改变state的值是一个很底层的操作,因此Angular在视图上提供了很多方法。通过ChangeDetectorRef
每个组件可以获得与之关联的视图。
class ChangeDetectorRef { markForCheck() : void detach() : void reattach() : void detectChanges() : void checkNoChanges() : void }
这个方法简单的禁止了对当前视图的检测;
detach(): void { this._view.state &= ~ViewState.checksEnabled; }
在组件中的使用方法:
export class AComponent { constructor( private cd: ChangeDectectorRef, ) { this.cd.detach(); } }
这样就会导致在接下来的变更检测中AComponent及子组件都会被跳过。
这里有两点需要注意:
下面是一个简单示例,点击按钮后在输入框中修改就再也不会引起下面的p标签的变化,外部父组件传递进来的值发生变化也不会触发变更检测:
import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'app-change-dection', template: ` <input [(ngModel)]="name"> <button (click)="stopCheck()">停止检测</button> <p>{{name}}</p> `, styleUrls: ['./change-dection.component.css'] }) export class ChangeDectionComponent implements OnInit { name = 'erik'; constructor( private cd: ChangeDetectorRef, ) { } ngOnInit() { } stopCheck() { this.cd.detach(); } }
文章第一部分提到:如果AComponent的输入属性aProp发生变化,OnChanges生命周期钩子仍会被调用,这意味着一旦我们得知输入属性发生变化,我们可以激活当前组件的变更检测并在下一个tick中继续detach变更检测。
reattach(): void { this._view.state |= ViewState.ChecksEnabled; }
export class ChangeDectionComponent implements OnInit, OnChanges { @Input() aProp: string; name = 'erik'; constructor( private cd: ChangeDetectorRef, ) { } ngOnInit() { } ngOnChanges(change) { this.cd.reattach(); setTimeout(() => { this.cd.detach(); }); } }
上面这种做法几乎与将ChangeDetectionStrategy改为OnPush是等效的。他们都在第一轮变更检测后禁用了检测,当父组件向子组件传值发生变化时激活变更检测,然后又禁用变更检测。
需要注意的是,在这种情况下,只有被禁用检测分支最顶层组件的OnChanges钩子才会被触发,并不是这个分支的所有组件的OnChanges都会被触发,原因也很简单,被禁用检测的这个分支内不存在了变更检测,自然内部也不会向子元素变更所传递的值,但是顶层的元素仍可以接受到外部变更的输入属性。
译注:其实将retach()和detach()放在ngOnChanges()和OnPush策略还是不一样的,OnPush策略的确是只有在input值的引用发生变化时才出发变更检测,这一点是正确的,但是OnPush策略本身并不影响组件内部的值的变化引起的变更检测,而上例中组件内部的变更检测也会被禁用。如果将这段逻辑放在ngDoCheck()中才更正确一点。
上面的reattach()方法可以对当前组件开启变更检测,然而如果这个组件的父组件或者更上层的组件的变更检测仍被禁用,用reattach()后是没有任何作用的。这意味着reattach()方法只对被禁用检测分支的最顶层组件有意义。
因此我们需要一个方法,可以将当前元素及所有祖先元素直到根元素的变更检测都开启。ChangeDetectorRef提供了markForCheck方法:
let currView: ViewData|null = view; while (currView) { if (currView.def.flags & ViewFlags.OnPush) { currView.state |= ViewState.ChecksEnabled; } currView = currView.viewContainerParent || currView.parent; }
在这个实现中,它简单的向上迭代并启用对所有直到根组件的祖先组件的检查。
这个方法在什么时候有用呢?禁用变更检测策略之后,ngDoCheck生命周期还是会像ngOnChanges一样被触发。当然,跟OnChanges一样,DoCheck也只会在禁用检测分支的顶部组件上被调用。但是我们就可以利用这个生命周期钩子来实现自己的业务逻辑和将这个组件标记为可以进行一轮变更检测。
由于Angular只检测对象引用,我们需要通过对对象的某些属性来进行这种脏检查:
// 这里如果外部items变化为改变引用位置,此组件是不会执行变更检测的 // 但是如果在DoCheck()钩子中调用markForCheck // 由于OnPush策略不影响DoCheck的执行,这样就可以侦测到这个变更 Component({ ..., changeDetection: ChangeDetectionStrategy.OnPush }) MyComponent { @Input() items; prevLength; constructor(cd: ChangeDetectorRef) {} ngOnInit() { this.prevLength = this.items.length; } ngDoCheck() { // 通过比较前后的数组长度 if (this.items.length !== this.prevLength) { this.cd.markForCheck(); this.prevLenght = this.items.length; } } }
Angular提供了一个方法detectChanges
,对当前组件和所有子组件运行一轮变更检测。这个方法会无视组件的ViewState,也就是说这个方法不会改变组件的变更检测策略,组件仍会维持原有的会被检测或不会被检测状态。
export class AComponent { @Input() inputAProp; constructor(public cd: ChangeDetectorRef) { this.cd.detach(); } ngOnChanges(values) { this.cd.detectChanges(); } }
通过这个方法我们可以实现一个类似Angular.js的手动调用脏检查。
这个方法是用来当前变更检测没有产生任何变化。他执行了文章第一部分1,7,8三个操作,并在发现有变更导致DOM需要更新时抛出异常。
结束!哈!
更多编程相关知识,请访问:编程视频!!
以上がAngular での変更検出を詳しく見るの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。