В вашем приложении Angular у вас есть сценарий двух маршрутов, которые загружают один и тот же компонент ComponentA. ComponentA настроен на использование ChangeDetectionStrategy.OnPush. Он проверяет данные маршрута, вызывает REST API для получения данных с сервера, а затем связывает данные с сеткой или таблицей.

  • Вы замечаете, что при посещении первого маршрута все работает нормально. Данные загружаются с сервера, и модель DOM отображает данные правильно.
  • Позже, щелкнув другой маршрут, указывающий на тот же компонент, вы заметите, что старые данные все еще отображаются в модели DOM. Всего через несколько секунд с сервера приходят новые данные, и DOM это отражает.

При переходе к другому маршруту старые данные остаются в DOM в течение времени, прошедшего между щелчком по маршруту и ​​получением новых данных с сервера!

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

Фон

Я предполагаю, что вы хорошо понимаете, как работает Обнаружение изменений в Angular. Если нет, то советую прочитать статью Все, что вам нужно знать об обнаружении изменений в Angular, написанную Max NgWizard K. В статье подробно объясняется, как работает цикл обнаружения изменений в Angular. Две вещи, на которых я хочу, чтобы вы сосредоточились и вынесли из чтения этой статьи:

  • Когда Angular проверяет и обновляет входные параметры дочернего компонента.
  • Когда Angular запускает функцию ngOnChanges () для компонентов, для которых ChangeDetectionStrategy установлено значение OnPush

Кроме того, ознакомьтесь со статьей Стратегия повторного использования компонентов Angular 2, написанной Юлией Пасынковой, где она подробно объясняет, как Angular повторно использует один и тот же экземпляр компонентов для их повторного рендеринга в DOM.

Сценарий в глубине

Рассматриваемый сценарий включает в себя основной компонент ComponentA, который включает другой дочерний компонент, ComponentB, и два маршрута, которые указывают на ComponentA. Это означает, что два маршрута будут загружать один и тот же компонент. На рисунке ниже показано, как все они участвуют в процессе.

В итоге,

  • ComponentB определяется как базовый компонент со стратегией обнаружения изменений, установленной на OnPush.
  • ComponentB определяет @Input для получения данных из внешнего мира.
  • ComponentA определяется как компонент контейнер.
  • ComponentA использует ComponentB, встраивая его на стороне HTML.
  • ComponentB определяет переменную data, которая привязана к входу ComponentB.
  • Маршрут 1 указывает на ComponentA. В данном случае Route 1 - это ссылка для отображения данных блогов.
  • Направьте 2 точки к ComponentA. В данном случае Маршрут 2 - это ссылка для отображения данных о сотрудниках.

Я предоставил образец приложения Angular, чтобы отразить описанный выше сценарий. Вы можете получить доступ к приложению на StackBlitz.

Просматривая код, вы заметите, что я использовал компоненты basic и container, следуя лучшим практикам разработки приложений Angular. Иногда их еще называют тупыми и умными компонентами. Если вам интересно узнать больше о том, как организовать свои компоненты в приложении Angular, перейдите по этой ссылке: Компоненты представления и контейнера.

Эта проблема

Запустите приложение и нажмите на Блоги. ComponentA загружает, извлекает данные с сервера, а затем связывает данные с входными параметрами ComponentB. Рисунок 2 иллюстрирует это действие.

Я использую поддельный серверный провайдер для имитации вызова REST API, размещенного где-то в облаке, для получения некоторых данных. Вы можете проверить код для получения дополнительных сведений.

Теперь нажмите на Сотрудники, чтобы загрузить данные сотрудников.

По задумке Angular повторно использует одни и те же экземпляры ComponentA и его дочерних компонентов при маршрутизации на ComponentA несколько раз. Вы ясно видите, как возникает проблемное поведение. Рисунки 3 и 4 иллюстрируют это.

Даже если вы выбрали новый Маршрут, ComponentA по-прежнему показывает старые данные!

После получения новых данных от сервера ComponentA отражает их на ComponentB и, следовательно, на DOM.

Основная проблема, с которой мы сталкиваемся здесь, заключается в том, что ComponentA продолжает удерживать старые данные, когда новый Route, указывающий также на ComponentA, щелкнул. Только когда с сервера поступят новые данные, ComponentA сбросит DOM и отобразит новый!

Давайте проведем тщательный анализ этой проблемы с точки зрения Angular.

