>  기사  >  웹 프론트엔드  >  Angular의 변경 감지에 대해 자세히 살펴보기

Angular의 변경 감지에 대해 자세히 살펴보기

青灯夜游
青灯夜游앞으로
2021-04-09 11:03:221766검색

이 글에서는 Angular의 변경 감지 방법을 안내합니다. 도움이 필요한 친구들이 모두 참고할 수 있기를 바랍니다.

Angular의 변경 감지에 대해 자세히 살펴보기

원본 링크: https://blog.angularinlength.com/everything-you-need-to-know-about-change-Detection-in-angular-8006c51d206fhttps://blog.angularindepth.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f

这篇文章只是作者自己一边看原文一边随手翻译的,个中错误以及表述多有不正确,阅读原文才是最佳选择。

如果你也像我一样想对Angular的变更检测有更深入的了解,去看源码无疑是必须的选择,因为网络上相关的信息实在是太少了。大多数文章都只是指出每个组件都有自己的变更检测器,但是并没有继续深入,他们大多关注于不可变对象和变更检测策略的使用。所以这篇文章的目的是告诉你,为什么使用不可变对象会有效?变更检测策略是如何影响到检测的?同时,这篇文章也能让你能对不同场景下提出相应的性能优化方法。

这篇文章由两部分组成,第一部分技术性较强且有很多源码部分的引用。主要解释了变更检测在底层的工作细节。基于Angular-4.0.1。需要注意的是,2.4.1之后版本的变更检测策略有较大的变化。如果你对以前版本的变更检测有兴趣,可以阅读这篇回答

相关教程推荐:《angular教程

第二部分主要讲解了如何在应用中使用变更检测,这部分对于Angular2+都是相同的。因为Angular的公共API并没有发生变化。

核心概念-视图View


Angular的文档中通篇都提到了一个Angular应用是一个组件树。但是Angular底层其实使用了一个低级抽象-视图View。视图View和组件之间的关系很直接-一个视图与一个组件相关联,反之亦然。每个视图都在它的component属性中保持了一个与之关联的组件实例的引用。所有的类似于属性检测、DOM更新之类的操作都是在视图上进行的。因此,技术上而言把Angular应用描述成一个视图树更加准确,因为组件是视图的一个高阶描述。在源码中有关视图是这么描述的:

A View is a fundamental building block of the application UI. It is the smallest grouping of Elements which 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.

视图中元素的属性可以发生变化,但是视图中元素的数量和顺序不能变化。如果想要改变的话,需要通过VireContainerRef来执行插入,移动和删除操作。每个视图都会包括多个View Container。

在这篇文章中,组件和组件视图的概念是互相可替代的。

需要注意的是:网络上很多文章都把我们这里所描述的视图作为了变更检测对象或者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会决定是否对这个视图和他所有的子视图运行变更检测。视图有很多状态值,但是在这篇文章中,下面四个状态值最为重要:

// Bitmask of states
export const enum ViewState {
  FirstCheck = 1 << 0,
  ChecksEnabled = 1 << 1,
  Errored = 1 << 2,
  Destroyed = 1 << 3
}

如果CheckedEnabled值为false或者视图处于Errored或者Destroyed状态时,这个视图的变更检测就不会执行。默认情况下,所有视图初始化时都会带上CheckEnabled,除非使用了ChangeDetectionStrategy.onPush blockquote>

🎜이 글은 작성자가 원문을 읽으면서 방금 번역한 글입니다. 오류나 잘못된 표현이 많이 포함되어 있으니 원문을 읽는 것이 최선의 선택입니다. 🎜🎜나처럼 Angular의 변경 감지에 대해 더 깊이 이해하고 싶다면 의심할 여지없이 소스 코드로 이동하는 것이 필수 선택입니다. 왜냐하면 인터넷에는 관련 정보가 너무 적기 때문입니다. 대부분의 기사에서는 각 구성 요소에 고유한 변경 감지기가 있음을 지적하지만 더 이상 설명하지 않습니다. 대부분은 불변 객체 사용과 변경 감지 전략에 중점을 둡니다. 따라서 이 기사의 목적은 불변 객체를 사용하는 것이 왜 효과적인지 설명하는 것입니다. 변경 감지 전략은 감지에 어떤 영향을 미치나요? 동시에 이 문서를 통해 다양한 시나리오에 해당하는 성능 최적화 방법을 제안할 수도 있습니다. 🎜🎜이 문서는 두 부분으로 구성됩니다. 첫 번째 부분은 좀 더 기술적이고 소스 코드에 대한 많은 참조를 포함합니다. 주로 변경 감지의 기본 작업에 대한 세부 사항을 설명합니다. Angular-4.0.1을 기반으로 합니다. 2.4.1 이후 버전의 변경 감지 전략은 큰 변화를 겪었습니다. 이전 버전의 변경 감지에 관심이 있다면 이 답변. 🎜🎜추천 튜토리얼: "각 튜토리얼"🎜🎜두 번째 부분에서는 주로 학습에 대해 설명합니다. 애플리케이션에서 변경 감지를 사용하는 방법은 이 부분은 Angular2+에서도 동일합니다. Angular의 공개 API는 변경되지 않았기 때문입니다. 🎜

핵심 개념 - 뷰


