Как лучше всего обрабатывать несколько подписок на одно и то же наблюдаемое в шаблоне? - PullRequest
2 голосов
/ 10 июля 2020

Допустим, у меня есть наблюдаемый объект с именем 'todoList $'. Используя оператор asyn c, я могу автоматически подписаться / отказаться от подписки на него. Проблема в приведенном ниже коде заключается в том, что есть две идентичные подписки на одно и то же наблюдаемое:

<ng-container *ngIf="(todoList$ | async).length > 0>
  <div *ngFor="let todo of todoList$ | async">
    ...

Это не очень DRY, и, следовательно, мы выделяем память для подписки, которая могла бы обрабатываться более эффективно. .

Из-за синтаксиса условия ngIf я не верю, что могу использовать ключевое слово as для создания переменной шаблона для наблюдаемого вывода. Вместо этого работает то, что я использую оператор Rx Js 'share' из файла компонента :

todoList$ = this.store.select(todoList).pipe(tap(x => {console.log('testing')}), share());
//testing  

Без оператора share «тестирование» печатается дважды. Это наводит меня на мысль, что оператор share () решает эту проблему. Если да, не совсем уверен, почему и как? Поскольку это может быть распространенной проблемой / запахом кода, как лучше всего обрабатывать несколько идентичных подписок в одном шаблоне?

Я признаю, что в StackOverflow есть несколько вариантов аналогичного вопроса . Но никто не дал мне именно то, что я ищу.

Ответы [ 5 ]

2 голосов
/ 10 июля 2020

Как правило, я использую оператор shareReplay({ refCount: true, bufferSize: 1 }) в конце каждого Observable внутри моего шаблона. Я также добавляю его в базовые наблюдаемые объекты, которые использую для перехода к другим наблюдаемым объектам, которые затем используются в шаблоне. Это обеспечит совместное использование подписок между всеми подписчиками, и, используя shareReplay, вы также можете получить последний выданный результат внутри вашего компонента, используя take(1).

Причина { refCount: true, bufferSize: 1 } - что если вы просто используете shareReplay(1), это может вызвать утечку подписок, независимо от того, используете ли вы канал async.

Возвращаясь к вашему примеру, ответ, предоставленный Michael D, неплох, и он делает смысл поступать так. Тем не менее, это требует некоторого logi c в шаблоне, что я лично не одобряю.

Итак, пока вы используете shareReplay, нет никаких недостатков в использовании нескольких вызовов async в вашем шаблоне, и вы даже можете сделать их описательными и повторно используемыми в вашем шаблоне, определив их в своем компоненте:

export class TodoComponent {
  readonly todoList$ = this.store.select(todoList).pipe(
    shareReplay({ refCount: true, bufferSize: 1 })
  );

  readonly hasTodos$ = this.todoList$.pipe(
    map((todos) => todos?.length > 0),
    shareReplay({ refCount: true, bufferSize: 1 })
  );
}

Затем вы можете оставить свой шаблон описательным:

<ng-container *ngIf="hasTodos$ | async>
  <div *ngFor="let todo of todoList$ | async">
  <!-- -->

не забудьте свой trackBy!

Если вам не нравится повторять свой код, вы даже можете создать свой собственный оператор и использовать его:

export function shareRef<T>() {
  return (source: Observable<T>) => source.pipe(
    shareReplay({ refCount: true, bufferSize: 1 })
  );
}

, который изменяет вашу наблюдаемую на:

readonly todoList$ = this.store.select(todoList).pipe(
  shareRef()
);
2 голосов
/ 10 июля 2020

Если да, не совсем уверен, почему / как?

Давайте посмотрим, как определяется share() :

function shareSubjectFactory() {
  return new Subject<any>();
}

return (source: Observable<T>) => refCount()(multicast(shareSubjectFactory)(source)) as Observable<T>;

Прежде всего,

(source: Observable<T>) => refCount()(multicast(shareSubjectFactory)(source))

то же самое, что

(source: Observable<T>) => source.pipe(
  multicast(shareSubjectFactory),
  refCount()
)

multicast вернет ConnectableObservable, которое по-прежнему является Observable, но, среди прочего, он предоставляет метод connect.

// Inside `multicast` operator

const connectable: any = Object.create(source, connectableObservableDescriptor);
connectable.source = source;
connectable.subjectFactory = subjectFactory;

return <ConnectableObservable<R>> connectable;

Источник

Еще одна интересная особенность - это то, что когда подписан на , подписчик будет добавлен в список подписчиков Subject, и основной источник не будет подписан на , пока не будет вызван connect:

_subscribe(subscriber: Subscriber<T>) {
  return this.getSubject().subscribe(subscriber);
}

protected getSubject(): Subject<T> {
  const subject = this._subject;
  if (!subject || subject.isStopped) {
    this._subject = this.subjectFactory();
  }
  return this._subject!;
}