Анализ проблемы

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

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

Когда Angular запускает цикл обнаружения изменений в ComponentA, он сначала запускает поиск своих дочерних компонентов. В этом случае есть только один дочерний компонент, ComponentB.

Затем он обновляет все входные параметры в ComponentB. Это означает, что Angular повторно связывает значения переменных, которые привязаны к входным параметрам на ComponentB. Таким образом, любые изменения локальных переменных будут отражены в этих параметрах.

Angular продолжает цикл обнаружения изменений и проверяет, есть ли какие-либо изменения в значениях входных параметров на ComponentB. При обнаружении изменения запускается функция ngOnChanges () на ComponentB.

Наконец, Angular визуализирует DOM ComponentB с новыми значениями входных параметров, если было изменение, в противном случае он отобразит старые данные.

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

  1. Angular загружает ComponentA при нажатии маршрута Блоги. Это означает, что ComponentB также отображается в DOM.
  2. ComponentA получает данные с сервера, передает их ComponentB через входные параметры.
  3. ComponentB отображает новые данные в DOM.
  4. Когда вы выбираете маршрут Сотрудники, Angular повторно использует одни и те же экземпляры как ComponentA, так и ComponentB. Это означает, что ngOnInit () больше не вызывается. Кроме того, их конструкторы не вызываются.
  5. ComponentA отправит новый запрос на сервер для получения новых данных. Это показано в сценарии 1 ниже.
  6. HTTP-запросы, как правило, по своей природе асинхронны. Следовательно, существует небольшая задержка между отправкой запроса от клиента и получением ответа от сервера. Angular, по замыслу, запускает цикл обнаружения изменений после запуска асинхронного события. Следовательно, он будет выполнять те же шаги, что и выше.
  7. Внутри цикла обнаружения изменений Angular проверяет локальные переменные внутри ComponentA и замечает, что их значения еще не изменились (по-прежнему нет ответа от сервера), поэтому он не будет вызывать ngOnChanges () на ComponentB, поэтому те же самые старые данные будут отображаться в DOM. Именно это и происходит в нашем случае!
  8. Когда новые данные поступают с сервера, Angular выдает еще один цикл обнаружения изменений. В этом случае локальные переменные теперь обновляются внутри ComponentA. Итак, Angular связывает эти переменные с входными параметрами ComponentB. Он заметит, что входные параметры содержат новые данные, следовательно, он вызовет * ngOnChanges () на * ComponentB и отобразит в DOM новые данные. В демонстрационном коде вы заметили, что при получении данных с сервера пользовательский интерфейс обновляется, и теперь отображаются новые данные. Это показано в скрипте 2 ниже.
//////////////
// Script 1 //
//////////////
this.routeSubscription = this.route.params.subscribe( ( { source }) => {
       this.schemaService.getSchema(source).pipe(
         tap( (data: Schema[]) => this.schema = [... data ] ),
         switchMap( () => 
           this.service.getData(source).pipe(
             tap( (data: any) => {
               this.data = [ ... data ];
             }
           )
         )
       )).subscribe();
     });

//////////////
// Script 2 //
//////////////
this.schema = [... data ];
this.data = [ ... data ];

Решение

Мы хотим сбросить пользовательский интерфейс, ожидая поступления новых данных с сервера, после щелчка по второму маршруту. Это означает, что мы хотим сбросить локальные переменные в ComponentA сразу после щелчка по второму маршруту, чтобы Angular мог обнаружить изменение входных параметров ComponentB и, следовательно, ничего не отображает в DOM вместо старых данных.

Для этого мы подключимся к коллекции событий Router и найдем событие NavigationStart. Когда вы щелкаете маршрут, Angular запускает некоторые события, чтобы показать индикатор прогресса в цикле навигации. Одним из таких событий является событие NavigationStart, которое запускается в начале навигации.

this.routerSubscription = this.router.events.subscribe(route => {
  if (route instanceof NavigationStart) {
    this.schema = undefined;
    this.data = undefined;
  }
});

После подписки на коллекцию событий Router код действует только тогда, когда событие имеет тип NavigationStart. Он просто очищает локальные переменные в ComponentA, которые привязаны к входным параметрам ComponentB.

@Component({
  selector: 'c-a',
  template: `<c-b [schema]="schema" [data]="data"></c-b>`,
  styles: [``]
})
export class ComponentA implements OnInit, OnDestroy  {

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

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

Резюме

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