Как работает оператор RxJs switchMap

Mar 24, 2019

Как работают в Angular HTTP Observable?

  • Холодный Observable - не будет стартовать эмитить значения, до тех пор пока на него нет подписки.
  • В Angular холодным является http - эмитит лишь одно значение или error, далее они становятся complete, таким образом они не являются долгоиграющими Observable.
  • В большинстве случаев мы не обязаны помнить об отписке от этих Observables, так как они complete после эмита значения.

Симуляция HTTP в Angular:

function simulateHttp(val: any, delay:number) {
    return Observable.of(val).delay(delay);
}

Оператор of с одним значением "emit once and complete" поток, затем мы применяем оператор delay для асинхронности.

Симуляция нескольких HTTP-запросов:

console.log('simulating HTTP requests');

const http1$ = simulateHttp("1", 1000);

const http2$ = simulateHttp("2", 1000);
http1$.subscribe(
    console.log,
    console.error,
    () => console.log('http1$ completed')
);

http2$.subscribe(
    console.log,
    console.error,
    () => console.log('http2$ completed')
);

// Резуьтат:
/*
1
http1$ completed
2
http2$ completed
*/

Введение в SwitchMap оператор

Давайте объединим 2 HTTP запроса. Первый запрос - сохраняем данные пользователя, вторым обновляем некоторые другие данные.

const saveUser$ = simulateHttp("user saved ", 1000);

const httpResult$ = saveUser$.switchMap(sourceValue => {
    console.log(sourceValue);
    return simulateHttp(" data reloaded ", 2000);
});

httpResult$.subscribe(
    console.log,
    console.error,
    () => console.log('completed httpResult$')
);

// результат:
/*
user saved
data reloaded
completed httpResult$
*/

Разберем работу switchMap:

  • saveUser$ HTTP Observable считается Observable источником
  • switchMap отдает resultObservable$, который мы будем называть результирующий Observable
  • если мы не подпишемся на результирующий Observable, то ничего не произойдет
  • после того как Observable источник эмитит значение, значение передается функции, которую мы передали оператору switchMap
  • эта функция должна возвращать Observable, который может быть создан на основе исходного значения или нет
  • возвращаемый Observable называется внутренним Observable
  • внутренний Observable подписан, и его вывод затем эмитится также результирующим Observable
  • когда Observable источник complete, результирующий Observable также complete

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

Почему оператор назван switchMap?

Слово map очевидно, так как мы приводим в соответствие данные из Observable источника в калбэке переданным switchMap.

Почему switch? Проблема в понимании кроется в недолго живущихх HTTP подобных запросов - сначало идет эмит, потом complete, то есть очень конкретный случай. Рассмотрим долгоиграющий поток.

Симуляция AngularFire (долгоиграющего) потока

function simulateFirebase(val: any, delay: number) {
    return Observable.interval(delay).map(index => val + " " + index);
}

Давайте реализуем на основе вышеприведенной функции два потока:

const firebase1$ = simulateFirebase("FB-1 ", 5000);
const firebase2$ = simulateFirebase("FB-2 ", 1000);

firebase1$.subscribe(
    console.log,
    console.error,
    () => console.log('firebase1$ completed')
);

firebase2$.subscribe(
    console.log,
    console.error,
    () => console.log('firebase2$ completed')
);

// результат:
/*
1:  FB-2  0
2:  FB-2  1
3:  FB-2  2
4:  FB-2  3
5:  FB-1  0
6:  FB-2  4
7:  FB-2  5
8:  FB-2  6
9:  FB-2  7
10: FB-2  8
11: FB-1  1
12: FB-2  9
...
*/
//jsfiddle.net/dnzl/wucpgk02/embed/js,html,result/dark/

Долгоиграющие потоки

Особенности:

  • оба потока никогда не завершатся (complete)
  • потоки данного типа будут эмитить новые значения (если новое значение доступно), пока мы не отпишемся от него
  • firebase2$ эмитит значения чаще чем firebase1$ из-за разницы задержки у двух потоков

Понимание switchMap оператора

Давайте возьмем предыдущий пример и применим switchMap (switch map один поток в другой).

const firebase1$ = simulateFirebase("FB-1 ", 5000);
//const firebase2$ = simulateFirebase("FB-2 ", 1000);

