Лучше ли подписываться внутри подписки или использовать rxjs concat с tap

#javascript #angular #typescript #rxjs #observable

#javascript #angular #typescript #rxjs #наблюдаемый

Вопрос:

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

У меня есть два решения, но интересно, какое из них лучшее и есть ли какие-либо преимущества использования concat с tap в этом примере:

подписка внутри подписки:

 ...
this.reserveDataItem();
...

reserveDataItem(){
   this.myService.reserveData(this.selectedData).subscribe(res => {
     if (res.status == 'SUCCESS'){
       ...logSuccessMessagesAndOtherStuff
     }
     this.getReservedDataItemsId();
   });
 }


getReservedDataItemsId(){
   this.myService.getReservedDataItemsForRequestId(this.requestId).subscribe(res => {
     if (res.status == 'SUCCESS'){
       ..doStuffWithDataItems
     }
   });
 }
  

concat с помощью tap:
Я использовал tap, потому что не мог найти, как обрабатывать разные типы возвращаемых данных в одной подписке
, и поэтому мне интересно, есть ли здесь какое-либо преимущество..

 ...
concat(this.reserveDataItem(), this.getReservedDataItemsId())
   .subscribe();
...

reserveDataItem(): Observable<ApiResponseDto<any>>{
  return this.myService.reserveData(this.selectedData).pipe(
    tap(res => {
      if (res.status == 'SUCCESS'){
        ...logSuccessMessagesAndOtherStuff
      }
    })
  )
 }


getReservedDataItemsId():Observable<ApiResponseDto<DataItemDto[]>>{
   return this.myService.getReservedDataItemsForRequestId(this.requestId).pipe(
    tap(res => {
       if (res.status == 'SUCCESS'){
         ..doStuffWithDataItems
       }
     })
  )
 }

  

Ответ №1:

Насколько я понимаю ваш вопрос и проблему:

Вы записываете некоторые данные на серверной части (первый наблюдаемый), и после того, как данные будут записаны / сохранены, вы получите ответ (заполните первый наблюдаемый). Статус ответа из первого требования не обязательно должен быть «успешным», вы хотите сделать еще один запрос независимо от того, какой ответ вы получите. Только после завершения первого наблюдаемого вы хотите сделать еще один запрос для получения этих данных.

Предполагая, что ваш reserveData метод выполняет один Http-запрос, я бы использовал switchMap :

