Home > Article > Web Front-end > A closer look at change detection in Angular
This article takes you through change detection in Angular. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to everyone.
Original link:
https://blog.angularindepth.com/everything-you-need-to-know-about-change-detection- in-angular-8006c51d206f
This article was just translated by the author while reading the original text. There are many errors and incorrect expressions. It is best to read the original text. choose.
If you want to have a deeper understanding of Angular's change detection like me, going to the source code is undoubtedly a must choice, because there is really too little relevant information on the Internet. Most articles just point out that each component has its own change detector, but don't go any further. Most of them focus on the use of immutable objects and change detection strategies. So the purpose of this article is to tell you why using immutable objects works? How does change detection strategy affect detection? At the same time, this article also allows you to propose corresponding performance optimization methods for different scenarios.
This article consists of two parts. The first part is more technical and has many references to the source code. Mainly explains the details of how change detection works at the bottom level. Based on Angular-4.0.1. It should be noted that the change detection strategy of versions after 2.4.1 has undergone major changes. If you are interested in change detection of previous versions, you can read this answer.
Related tutorial recommendations: "angular Tutorial"
The second part mainly explains how to use change detection in applications. This part is the same for Angular2. Because Angular's public API has not changed.
Angular’s documentation mentions throughout that an Angular application is a component tree. But the bottom layer of Angular actually uses a low-level abstraction - view. Views The relationship between views and components is straightforward - a view is associated with a component and vice versa. Each view maintains a reference to its associated component instance in its component property. All operations such as attribute detection and DOM updates are performed on the view. Therefore, it is technically more accurate to describe an Angular application as a view tree, since components are a higher-level description of views. The relevant view is described in the source code as follows:
A View is a fundamental building block of the application UI. It is the smallest grouping of Elements which are created and destroyed together.
View is the smallest unit that makes up the application interface. It is a combination of a series of elements that are created and destroyed together.
Properties of elements in a View can change, but the structure (number and order) of elements in a View cannot. Changing the structure of Elements can only be done by inserting, moving or removing nested Views via a ViewContainerRef. Each View can contain many View Containers.
The properties of elements in the view can change, but the number and order of elements in the view cannot change. If you want to change it, you need to perform insert, move and delete operations through VireContainerRef. Each view will include multiple View Containers.
In this article, the concepts of component and component view are interchangeable.
It should be noted that many articles on the Internet use the view we describe here as a change detection object or ChangeDetectorRef. In fact, there is no separate object in Angular for change detection, all change detection is run directly on the view.
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; }
Each view has its own state. Based on the values of these states, Angular will decide whether to use this view and all other views. Run change detection on subviews. Views have many status values, but for this article, the following four status values are the most important:
// Bitmask of states export const enum ViewState { FirstCheck = 1 << 0, ChecksEnabled = 1 << 1, Errored = 1 << 2, Destroyed = 1 << 3 }
If the CheckedEnabled
value is false
or the view When in the Errored
or Destroyed
state, change detection for this view will not be performed. By default, all views are initialized with CheckEnabled
unless ChangeDetectionStrategy.onPush
is used. We will talk about onPush later. These states can also be combined. For example, a view can have both FirstCheck and CheckEnabled members.
针对操作视图,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需要更新时抛出异常。
结束!哈!
更多编程相关知识,请访问:编程视频!!
The above is the detailed content of A closer look at change detection in Angular. For more information, please follow other related articles on the PHP Chinese website!