DevGang
Авторизоваться

Angular для младших разработчиков: опасности и сокровища RxJS

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

Опасные операторы

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

combineLatestWith()

Это очень удобный оператор — обычно вы хотите передать туда одну или несколько наблюдаемых объектов и отреагировать на них.

// Mocks of the sources, just for illustration
const showNames$: Observable<boolean> = of(false);
const showEmails$: Observable<boolean> = of(false);
const users$ = of([{name: 'a', email: 'a@a'}]);

// Now let's create an observable 
// using `combineLatestWith()`

const usersList = users$.pipe(
  combineLatestWith(
    showNames$,
    showEmails$,
  ),
  // when `showNames$` OR `showEmails$` generate a new value,
  // we'll get latest values from all of them:
  map(([users, showNames, showEmails]) => {
    if (showEmails && showNames) {
      return users;
    }
    return users.map((user) => ({
      ...user,
      name: showNames ? user.name : '',
      email: showEmails ? user.email : ''
    }));
  })
);

Если хотя бы один из наблюдаемых, переданных в combLatestWith(), испускает, вы получите последние испускаемые значения из каждого наблюдаемого. Но это удобство сопряжено с двумя опасными ловушками:

#1: Он не будет излучаться до тех пор, пока каждая наблюдаемая не создаст хотя бы одно значение.

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

Каждый наблюдаемый объект, переданный в combineLatestWith(), должен быть перепроверен - вы должны быть уверены, что будет выдано хотя бы одно значение.

Если вы не уверены в этом, используйте startWith() и передайте туда какое-то значение по умолчанию, которое ваш код будет ожидать и может обрабатывать. В противном случае слишком легко создать «вечно висящий» наблюдаемый объект, когда задействована функция CombineLatestWith().

const usersList = users$.pipe(
  combineLatestWith(
    showNames$.pipe(startWith(true)), // 👈
    showEmails$.pipe(startWith(true)),
  ),
  map(([users, showNames, showEmails]) => {
    //    
  })
);

#2: Если ваш код вызовет одно из наблюдаемых (переданных в combLatestWith()) для выдачи нового значения, вы создадите бесконечный цикл.

Пример:

observable$.pipe(
  combineWithLatest(toggle$),
  map(([value, currentToggleValue]) => {
    if (value && currentToggleValue) {
      toggle$.next(true); // infinite loop
    }
  })
);

distinctUntilChanged()

Очень удобный оператор, что может пойти не так?

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

В таких случаях вы можете установить аргумент «компаратор» или использовать оператор distinctUntilKeyChanged().

forkJoin()

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

В этом операторе есть 3 опасные вещи:

# 1: Как сказано в документации: “Если в какой-либо момент возникнут какие-либо наблюдаемые ошибки, forkJoin также выдаст ошибку и немедленно откажется от подписки на другие наблюдаемые”.

В большинстве случаев это не то, что вы ожидаете, но обходной путь довольно прост: добавьте catchError(() => of(someValue)) к каждому запросу:

let ob$ = forkJoin([
  req1.pipe(catchError(() => of(someValue))),
  req2.pipe(catchError(() => of(someValue))),
  //...
]);

// or, if you have requests in an array

let ob$ = forkJoin(
  requests.map(req => req.pipe(catchError(() => of(someValue))))
);

Хотя, не возвращайте EMPTY, из-за следующего пункта:

# 2: Как сказано в документации: “...всякий раз, когда какая-либо из заданных наблюдаемых завершается без выдачи какого-либо значения, forkJoin также завершится в этот момент, и он также ничего не выдаст, даже если у него уже есть некоторые последние значения из других наблюдаемых”.

Это случается не так часто, но все же может случиться (если какой-то наблюдаемый в списке вернул EMPTY или фактически не завершился). И в вашем приложении это может создать довольно неприятную ошибку — это будет выглядеть так, как будто ваше приложение не отвечает и ничего не происходит.

Чтобы избежать этого, вы можете использовать defaultIfEmpty():