"For instance, when using switchMap each inner subscription is completed when the source emits, allowing only one active inner subscription." (из https://www.learnrxjs.io /)

Вот мой упрощенный код, как я бы решил проблему:

 myObs = new Observable((observer) => {
    observer.next({status: 'SUCCESS'});
    observer.complete();
});

myObs2 = new Observable((observer) => {
    observer.next({status: 'SUCCESS'});
    observer.complete();
});

this.myObs.pipe(
  switchMap((resp) => {
    if (resp.status === 'SUCCESS') ...logSuccessMessagesAndOtherStuff;
    return this.myObs2;
  })
).subscribe(resp => {
  if (resp.status === 'SUCCESS') ...doStuffWithDataItems;
})
  

Вот мой вопрос, который отчасти связан с вашей проблемой. Http-запрос / ответ выдает «только последнее уведомление (результат HTTP-запроса) возвращается как видно», поэтому, на мой взгляд, вам не нужно использовать mergeMap или concatMap (опять же, предполагая, что ваш reserveData метод в основном выполняет один Http-запрос и возвращает ответ).

Комментарии:

1. Действительно, предпосылка проблемы верна. Обе подписки просто выполняют http-вызов. Спасибо за ваше решение, но опять же вопрос в том .. есть ли какое-либо преимущество в этом случае использования решения rxjs вместо внутренней подписки?

2. о, хорошо.. Итак, на мой взгляд, это во многом вопрос чистого, читаемого и современного кода. Я действительно не знаю, работает ли использование rxjs switchMap быстрее или нет, но, несомненно, используя его, вы показываете, что знаете, что делаете / что пытаетесь архивировать, и код легко сканируется и понятен.

3. Поскольку ваш вопрос касается только последовательных наблюдаемых, наибольшая разница, на мой взгляд, заключается в стиле, но если вы спросили о разных методах, таких как mergeMap vs Promises, здесь вы можете прочитать хорошую статью о преимуществах RxJS. medium.com/@ravishivt/batch-processing-with-rxjs-6408b0761f39 . Я думаю, краткое содержание статьи — хороший ответ на ваш вопрос.

Ответ №2:

Вместо этого используйте forkJoin :

     const result1 = this.myService.reserveData(this.selectedData);
    const result2 = this.myService.getReservedDataItemsForRequestId(this.requestId);
    
    getData(): Observable<any> {
      return forkJoin([result1, result2]);
    }
    
    this.getData().subscribe(res => {
       //...logSuccessMessagesAndOtherStuff res[0]
       //..doStuffWithDataItems res[1]
    }, err => {
       console.log(err);
    });
  

Комментарии:

1. Хорошо, но forkJoin не гарантирует (насколько мне известно), что первый наблюдаемый объект будет завершен до вызова второго.. так что это не очень хорошее решение

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

3. ДА… но если getReservedDataItemsId завершится первым, он не будет содержать вновь зарезервированные данные, которые не завершили свое резервирование на серверной части с помощью функции reserveDataItem .. верно? Это означает, что значение, которое будет выдано, не будет правильным для варианта использования, упомянутого в вопросе.

4. О’кей, я думаю, что я не понимаю ваш запрос в первый раз, поэтому вы будете использовать другой оператор RxJS, например (mergeMap или switchMap)

Ответ №3:

Это должно имитировать ваш код без вложенных подписок:

 this.myService.reserveData(this.selectedData).pipe(
  mergeMap(res => {
    if (res.status == 'SUCCESS'){
      ...logSuccessMessagesAndOtherStuff
    }
    return this.myService.getReservedDataItemsForRequestId(this.requestId)
  })
).subscribe(res => {
  if (res.status == 'SUCCESS'){
    ..doStuffWithDataItems
  }
});
  

Не зная больше о том, что вы здесь делаете, трудно дать совет о том, какой из них лучше. Приведенный выше код позволяет вам расширять конвейер данных с помощью дополнительных преобразований (с помощью map) или фильтров без особых хлопот. Таким образом, вы не будете продолжать вкладывать все больше и больше подписок.

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

 this.myService.reserveData(this.selectedData).pipe(
  mergeMap(res => {
    if (res.status == 'SUCCESS'){
      ...logSuccessMessagesAndOtherStuff
    }
    return this.myService.getReservedDataItemsForRequestId(this.requestId)
  }),
  filter(res => res.status == 'SUCCESS')
).subscribe(res => {
  // Now we already *know* that res.status is 'SUCCESS' here
  // No need to check
  ..doStuffWithDataItems
});
  

Что делать, если я хочу, чтобы мои данные из серверной части были в определенном формате, или мне нужно только подмножество. Давайте представим, что результат возвращает пользователя с именем, фамилией и текущим возрастом. Я хочу, чтобы имя было обычным (первая буква в верхнем регистре, остальные в нижнем регистре), а фамилия должна быть заглавной. Мне также нужно рассчитать день рождения из года.

 this.myService.reserveData(this.selectedData).pipe(
  mergeMap(res => {
    if (res.status == 'SUCCESS'){
      ...logSuccessMessagesAndOtherStuff
    }
    return this.myService.getReservedDataItemsForRequestId(this.requestId)
  }),
  filter(res => res.status == 'SUCCESS'),
  map(res => ({
    firstName: res.userdata.fname[0].toUpperCase()   res.userdata.fname.toLowerCase().slice(1),
    lastName: res.userdata.lname.toUpperCase(),
    age: (2020 - res.userdata.birthyear)
  }))
).subscribe(user => {
  ..doStuffWithUser
});