const firebaseResult$ = firebase1$.switchMap(sourceValue => {
    console.log("source value " + sourceValue);
    return simulateFirebase("inner observable ", 1000)
});

firebaseResult$.subscribe(
    console.log,
    console.error,
    () => console.log('completed firebaseResult$')
);
// результат:
/*
source value FB-1  0
    inner observable  0
    inner observable  1
    inner observable  2
    inner observable  3
source value FB-1  1
    inner observable  0
    inner observable  1
    inner observable  2
    inner observable  3
source value FB-1  2
    inner observable  0
    inner observable  1
    inner observable  2
...
*/
//jsfiddle.net/dnzl/qehxk9fw/embed/js,html,result/dark/

Причины для термина Switch

  • Также как и с примером HTTP, ничего не произойдет, пока не будет подписан результирующий Observable
  • Observable источник эмитит значение FB-1 0
  • после того как Observable источник эмитит значение, значение передается функции, которую мы передали оператору switchMap
  • внутренний Observable будет подписан
  • результирующий Observable будет эмитить значения, также испускаемые внутренним Observable
  • мы можем видеть что внутренний Observable эмитит значения 0, 1, 2, которые являются выходными данными вновь созданного интервала

Почему термин Switch?

Отличия от HTTP примера: поскольку Observable источник является долгоживущим, он в конечном итоге выдаст новое значение, в данном случае FB-1 1.

  • похоже, что долгоиграющий внутренний Observable, который уже был в индексе 3, больше не используется
  • mapping функция вызывается снова и создается новый внутренний Observable
  • новый внутренний Observable подписан
  • вновь созданный внутренний Observable начинает эмитить значения с индекса 0
  • результирующий Observable эмитит значения испускаемые вновь созданным внутренним Observable

Что случилось с предыдущим запущенным внутренним Observable?

Предыдущий внутренний Observable был отписан, поэтому его значения больше не используются.

результирующий Observable switched (переключился) от испускаемых значений первого внутреннего Observable на испускаемые значения вновь созданного внутреннего Observable

И это объясняет использование термина switch в имени switchMap!

Суммируем

switchMap оператор создает Observable (называемый внутренним Observable) из Observable источника и испускает значения. Когда Observable источник эмитит новое значение, это создает новый внутренний Observable и switch (переключает) на эти новые значения. При этом происходит отписка от предыдущих внутренних Observable, которые создаются на лету, а не от источника.

Второй аргумент оператора switchMap

const course$ = simulateHttp({id:1, description: 'Angular For Beginners'}, 1000);

const httpResult$ = course$.switchMap(
  sourceValue => simulateHttp([... returns a lessons array ...], 2000));

httpResult$.subscribe(
    console.log,
    console.error,
    () => console.log('completed httpResult$')
);

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

Здесь может быть использован второй аргумент функции switchMap. В качестве второго аргумента функции switchMap мы можем передать другую функцю (selector функция), которая позволит нам объединять значения внутреннего и внешнего Observable.

Ниже приведен пример того, как мы можем получить как курс, так и его уроки:

const course$ = simulateHttp({id:1, description: 'Angular For Beginners'}, 1000);

const httpResult$ = course$.switchMap(
    courses => simulateHttp([], 2000),
    (courses, lessons, outerIndex, innerIndex) => [courses, lessons] );

httpResult$.subscribe(
    console.log,
    console.error,
    () => console.log('completed httpResult$')
);

selector функция принимает 4 оргумента:

  • первый аргумент - значение от Observable-источника, которым в нашем случае является объект курса, полученный при первом запросе
  • второй аргумент - значение отвнутреннего Observable, в нашем случае это список уроков
  • третий аргумент - индекс от Observable-источника, в нашем случае это 0 для первого испускаемого значения, 1,2,3 и т.д.
  • четвертый аргумент - внутренний индекс, в нашем случае это 0 для первого внутр. Observable 1, 2 и т.д. до тех пор пока внутр. Observable не будет отписан. Далее для второго внутр. Observable оттсчет начнется с 0.

Результат от результирующего Observable при использование selector функции может быть следующим:

[Object, Array(30)]

На основе rxjs-switchmap-operator

Добавить комментарий
Комментарии:
Leonid Molochniy
Jan 29, 2020
Спасибо тебе огромное!