const ob$ = forkJoin(
  requests.map(req => req.pipe(
    catchError(() => of(someValue)),
    defaultIfEmpty(someValue)
  ))
);

#3: Как сказано в документации: «если есть наблюдаемое, которое никогда не завершится, forkJoin тоже никогда не завершится».

Вполне возможно, что запросы HTTP (XHR) зависнут. В этом случае простым решением является оператор timeout().

Для других (не-HTTP, горячих) наблюдаемых есть вероятность, что вы подписываетесь на просмотр уже отправленного события. Общего решения для таких случаев нет.

takeUntil()

Опасности этого оператора легко избежать: он должен быть последним в последовательности операторов. С двумя исключениями: это должно быть перед shareReplay({refCount: false}) и перед операторами агрегирования (список).

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

Неосторожное использование switchMap()

Знаете ли вы разницу между switchMap(), concatMap(), mergeMap() и ExhaustMap()? Оказывается, люди небрежно используют switchMap() в тех местах, где это может вызвать нежелательные эффекты.

Это не значит, что switchMap() плохой или имеет какие-то недостатки — это суперполезный и хороший оператор, просто люди им злоупотребляют.

Несвязанные методы

Люди ленивы, поэтому мы можем написать такой код, как этот:

ob$.pipe(
  catchError(this.errHandler)
);

Это будет работать до тех пор, пока this.errHandler() не использует this — потому что this будет что-то неожиданное.

Этого довольно легко избежать:

ob$.pipe(
  catchError(() => this.errHandler())
);

Операторы сокровища

takeUntil()

Этот оператор позволит вам избежать утечек памяти в Angular Components — просто добавьте его (последним, как уже было сказано) в список операторов перед subscribe(), и проблема решена! Это самое простое и удобное решение.

Для этого есть супер-удобный линтер.

Вам не нужен этот оператор, если вы подписываетесь с помощью async канала.

finalize()

При составлении последовательности операторов, если вы думаете, что «после того, как все это закончится, мы должны сделать это», вы можете использовать finalize().

Это намного надежнее, чем просто добавить какой-то код в subscribe() или map(), tap(), catchError() — этот оператор будет выполняться, даже если наблюдаемое завершится без выдачи каких-либо значений. А в некоторых случаях это чистое золото.

ob$.pipe(
  tap(() => this.patchState({loading: true})),
  finalize(() => this.patchState({loading: false})),
  exhaustMap(() => this.api.createNewSomething())
);

first()

Используйте этот оператор, когда вы ожидаете, что наблюдаемый объект выдаст только одно значение (например, HTTP-запрос), но вы не можете этого гарантировать — обычно это происходит в функциях/методах/эффектах, где вы принимаете какой-либо наблюдаемый объект в качестве аргумента.

Без этого оператора могут появиться очень любопытные побочные эффекты и ошибки, если наблюдаемый объект начнет выдавать несколько значений, когда ваш код к этому не готов.

throttleTime()

Если вы когда-нибудь пытались воспроизвести эту логику: «принять первое сгенерированное значение, запустить какой-то побочный эффект, затем игнорировать любые последовательные значения в течение N миллисекунд, затем, если за это время было сгенерировано какое-то новое значение — также сгенерировать его», то threadthrow() спасет ваш день.

Он работает аналогично debounceTime(), хотя его аргумент конфигурации имеет leading и trailing параметры, и это поможет вам создать более сложное поведение.

Пример логики, упомянутой выше:

input$.pipe(
  throttleTime(250, undefined, {leading: true, traling: true}),
  distinctUntilChanged(),
  switchMap((input) => this.api.loadItems(value))
);

tapResponse()

И последним в этом списке будет сторонний оператор (не от RxJS).

Вы можете взять его из библиотеки ngrx/component-store.

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

ob$.pipe(
  tapResponse(
    (result) => this.handle(result),
    // the next argument is required, so you will not forget it
    (err) => console?.error(err)
  )
);
#Angular #Начинающим #RxJS
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу