Обнаружение изменений в store.select для подписки требует markForCheck. Почему?

У моего компонента приложения есть подписка на выбор магазина. Я установил ChangeDetectionStrategy на OnPush. Я читал о том, как это работает; Ссылка на объект должна быть обновлена, чтобы изменения вступили в силу. Однако, когда вы используете async pipe, Angular ожидает новых наблюдаемых изменений и выполняет MarkForCheck за вас. Итак, почему мой код не отображает каналы (если я не вызываю MarkForCheck), когда срабатывает подписка, и я устанавливаю channels$ новый наблюдаемый массив каналов.

@Component({
  selector: 'podcast-search',
  changeDetection: ChangeDetectionStrategy.OnPush, // turn this off if you want everything handled by NGRX. No watches. NgModel wont work
  template: `
    <h1>Podcasts Search</h1>
    <div>
      <input name="searchValue" type="text" [(ngModel)]="searchValue" ><button type="submit" (click)="doSearch()">Search</button>
    </div>
    <hr>
    <div *ngIf="showChannels">

      <h2>Found the following channels</h2>
      <div *ngFor="let channel of channels$ | async" (click)="loadChannel( channel )">{{channel.trackName}}</div>
    </div>
  `,
})

export class PodcastSearchComponent implements OnInit {
  channels$: Observable<Channel[]>;
  searchValue: string;
  showChannels = false;
  test: Channel;

  constructor(
    @Inject( Store)  private store: Store<fromStore.PodcastsState>,
    @Inject( ChangeDetectorRef ) private ref: ChangeDetectorRef,
  ) {}

  ngOnInit() {

    this.store.select( fromStore.getAllChannels ).subscribe( channels =>{
      if ( channels.length ) {
        console.log('channels', !!channels.length, channels);
        this.channels$ =  of ( channels );
        this.showChannels = !!channels.length;
        this.ref.markForCheck();
      }
    } );
  }

Я пробовал несколько решений, включая использование subject и вызов next, но это не сработает, если я не вызову MarkForCheck.

Кто-нибудь может сказать мне, как я могу избежать звонка markForCheck?


person Mattijs    schedule 22.07.2018    source источник


Ответы (1)


Это может быть немного сложно объяснить, но я сделаю все возможное. Когда ваш исходный Observable (магазин) излучает, он не привязан к шаблону. Поскольку вы используете обнаружение изменений OnPush, когда этот наблюдаемый объект излучает, он не отмечает компонент для изменений из-за отсутствия привязки.

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

Вы правы, полагая, что асинхронный конвейер отмечает компонент для изменений при появлении нового значения. Вы можете увидеть это в исходном коде Angular здесь: https://github.com/angular/angular/blob/6.0.9/packages/common/src/pipes/async_pipe.ts#L139

Однако вы заметите, что это работает только в том случае, если значение (называемое async), свойство, которое вы используете с каналом async, соответствует this._obj, объекту, который канал async уже записал как излучаемый наблюдаемый объект.

Поскольку вы выполняете channels$ = <new Observable>, async === this._obj на самом деле неверно, поскольку вы меняете ссылки на объекты. Вот почему ваш компонент не помечен для изменений.

Вы также можете увидеть это в действии в Stackblitz, который я собрал. Первый компонент перезаписывает Observable, переданный в async pipe, тогда как второй не перезаписывает его и обновляет данные, отвечая на внесенные изменения - это то, что вы хотите сделать:

https://stackblitz.com/edit/angular-pgk4pw (я использую timer, потому что это простой способ имитировать сторонний несвязанный наблюдаемый источник. Использование выходной привязки, например, обновление при щелчке мышью, более сложно настроить, поскольку, если это выполняется в том же компоненте, выходное действие вызывает отметку об изменениях).

Для вас еще не все потеряно - я бы посоветовал вам сделать вместо этого this.channels$ = this.store.select(...). Труба async сделает .subscribe за вас. Если вы используете канал async, вам в любом случае не следует использовать .subscribe.

this.channels$ = this.store.select(fromStore.getAllChannels).pipe(
  filter(channels => channels.length),
  // channels.length should always be truthy at this point
  tap(channels => console.log('channels', !!channels.length, channels),
);

Обратите внимание, что вы также можете использовать ngIf с асинхронным конвейером, что должно избавить вас от необходимости в showChannels.

person Explosion Pills    schedule 22.07.2018
comment
Спасибо за развернутый ответ. Я думаю, что ваш анализ верен, поскольку позже я заметил, что если я дважды нажму кнопку поиска, то второй раз отобразится результат поиска. Это доказывает то, что вы говорите о сбросе новой ссылки из async var в новое присваивание Observable. Я попробовал ваше предложение, но у меня другое странное поведение. Для начала, пока мой as-шаблон *ngFor не отображается из-за того, что showChannels является ложным. В моей консоли ничего не регистрируется, пока я не установлю showChannels в значение true и не нажму на поиск. Продолжение следует в комментарии 2 - person Mattijs; 24.07.2018
comment
Когда я заменяю showChannels в шаблоне на (channels$ | async) ?.length !== 0, он работает, но журнал консоли запускается дважды. Вероятно, потому что ngIf запускает наблюдаемый канал и фактический ngFor. Ваш filter подход не помог мне из-за проблемы с набором текста, но когда я заменяю его на map( channels => channels ),, я могу сделать channels.length в кране. Однако эта настройка работает только в том случае, если я не использую `* ngIf * вокруг цикла for, как описано выше. Мне все это очень интересно. (не по теме: довольно сложно ответить на вопрос в комментарии ...) - person Mattijs; 24.07.2018
comment
Подумайте о том, чтобы задать еще один вопрос - person Explosion Pills; 24.07.2018
comment
У меня проблема с набором текста с предложенным вами filter(channels => channels.length), потому что он требует двойного взрыва (!!), чтобы стать логическим. Я задаю еще один вопрос по другому вопросу - person Mattijs; 26.07.2018
comment
stackoverflow.com/ questions / 51536817 / - person Mattijs; 26.07.2018