Изучаем mergeMap, switchMap, concatMap и exhaustMap

от автора

Небольшая история

Я Angular разработчик. Это моя первая статья и таких я собираюсь написать много. Пришла эта идея мне в голову потому что иногда, пытаясь найти какую-то информацию в интернете о принципах работы какого-либо инструмента разработки, я не нахожу абсолютно ничего. Это либо ненавистные обожаемые мною доки, в которых написаны очень краткие принципы работы для знающих людей, либо stackoverflow, где кто-то норовит написать кучу слов ни о чём или без практической составляющей, либо просто статьи, не отражающие сути. Но иногда можно наткнуться на знающего человека, который за одну минуту объяснит тебе всю суть и от счастья хочется прыгать. Я решил писать обо всех таких моментах, которые мне очень сложно давались в понимании из-за отсутствия адекватной информации или моей криворукости. Я буду писать подобные статьи прежде всего для себя и если кому-то это поможет — я буду безумно рад, что какому-нибудь камраду не пришлось долго мучиться и понимать смысл того или иного инструмента для разработки. Статьи будут максимально краткими и по делу. Погнали!


Общее

1) У нас есть метод в сервисе нашего Angular приложения

export interface RxjsExample {     id: number;     name: string; }  getRxJxExampleById(id: number): Observable<RxjsExample> {     return this.httpClient.get<RxjsExample>(`${this.basePath}/users/${id}`)         .pipe(             catchError(err => {                 console.log(err)                 return EMPTY;             })         ); }

Тут всё элементарно: происходит запрос на backend и, в зависимости от id, мы получаем разные ответы в виде

{   id: number;   name: string; }

2) Есть backend нашего приложения

mergeConcatSwitchExhaustedRoutes.get('/users/:id', async (req: Request, res: Response): Promise<void> => {     const id = req.params.id;      if (id === '1') {         await wait(3000);         res.status(200).send({             id: 1,             name: 'Первый'         });     }     if (id === '2') {         await wait(1000);         res.status(200).send({             id: 2,             name: 'Второй'         });     }     if (id === '3') {         await wait(5000);         res.status(200).send({             id: 3,             name: 'Третий'         });     }     if (id === '4') {         await wait(2000);         res.status(200).send({             id: 4,             name: 'Четвёртый'         });     } });  const wait = (ms: number): Promise<void > => {     return new Promise(resolve => setTimeout(resolve, ms)); }

Просто роут на бэке, тут в зависимости от id мы возвращаем ответ с определённым ожиданием. Это сделано специально для понимания операторов, описываемых ниже


switchMap

(Злюка)

Начнём с самого простого и понятного оператора switchMap

switchMapExample(): void {     from([1, 2, 3, 4])         .pipe(             switchMap((id) => this.rxjsExampleService.getRxJxExampleById(id)),             tap((result) => console.log(result))         )         .subscribe(); }

Итак, мы эмитим четыре значения подряд, это наши idшки, по ним мы делаем запрос на бэк, как и было сказано выше

Оператор switchMap, получив следующее значение тут же отпишется от предыдущего и подпишется на новый.

Алгоритм:

  • Эмит 1 -> пошёл запрос на сервер для id = 1

  • Эмит 2 -> отписались от выполнения 1, подписались на id = 2

  • Эмит 3 -> отписались от выполнения 2, подписались на id = 3

  • Эмит 4 -> отписались от выполнения 3, подписались на id = 4

  • Дождались ответа от сервера по id = 4

// Ответ {     "id": 4,     "name": "Четвёртый" }

mergeMap

(Добряш)

mergeMapExample(): void {     from([1, 2, 3, 4])         .pipe(             mergeMap((id) => this.rxjsExampleService.getRxJxExampleById(id)),             tap((result) => console.log(result))         )         .subscribe(); }

Оператор mergeMap примет все значения, которые в него пришли и выполнит их в любом порядке, независимо от времени ответа от сервера

Алгоритм:

  • Эмит 1 -> ждём ответа от id = 1 (3 секунды) (параллельно)

  • Эмит 2 -> ждём ответа от id = 2 (1 секунда) (параллельно)

  • Эмит 3 -> ждём ответа от id = 3 (5 секунд) (параллельно)

  • Эмит 4 -> ждём ответа от id = 4 (2 секунды) (параллельно)

Все запросы идут параллельно, никто никого не ждёт

// Первый ответ {     "id": 2,     "name": "Второй" } // Второй ответ {     "id": 4,     "name": "Четвёртый" } // Третий ответ {     "id": 1,     "name": "Первый" } // Четвёртый ответ {     "id": 3,     "name": "Третий" }

concatMap

(Перфекционист)

concatMapExample(): void {     from([1, 2, 3, 4])         .pipe(             concatMap((id) => this.rxjsExampleService.getRxJxExampleById(id)),             tap((result) => console.log(result))         )         .subscribe(); }

Оператор concatMap примет все значения, которые в него пришли и выполнит их в порядке их эмитов, независимо от того как долго он будет получать ответ от сервера по каждому id

Алгоритм

  • Эмит 1 -> ждём ответа от id = 1 (3 секунды) пока не выполнится

  • Эмит 2 -> ждём ответа от id = 2 (1 секунда) пока не выполнится

  • Эмит 3 -> ждём ответа от id = 3 (5 секунд) пока не выполнится

  • Эмит 4 -> ждём ответа от id = 4 (2 секунды) пока не выполнится

// Первый ответ {     "id": 1,     "name": "Первый" } // Второй ответ {     "id": 2,     "name": "Второй" } // Третий ответ {     "id": 3,     "name": "Третий" } // Четвёртый ответ {     "id": 4,     "name": "Четвёртый" }

exhaustMap

(У меня есть задача, отвалите)

exhaustMapExample(): void {     from([1, 2, 3, 4])         .pipe(             exhaustMap((id) => this.rxjsExampleService.getRxJxExampleById(id)),             tap((result) => console.log(result))         )         .subscribe(); }

Оператор exhaustMap, получив первый эмит, забивает на все последующие эмиты, пока не выполнит запрос по первому. То есть idшки 2, 3 и 4 просто пройдут мимо и никогда не выполнятся

Алгоритм:

  • Эмит 1 -> ждём ответа от id = 1 (3 секунды) пока не выполнится

  • Эмит 2 -> не выполняется, ещё первый не выполнился, пролетел

  • Эмит 3 -> не выполняется, ещё первый не выполнился, пролетел

  • Эмит 4 -> не выполняется, ещё первый не выполнился, пролетел

// Первый и единственный ответ {     "id": 1,     "name": "Первый" }

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

Фронт часть

Бэк часть

Это всё. Надеюсь, статья была для вас полезной.

Спасибо 🙂


ссылка на оригинал статьи https://habr.com/ru/post/715882/