>  기사  >  웹 프론트엔드  >  Angular의 onPush 변경 감지 전략을 빠르게 이해

Angular의 onPush 변경 감지 전략을 빠르게 이해

青灯夜游
青灯夜游앞으로
2021-09-09 19:44:362173검색

이 기사는 Angular의 onPush 변경 감지 전략에 대한 심층적인 이해를 제공할 것입니다. 도움이 되기를 바랍니다.

Angular의 onPush 변경 감지 전략을 빠르게 이해

기본 변경 감지 전략

기본적으로 Angular는 변경 감지를 위해 ChangeDetectionStrategy.Default 전략을 사용합니다. ChangeDetectionStrategy.Default策略来进行变更检测。

默认策略并不事先对应用做出任何假设,因此,每当用户事件、记时器、XHR、promise等事件使应用中的数据将发生了改变时,所有的组件中都会执行变更检测。

这意味着从点击事件到从ajax调用接收到的数据之类的任何事件都会触发更改检测。

通过在组件中定义一个getter并且在模板中使用它,我们可以很容易的看出这一点:

@Component({
  template: `
    <h1>Hello {{name}}!</h1>
    {{runChangeDetection}}
  `
})
export class HelloComponent {
  @Input() name: string;

  get runChangeDetection() {
    console.log(&#39;Checking the view&#39;);
    return true;
  }
}
@Component({
  template: `
    <hello></hello>
    <button (click)="onClick()">Trigger change detection</button>
  `
})
export class AppComponent  {
  onClick() {}
}

执行以上代码后,每当我们点击按钮时。Angular将会执行一遍变更检测循环,在console里我们可以看到两行“Checking the view”的日志。

这种技术被称作脏检查。为了知道视图是否需要更新,Angular需要访问新值并和旧值比较来判断是否需要更新视图。

现在想象一下,如果有一个有成千上万个表达式的大应用,Angular去检查每一个表达式,我们可能会遇到性能上的问题。

那么有没有办法让我们主动告诉Angular什么时候去检查我们的组件呢?

OnPush变更检测策略

我们可以将组件的ChangeDetectionStrategy设置成ChangeDetectionStrategy.OnPush

这将告诉Angular该组件仅仅依赖于它的@inputs(),只有在以下几种情况才需要检查:

1. Input引用发生改变

通过设置onPush

기본 전략은 애플리케이션에 대해 미리 가정하지 않습니다. 따라서 사용자 이벤트, 타이머, XHR, 약속 및 기타 이벤트로 인해 애플리케이션의 데이터가 변경될 때마다 모든 구성 요소에서 변경 감지가 수행됩니다.

즉, 클릭 이벤트부터 Ajax 호출에서 수신된 데이터까지의 모든 이벤트가 변경 감지를 트리거한다는 의미입니다.

컴포넌트에서 getter를 정의하고 템플릿에서 이를 사용하면 이를 쉽게 확인할 수 있습니다.

@Component({
  selector: &#39;tooltip&#39;,
  template: `
    <h1>{{config.position}}</h1>
    {{runChangeDetection}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TooltipComponent  {

  @Input() config;

  get runChangeDetection() {
    console.log(&#39;Checking the view&#39;);
    return true;
  }
}
@Component({
  template: `
    <tooltip [config]="config"></tooltip>
  `
})
export class AppComponent  {
  config = {
    position: &#39;top&#39;
  };

  onClick() {
    this.config.position = &#39;bottom&#39;;
  }
}

위 코드를 실행한 후 버튼을 클릭할 때마다. Angular는 변경 감지 루프를 실행하고 콘솔에서 "보기 확인" 로그 두 줄을 볼 수 있습니다.

이 기술을 더티 검사라고 합니다. 뷰를 업데이트해야 하는지 확인하기 위해 Angular는 새 값에 액세스하고 이를 이전 값과 비교하여 뷰를 업데이트해야 하는지 결정해야 합니다.

이제 수천 개의 표현식이 포함된 대규모 애플리케이션이 있고 Angular가 각 표현식을 검사한다고 상상해 보세요. 성능 문제가 발생할 수 있습니다.

그렇다면 구성 요소를 확인할 시기를 Angular에 사전에 알릴 수 있는 방법이 있을까요?

OnPush 변경 감지 전략

구성 요소의 ChangeDetectionStrategyChangeDetectionStrategy.OnPush로 설정할 수 있습니다.

이렇게 하면 구성 요소가 @inputs()에만 의존하고 다음 상황에서만 확인하면 된다는 것을 Angular에 알립니다.

1 . 입력 참조 변경

onPush 변경 감지 전략을 설정하여 불변 객체(또는 나중에 소개될 Observable)를 강제로 사용하도록 Angular와 계약합니다. ).

변경 감지 맥락에서 불변 객체를 사용하면 Angular가 참조가 변경되었는지 확인하여 뷰를 확인해야 하는지 여부를 결정할 수 있다는 이점이 있습니다. 이것은 심층 검사보다 훨씬 쉬울 것입니다.

오브젝트를 수정해보고 결과를 확인해 보겠습니다.

/** Returns false in our case */
if( oldValue !== newValue ) { 
  runChangeDetection();
}
@Component({
  template: `
    <tooltip [config]="config"></tooltip>
  `
})
export class AppComponent  {
  config = {
    position: &#39;top&#39;
  };

  onClick() {
    this.config = {
      position: &#39;bottom&#39;
    }
  }
}

지금 버튼을 클릭하면 로그를 볼 수 없습니다. 이는 Angular가 다음과 유사하게 이전 값과 새 값의 참조를 비교하기 때문입니다.

@Component({
  template: `
    <button (click)="add()">Add</button>
    {{count}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = 0;

  add() {
    this.count++;
  }

}

숫자, 부울, 문자열, null, 정의되지 않음은 모두 기본 유형입니다. 모든 기본 유형은 값으로 전달됩니다. 객체, 배열 및 함수도 값으로 전달되지만 값은 참조 주소의 복사본입니다.

따라서 이 구성 요소에 대한 변경 감지를 실행하려면 이 개체에 대한 참조를 변경해야 합니다.

@Component({
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = 0;

  constructor() {
    setTimeout(() => this.count = 5, 0);

    setInterval(() => this.count = 5, 100);

    Promise.resolve().then(() => this.count = 5); 
    
    this.http.get(&#39;https://count.com&#39;).subscribe(res => {
      this.count = res;
    });
  }

  add() {
    this.count++;
  }
detectChanges()来告诉Angular在该组件和它的子组件中去执行变更检测。

@Component({
  selector: &#39;counter&#39;,
  template: `{{count}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent { 
  count = 0;

  constructor(private cdr: ChangeDetectorRef) {

    setTimeout(() => {
      this.count = 5;
      this.cdr.detectChanges();
    }, 1000);

  }

}

第二个是ApplicationRef.tick(),它告诉Angular来对整个应用程序执行变更检测。

tick() {
 
  try {
    this._views.forEach((view) => view.detectChanges());
    ...
  } catch (e) {
    ...
  }
}

第三是markForCheck(),它不会触发变更检测。相反,它会将所有设置了onPush的祖先标记,在当前或者下一次变更检测循环中检测。

markForCheck(): void { 
  markParentViewsForCheck(this._view); 
}

export function markParentViewsForCheck(view: ViewData) {
  let currView: ViewData|null = view;
  while (currView) {
    if (currView.def.flags & ViewFlags.OnPush) {
      currView.state |= ViewState.ChecksEnabled;
    }
    currView = currView.viewContainerParent || currView.parent;
  }
}

需要注意的是,手动执行变更检测并不是一种“hack”,这是Angular有意的设计并且是非常合理的行为(当然,是在合理的场景下)。

Angular Async  pipe

async pipe会订阅一个 Observable 或 Promise,并返回它发出的最近一个值。

让我们看一个input()객체 참조를 변경한 후 뷰가 확인되고 새 값이 표시되는 것을 볼 수 있습니다.

2. 구성 요소 또는 하위 구성 요소에서 발생하는 이벤트

🎜구성 요소 또는 하위 구성 요소에서 이벤트가 트리거되면 구성 요소의 내부 상태가 업데이트됩니다. 예: 🎜
@Component({
  template: `
    <button (click)="add()">Add</button>
    <app-list [items$]="items$"></app-list>
  `
})
export class AppComponent {
  items = [];
  items$ = new BehaviorSubject(this.items);

  add() {
    this.items.push({ title: Math.random() })
    this.items$.next(this.items);
  }
}
🎜 버튼을 클릭하면 Angular는 변경 감지 루프를 실행하고 뷰를 업데이트합니다. 🎜🎜처음에 말했듯이 모든 비동기 API가 변경 감지를 트리거한다고 생각할 수도 있지만 그렇지 않습니다. 🎜🎜이 규칙은 DOM 이벤트에만 적용된다는 것을 알 수 있습니다. 다음 API는 변경 감지를 트리거하지 않습니다. 🎜
@Component({
  template: `
     <div *ngFor="let item of _items ; ">{{item.title}}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items: Observable<Item>;
  _items: Item[];
  
  ngOnInit() {
    this.items.subscribe(items => {
      this._items = items;
    });
  }

}
🎜 여전히 속성을 업데이트했기 때문에 버튼 클릭과 같은 다음 변경 감지 프로세스에서는 값은 6(5+1)이 됩니다. 🎜🎜3. 디스플레이 변경 감지🎜🎜Angular는 변경 감지를 트리거하는 3가지 방법을 제공합니다. 🎜🎜첫 번째는 Angular에 이 구성 요소와 하위 구성 요소의 변경 감지를 수행하도록 지시하는 DetectChanges()입니다. 🎜
@Component({
  template: `
    <div *ngFor="let item of items | async">{{item.title}}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items;
}
🎜두 번째는 ApplicationRef.tick()으로, Angular가 전체 애플리케이션에서 변경 감지를 수행하도록 지시합니다. 🎜
private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}
🎜세 번째는 변경 감지를 트리거하지 않는 markForCheck()입니다. 대신 현재 또는 다음 변경 감지 주기에 onPush가 설정된 모든 상위 태그를 감지합니다. 🎜
@Component({
  selector: &#39;app-tabs&#39;,
  template: `<ng-content></ng-content>`
})
export class TabsComponent implements OnInit {
  @ContentChild(TabComponent) tab: TabComponent;

  ngAfterContentInit() {
    setTimeout(() => {
      this.tab.content = &#39;Content&#39;; 
    }, 3000);
  }
}
🎜수동으로 변경 감지를 수행하는 것은 "해킹"이 아니며 Angular의 의도적인 설계이며 매우 합리적인 동작입니다(물론 합리적인 시나리오에서). 🎜

Angular Async Pipe🎜🎜async 파이프는 Observable 또는 Promise를 구독하고 방출되는 최신 값을 반환합니다. 🎜🎜 input()이 관찰 가능한 onPush 구성 요소를 살펴보겠습니다. 🎜
@Component({
  selector: &#39;app-tab&#39;,
  template: `{{content}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
  @Input() content;
}
<app-tabs>
  <app-tab></app-tab>
</app-tabs>
🎜버튼을 클릭하면 뷰 업데이트를 볼 수 없습니다. 위에서 언급한 상황 중 어느 것도 발생하지 않았기 때문에 Angular는 현재 변경 감지 주기에서 구성 요소를 확인하지 않습니다. 🎜

现在,让我们加上async pipe试试。

@Component({
  template: `
    <div *ngFor="let item of items | async">{{item.title}}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items;
}

现在可以看到当我们点击按钮时,视图也更新了。原因是当新的值被发射出来时,async pipe将该组件标记为发生了更改需要检查。我们可以在源码中看到:

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}

Angular为我们调用markForCheck(),所以我们能看到视图更新了即使input的引用没有发生改变。

如果一个组件仅仅依赖于它的input属性,并且input属性是observable,那么这个组件只有在它的input属性发射一个事件的时候才会发生改变。

Quick tip:对外部暴露你的subject是不值得提倡的,总是使用asObservable()方法来暴露该observable。

onPush和视图查询

@Component({
  selector: &#39;app-tabs&#39;,
  template: `<ng-content></ng-content>`
})
export class TabsComponent implements OnInit {
  @ContentChild(TabComponent) tab: TabComponent;

  ngAfterContentInit() {
    setTimeout(() => {
      this.tab.content = &#39;Content&#39;; 
    }, 3000);
  }
}
@Component({
  selector: &#39;app-tab&#39;,
  template: `{{content}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
  @Input() content;
}
<app-tabs>
  <app-tab></app-tab>
</app-tabs>

也许你会以为3秒后Angular将会使用新的内容更新tab组件。

毕竟,我们更新来onPush组件的input引用,这将会触发变更检测不是吗?

然而,在这种情况下,它并不生效。Angular不知道我们正在更新tab组件的input属性,在模板中定义input()是让Angular知道应在变更检测循环中检查此属性的唯一途径。

例如:

<app-tabs>
  <app-tab [content]="content"></app-tab>
</app-tabs>

因为当我们明确的在模板中定义了input(),Angular会创建一个叫updateRenderer()的方法,它会在每个变更检测循环中都对content的值进行追踪。

Angular의 onPush 변경 감지 전략을 빠르게 이해

在这种情况下简单的解决办法使用setter然后调用markForCheck()

@Component({
  selector: &#39;app-tab&#39;,
  template: `
    {{_content}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
  _content;

  @Input() set content(value) {
    this._content = value;
    this.cdr.markForCheck();
  }

  constructor(private cdr: ChangeDetectorRef) {}

}

=== onPush++

在理解了onPush的强大之后,我们来利用它创造一个更高性能的应用。onPush组件越多,Angular需要执行的检查就越少。让我们看看你一个真是的例子:

我们又一个todos组件,它有一个todos作为input()。

@Component({
  selector: &#39;app-todos&#39;,
  template: `
     <div *ngFor="let todo of todos">
       {{todo.title}} - {{runChangeDetection}}
     </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
  @Input() todos;

  get runChangeDetection() {
    console.log(&#39;TodosComponent - Checking the view&#39;);
    return true;
  }

}
@Component({
  template: `
    <button (click)="add()">Add</button>
    <app-todos [todos]="todos"></app-todos>
  `
})
export class AppComponent {
  todos = [{ title: &#39;One&#39; }, { title: &#39;Two&#39; }];

  add() {
    this.todos = [...this.todos, { title: &#39;Three&#39; }];
  }
}

上述方法的缺点是,当我们单击添加按钮时,即使之前的数据没有任何更改,Angular也需要检查每个todo。因此第一次单击后,控制台中将显示三个日志。

在上面的示例中,只有一个表达式需要检查,但是想象一下如果是一个有多个绑定(ngIf,ngClass,表达式等)的真实组件,这将会非常耗性能。

我们白白的执行了变更检测!

更高效的方法是创建一个todo组件并将其变更检测策略定义为onPush。例如:

@Component({
  selector: &#39;app-todos&#39;,
  template: `
    <app-todo [todo]="todo" *ngFor="let todo of todos"></app-todo>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
  @Input() todos;
}

@Component({
  selector: &#39;app-todo&#39;,
  template: `{{todo.title}} {{runChangeDetection}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoComponent {
  @Input() todo;

  get runChangeDetection() {
    console.log(&#39;TodoComponent - Checking the view&#39;);
    return true;
  }

}

现在,当我们单击添加按钮时,控制台中只会看到一个日志,因为其他的todo组件的input均未更改,因此不会去检查其视图。

并且,通过创建更小粒度的组件,我们的代码变得更具可读性和可重用性。

原文链接: https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4

原文作者:Netanel Basal

译者:淼淼

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

위 내용은 Angular의 onPush 변경 감지 전략을 빠르게 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

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