Например:

const src$ = privateSrc.pipe(
  tap(() => console.log('from src')),
  share(),
  tap(() => console.log('from share()')),
)

Когда src$ подписан:

// Subscriber #1
src$.subscribe(/* ... */)

, подписчик будет добавлен в список подписчиков Subject и источник src$, будет подписан. Зачем? Поскольку share также использует refCount, который подписывает на источник, если новый подписчик регистрируется, когда не было предыдущих активных подписчиков, и отписывает от источника, если нет больше активных подписчиков.

Давайте посмотрим на другой пример:

const src$ = (new Observable(s => {
  console.warn('[SOURCE] SUBSCRIBED')

  setTimeout(() => {
    s.next(1);
  }, 1000);
})).pipe(share());

// First subscriber,
// because it's the first one, `refCount` will to its job and the source will be subscribed
// and this subscriber will be added to the `Subject`'s subscribers list
// note that the source sends the data asynchronously
src$.subscribe(/* ... */)

// The second subscriber
// since the source is already subscribed, `refCount` won't subscribe to it again
// instead, this new subscriber will be added to `Subject`'s list
src$.subscribe(/* ... */)

После 1s источник отправит значение 1, а субъект получит это значение и будет отправить его своим зарегистрированным подписчикам.

Это как refCount делает свое волшебство c:

// When a new subscriber is registered

(<any> connectable)._refCount++;

// `RefCountSubscriber` will make sure that if no more subscribers are left
// the source will be unsubscribed
const refCounter = new RefCountSubscriber(subscriber, connectable);

// Add the subscriber to the `Subject`'s list
const subscription = connectable.subscribe(refCounter);

if (!refCounter.closed) {
  (<any> refCounter).connection = connectable.connect();
}

return subscription;

И ConnectableObservable.connect определяется следующим образом :

connect(): Subscription {
  let connection = this._connection;
  
  if (!connection) {
    // If the source wasn't subscribed before

    this._isComplete = false;
    connection = this._connection = new Subscription();
    
    // Subscribing to the source
    // Every notification send by the source will be first received by `Subject`
    connection.add(this.source
      .subscribe(new ConnectableSubscriber(this.getSubject(), this)));
    
    /* ... */
  }
  return connection;
}

Итак, если у нас есть src$ наблюдаемый объект, на который необходимо подписаться несколько раз в шаблоне, мы можем применить вышеупомянутые концепции.

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

Если наш шаблон выглядит так:

<!-- #1 -->
<div *ngIf="src$ | async"></div>

<!-- ... -->

<!-- #2 -->
<div *ngIf="src$ | async"></div>

и src$:

src$ = store.pipe(select(/* ... */), share())

, то, если store уже имеет значение, он будет извлечено синхронно , что означает, что когда #1 будет зарегистрирован, store будет подписан и отправит это значение, но обратите внимание, что в это время #2 равно еще не подписан, поэтому он ничего не получит.

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

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

src$ = store.pipe(
  select(/* ... */),
  subscribeOn(asyncScheduler),
  share()
)

subscribeOn(asyncScheduler) это примерно то же, что и задержка подписки на источник с setTimeout(() => {}, 0). Но это позволяет подписаться на #2, так что, когда источник, наконец, подписан, оба подписчика получат это значение.

1 голос
/ 10 июля 2020

Вы можете использовать подпись as в директиве *ngIf, чтобы иметь только одну активную подписку. Попробуйте следующее

<ng-container *ngIf="(todoList$ | async) as todoList">
  <ng-container *ngIf="todoList.length > 0">
    <div *ngFor="let todo of todoList">
      ...
0 голосов
/ 10 июля 2020

Другой вариант (я думаю, что он проще)

<ng-container *ngIf="todoList$|async as todoList;else loading">
    <div *ngFor="let todo of todoList">
        {{todo}}
    </div>
  <div *ngIf="!todoList.length">Empty</div>
</ng-container>
<ng-template #loading>loading...</ng-template>

Другой, с использованием промежуточного объекта (*)

<ng-container *ngIf="{data:todoList$|async} as todoList">
  <div *ngIf="!todoList.data">loading...</div>
    <div *ngFor="let todo of todoList.data">
        {{todo}}
    </div>
  <div *ngIf="!todoList.data.length">Empty</div>
</ng-container>

(*) Посмотрите, что первый * ngIf возвращает всегда верно, но под ng-контейнером у нас есть данные в todoList.data.

0 голосов
/ 10 июля 2020

используйте *ngIf с этим типом условия. Надеюсь, это вам поможет.

<ng-container *ngIf="(todoList$ | async) as todoList">
  <ng-container *ngIf="todoList && todoList != undefined && todoList.length">
    <div *ngFor="let todo of todoList">
      ...
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...