🎜Angular 문서에서는 전체적으로 Angular 애플리케이션이 구성 요소 트리라고 언급합니다. 그러나 Angular의 최하위 레이어는 실제로 낮은 수준의 추상화인 뷰를 사용합니다. 보기 보기와 구성 요소 간의 관계는 간단합니다. 보기는 구성 요소와 연결되고 그 반대도 마찬가지입니다. 각 뷰는 해당 구성 요소 속성에서 연관된 구성 요소 인스턴스에 대한 참조를 유지합니다. 속성 감지 및 DOM 업데이트와 같은 모든 작업은 뷰에서 수행됩니다. 따라서 구성 요소는 뷰에 대한 상위 수준 설명이므로 Angular 애플리케이션을 뷰 트리로 설명하는 것이 기술적으로 더 정확합니다. 뷰는 소스 코드에 다음과 같이 설명되어 있습니다.
🎜🎜🎜뷰는 애플리케이션 UI의 기본 구성 요소로, 함께 생성되고 소멸되는 요소의 가장 작은 그룹입니다.🎜🎜 🎜 뷰는 애플리케이션 인터페이스를 구성하는 가장 작은 단위로 함께 생성되고 소멸되는 일련의 요소의 조합입니다. 🎜🎜🎜뷰에 있는 요소의 속성은 변경될 수 있지만 뷰에 있는 요소의 구조(수 및 순서)는 변경할 수 없습니다. ViewContainerRef. 각 뷰에는 많은 뷰 컨테이너가 포함될 수 있습니다.🎜🎜🎜뷰에 있는 요소의 속성은 변경될 수 있지만 뷰에 있는 요소의 수와 순서는 변경할 수 없습니다. 변경하려면 VireContainerRef를 통해 삽입, 이동, 삭제 작업을 수행해야 합니다. 각 보기에는 여러 보기 컨테이너가 포함됩니다. 🎜🎜이 문서에서는 구성 요소와 구성 요소 보기의 개념을 서로 바꿔서 사용할 수 있습니다. 🎜🎜인터넷의 많은 기사에서는 여기에서 설명하는 보기를 변경 감지 개체 또는 ChangeDetectorRef로 사용한다는 점에 유의해야 합니다. 실제로 Angular에는 변경 감지를 위한 별도의 객체가 없으며 모든 변경 감지는 뷰에서 직접 실행됩니다. 🎜
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
}

뷰 상태


🎜각 뷰에는 이러한 상태 값을 기반으로 하는 고유한 상태가 있습니다. will 이 뷰와 모든 하위 뷰에 대해 변경 감지를 실행할지 여부를 결정합니다. 보기에는 여러 가지 상태 값이 있지만 이 문서에서는 다음 네 가지 상태 값이 가장 중요합니다.
🎜
class ChangeDetectorRef {
  markForCheck() : void
  detach() : void
  reattach() : void
  
  detectChanges() : void
  checkNoChanges() : void
}
🎜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

这个方法简单的禁止了对当前视图的检测;

detach(): void {
    this._view.state &= ~ViewState.checksEnabled;
}

在组件中的使用方法:

export class AComponent {
    constructor(
        private cd: ChangeDectectorRef,
    ) {
        this.cd.detach();
    }
}

这样就会导致在接下来的变更检测中AComponent及子组件都会被跳过。

这里有两点需要注意:

  • 虽然我们只修改了AComponent的state值,但是他的子组件也不会被执行变更检测;
  • 由于AComponent及其子组件不会有变更检测,因此他们的DOM也不会有任何更新

下面是一个简单示例,点击按钮后在输入框中修改就再也不会引起下面的p标签的变化,外部父组件传递进来的值发生变化也不会触发变更检测:

import { Component, OnInit, ChangeDetectorRef } from &#39;@angular/core&#39;;
@Component({
    selector: &#39;app-change-dection&#39;,
    template: `
    <input [(ngModel)]="name">
    <button (click)="stopCheck()">停止检测</button>
    <p>{{name}}</p>
    `,
    styleUrls: [&#39;./change-dection.component.css&#39;]
})
export class ChangeDectionComponent implements OnInit {
    name = &#39;erik&#39;;
    constructor(
        private cd: ChangeDetectorRef,
    ) { }
    ngOnInit() {
    }
    stopCheck() {
        this.cd.detach();
    }
}

reattach

文章第一部分提到:如果AComponent的输入属性aProp发生变化,OnChanges生命周期钩子仍会被调用,这意味着一旦我们得知输入属性发生变化,我们可以激活当前组件的变更检测并在下一个tick中继续detach变更检测。

reattach(): void { 
    this._view.state |= ViewState.ChecksEnabled; 
}
export class ChangeDectionComponent implements OnInit, OnChanges {
    @Input() aProp: string;
    name = &#39;erik&#39;;
    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()中才更正确一点。

maskForCheck

上面的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;
        }
    }
}

detectChanges

Angular提供了一个方法detectChanges,对当前组件和所有子组件运行一轮变更检测。这个方法会无视组件的ViewState,也就是说这个方法不会改变组件的变更检测策略,组件仍会维持原有的会被检测或不会被检测状态。

export class AComponent {
  @Input() inputAProp;

  constructor(public cd: ChangeDetectorRef) {
    this.cd.detach();
  }

  ngOnChanges(values) {
    this.cd.detectChanges();
  }
}

通过这个方法我们可以实现一个类似Angular.js的手动调用脏检查。

checkNoChanges

这个方法是用来当前变更检测没有产生任何变化。他执行了文章第一部分1,7,8三个操作,并在发现有变更导致DOM需要更新时抛出异常。

结束!哈!

更多编程相关知识,请访问:编程视频!!

위 내용은 Angular의 변경 감지에 대해 자세히 살펴보기의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제