[Перевод] Глубокое понимание стратегии обнаружения изменений Angular onPush

внешний интерфейс Angular.js внешний фреймворк

Оригинальная ссылка:A Comprehensive Guide to Angular onPush Change Detection Strategy

Оригинальный автор: Нетанел Базаль

Переводчик: Мяо Мяо

Стратегия обнаружения изменений по умолчанию

По умолчанию Angular используетChangeDetectionStrategy.Defaultстратегия обнаружения изменений.

Стратегия по умолчанию не делает никаких предположений о приложении заранее, поэтому всякий раз, когда пользовательские события, таймеры, XHR, промисы и т. д. вызывают изменение данных в приложении, обнаружение изменений выполняется во всех компонентах.

Это означает, что все, от события клика до данных, полученных от вызова ajax, вызовет обнаружение изменений.

Мы можем легко увидеть это, определив геттер в компоненте и используя его в шаблоне:

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

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

После выполнения приведенного выше кода всякий раз, когда мы нажимаем кнопку. Angular пройдет цикл обнаружения изменений, и в консоли мы увидим две строки журнала «Проверка представления».

Этот метод называется грязной проверкой. Чтобы узнать, нужно ли обновлять представление, Angular должен получить доступ к новому значению и сравнить его со старым значением, чтобы определить, нужно ли обновлять представление.

Теперь представьте, что если у нас есть большое приложение с тысячами выражений, и Angular проверяет каждое из них, мы можем столкнуться с проблемами производительности.

Итак, есть ли у нас способ активно сообщать Angular, когда проверять наши компоненты?

Стратегия обнаружения изменений OnPush

Мы можем поместить компонентChangeDetectionStrategyустановлен вChangeDetectionStrategy.OnPush.

Это скажет Angular, что компонент зависит только от его@inputs(), что необходимо проверять только в следующих случаях:

1. Inputцитаты изменены

установивonPushСтратегия обнаружения изменений, наше соглашение с Angular требует использования неизменяемых объектов (или наблюдаемых объектов, которые будут представлены позже).

Преимущество использования неизменяемых объектов в контексте обнаружения изменений заключается в том, что Angular может определить, нужно ли проверять представление, проверив, изменилась ли ссылка. Это будет намного проще, чем глубокая проверка.

Давайте попробуем модифицировать объект и посмотрим на результат.

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

  @Input() config;

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

  onClick() {
    this.config.position = 'bottom';
  }
}

На данный момент я не вижу никаких журналов, когда нажимаю кнопку, это потому, что Angular сравнивает старое значение с новой ссылкой на значение, например:

/** Returns false in our case */
if( oldValue !== newValue ) { 
  runChangeDetection();
}

Стоит отметить, что числа, логические значения, строки, null и undefined — все это примитивные типы. Все примитивные типы передаются по значению.Объекты, массивы и функции также передаются по значению, но значениекопия указанного адреса.

Итак, чтобы инициировать обнаружение изменений для этого компонента, нам нужно изменить ссылку на этот объект.

@Component({
  template: `
    <tooltip [config]="config"></tooltip>
  `
})
export class AppComponent  {
  config = {
    position: 'top'
  };

  onClick() {
    this.config = {
      position: 'bottom'
    }
  }
}

После изменения ссылки на объект мы увидим, что представление проверено и отображается новое значение.

2. События, происходящие из этого компонента или его подкомпонентов

Когда событие запускается в компоненте или его подкомпонентах, внутреннее состояние компонента обновляется. Например:


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

  add() {
    this.count++;
  }

}

Когда мы нажимаем кнопку, Angular выполняет цикл обнаружения изменений и обновляет представление.

Вы можете подумать, как мы сказали в начале, что каждый асинхронный API инициирует обнаружение изменений, но это не так.

Вы обнаружите, что это правило применяется только к событиям DOM, следующие API не запускают обнаружение изменений:

@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('https://count.com').subscribe(res => {
      this.count = res;
    });
  }

  add() {
    this.count++;
  }

Обратите внимание, что вы все еще обновляете свойство, поэтому в следующем процессе обнаружения изменений, например при нажатии кнопки, значение счетчика станет равным 6 (5+1).

3. Отображается для обнаружения изменений

Angular дает нам 3 способа запуска обнаружения изменений.

первыйdetectChanges()чтобы указать Angular выполнить обнаружение изменений в этом компоненте и его дочерних компонентах.

@Component({
  selector: 'counter',
  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;
  }
}

Важно отметить, что ручное обнаружение изменений не является «хаком», это по замыслу Angular и является очень разумным поведением (конечно, в разумных сценариях).

🤓 Угловой асинхронный канал

asyncКанал подписывается на Observable или Promise и возвращает самое последнее сгенерированное им значение.

давайте посмотрим на одинinput()Является компонентом onPush наблюдаемого объекта.

@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);
  }
}
@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;
    });
  }

}

Когда мы нажимаем кнопку, мы не видим обновление представления. Это связано с тем, что ни одна из вышеупомянутых ситуаций не произошла, поэтому Angular не проверяет компонент в текущем цикле обнаружения изменений.

Теперь добавимasyncПопробуйте трубу.

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

Теперь вы можете видеть, что когда мы нажимаем кнопку, представление также обновляется. Причина в том, что когда выдается новое значение,asyncТруба помечает компонент как измененный и требует проверки. Мы видим в исходном коде:

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

Angular звонит намmarkForCheck(), поэтому мы можем видеть, что представление обновляется, даже если ссылка ввода не изменилась.

Если компонент зависит только от своего входного свойства, а входное свойство является наблюдаемым, то компонент будет изменяться только тогда, когда его входное свойство генерирует событие.

Небольшой совет: выставлять объект на улицу не рекомендуется, всегда используйтеasObservable()метод выявления наблюдаемого.

👀 onPush и просмотр запроса

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

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

Вы можете подумать, что через 3 секунды Angular обновит компонент вкладки новым содержимым.

В конце концов, мы обновляем входную ссылку компонента onPush, что вызовет обнаружение изменений, верно?

Однако в данном случае это не работает. Angular не знает, что мы обновляем свойство ввода компонента вкладки, определенное в шаблоне.input()это единственный способ сообщить Angular, что это свойство должно быть проверено в цикле обнаружения изменений.

Например:

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

Потому что, когда мы явно определяем в шаблонеinput(), Angular создастupdateRenderer()метод, который отслеживает значение содержимого в каждом цикле обнаружения изменений.

AppComponent.ngfactory.ts

Простое решение в этом случае использует сеттер, а затем вызываетmarkForCheck().

@Component({
  selector: 'app-tab',
  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: 'app-todos',
  template: `
     <div *ngFor="let todo of todos">
       {{todo.title}} - {{runChangeDetection}}
     </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
  @Input() todos;

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

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

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

Недостатком описанного выше подхода является то, что когда мы нажимаем кнопку «Добавить», Angular должен проверять каждую задачу, даже если предыдущие данные не изменились. Так после первого клика в консоли появятся три лога.

В приведенном выше примере нужно проверить только одно выражение, но представьте, если бы это был реальный компонент с несколькими привязками (ngIf, ngClass, выражение и т. д.), это было бы очень дорого.

Мы провели обнаружение изменений даром!

Более эффективный способ — создать компонент todo и определить его стратегию обнаружения изменений как onPush. Например:

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

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

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

}

Теперь, когда мы нажимаем кнопку «Добавить», мы видим только в журнале консоли, потому что ни одна из входов других компонентов TODO не изменилась, поэтому их представления не проверяются.

И, создавая более мелкие компоненты, наш код становится более читабельным и пригодным для повторного использования.