RxJS 系列 – Transformation Operators

前言

前几篇介绍过了 

Creation Operators

Filter Operators

Join Creation Operators

Error Handling Operators

这篇继续介绍 Transformation Operators.

 

参考

Docs – Transformation Operators

 

map

就是 Array 的 map 咯

const obs = from([1, 2, 3, 4]);
obs.pipe(map(v => v + 10)).subscribe(v => console.log(v)); // 11..12..13..14

 

scan

scan 相等于 Array 的 reduce

const obs = from([1, 2, 3, 4]);
obs
  .pipe(
    scan((acc, value, _index) => {
      return acc + value;
    }, 0)
  )
  .subscribe(v => console.log(v)); // 1..3..6..10

每一次 acc 代表上一次 return value

value 表示 obs 这一次的值

而第一次发布的时候, 由于没有上一次, acc 的值将是 init value (也就是 scan 的第二个参数)

 

pairwise

pairwise 是 "一对" 的概念, 它每次接收都是 2 个值, 当前值和上一次值

const obs = from([1, 2, 3, 4]);
obs.pipe(pairwise()).subscribe(v => console.log(v)); // [1,2]..[2,3]..[3,4]

由于第一次发布时, 没有 "上一次值", 所以不会接收, 上面例子中, 发布了 4 次, 但是接收只有 3 次. 

可以在 pairwise 前加一个 startWith,这样就有初始的一对了。

 

concatMap

之前介绍过 concat, 把一堆 Observables 丢给它, 它会从第一个开始 subscribe 直到那一个 Observable complete 后再去 subscribe 下一个 Observable, 直到那一堆 Observable 全部结束.

concat(o1, o2, o3, o4).subscribe()

concatMap 和 concat 概念一样都是 complete 后去 subscribe 下一个. 不同地方在于那一堆 Observables 提供的方式

const obs = from([1, 2, 3, 4]);
obs.pipe(concatMap(v => of(v))).subscribe(v => console.log(v));

concatMap() 接收 obs 的值, 然后返回 Observable. 上面例子中 obs 发布 4 次. concatMap 也就发布 4 个 Observables

而这 4 个 Observables 就被 concat(o1, o2, o3, o4) 了.

所以你也能把它理解为一种动态的 concat. 因为 concat(observables) 是初始化就决定了多少个 observables 被放进去. 

而 concatMap 则是一个一个添加进去的.

 

mergeMap

理解了 concatMap 就理解了 mergeMap. 它就是动态的 merge.

merge 的特点是, 它不像 concat 那样会等待 complete. 它会直接 subscribe 所以的 Observables.

 

switchMap

没有 switch 只有 switchMap. 

switchMap 的接口和 concatMap, mergeMap 一样, 接收 obs 的值, 并且返回一个 Observable

concatMap 和 mergeMap 会把返回的 Observable 堆叠起来 (concat 挨个挨个 subscribe, merge 直接 subscribe all)

但 switchMap 不会把 Observable 堆叠起来, 它会 subscribe Observable, 一旦有下一个 Observable, 它会 unsubscribe 上一个 Observable (丢弃它), 然后 subscribe 下一个 Observable.

这也是为什么没有 switch 只有 switchMap, 因为它只有一个 Observable 不像 merge 和 concat 有一堆 Observable.

const obs = fromEvent(document, 'click');
obs
  .pipe(switchMap(v => fetch('https://random-data-api.com/api/v2/users').then(r => r.json())))
  .subscribe(v => console.log(v));

每次点击就会发 ajax, 如果点击很快, ajax 还没有返回, 那么会放弃上一次的 ajax 请求, 马上在发新的 ajax.

考题1:当 switchMap 遇上 complete

const subject = new Subject<void>();

subject.pipe(switchMap(() => interval(1000))).subscribe({
  next: v => console.log(v),
  complete: () => console.log('complete'),
});

subject.complete();

问:当 subject complete 后,switchMap 的 interval 还会继续 emit 吗?subscribe 会接收到 complete 吗?

答:interval 会继续 emit,subscribe 不会接收到 complete。

为什么呢?

因为这是两件事。

我们换成 concatMap 或 mergeMap 会更容易理解。

subject 的 emit 是创建新的 observable 提供给 concat 或 merge。

它 complete 了也只表示,以后不会再有新的 observable 提供给 concat 或 merge 了。

但是!之前已经提供给 concat 和 merge 的 observable,它们是独立的,和 subject 没有关联。

因此这些 observable 自然会继续 emit。

而只有等到 switchMap 中的 interval complete,subscribe 才会接收到 complete。

考题1:当 switchMap 遇上 shareReplay

const obs = timer(1000).pipe(switchMap(() => interval(1000)));
obs.subscribe(() => console.log('fire1'));
obs.subscribe(() => console.log('fire2'));

obs 被订阅了两次,因此会有两个 interval。

如果我要用 shareReplay 变成一个,我的 shareReplay 要放在哪里?

是放在最外层?

const obs = timer(1000).pipe(
  switchMap(() => interval(1000)),
  shareReplay({ refCount: true, bufferSize: 1 }),
);

还是放在里面

const obs = timer(1000).pipe(
  switchMap(() => interval(1000).pipe(shareReplay({ refCount: true, bufferSize: 1 }))),
);

答:当然是放在外面,switchMap 参数返回的 observable 是给 switchMap 内订阅的,它不是最终返回的 obs,千万不要搞错了。

 

exhaustMap

switchMap 和 exhaustMap 的关系有点类似 debounceTime 和 throttleTime 的关系.

debounceTime 的特色是 delay and keep postpone

throttleTime 的特色是 immediately + skip

switchMap 有新 Observable 它就会丢弃旧的, subscribe 新的. 这样连续就会导致 subscriber 一直接收不到值. 这个就像 debounceTime 的 keep postpone.

exhaustMap 则像 throttleTime, 有 Observable 后它就 subscribe. 在 Observable 没有 complete 前 (注: 一定要 complete 哦, next 不够), 它无视接下来每一个新的 Observables.

 

switchScan

switchScan 和 swtichMap 概念差不多, 只是引入了 scan 的概念.

const obs = fromEvent(document, 'click');
obs
  .pipe(
    switchScan((acc, _value, _index) => {
      return timer(2000).pipe(map(() => acc + 1));
    }, 0)
  )
  .subscribe();

第 1 秒 click, acc = 0 (初始值)

第 2 秒 click, acc = 0 因为返回的 Observable 需要 2 秒, 而 click 太快了, swtich 的概念就是放弃之前的, 拥抱新的.

第 4 秒 click, acc = 1

第 6 秒 click, acc = 2

再一个常见的例子 input filter:

有一个 input,和一个 cars array

const input = new BehaviorSubject<string>('');
const allCars = [
  'Tesla Model S',
  'Tesla Model S+',
  'Tesla Model S++',
  'Nissan Leaf',
  'Chevrolet Bolt',
  'Ford Mustang Mach-E',
  'Porsche Taycan',
  'BMW 7 Series',
  'Mercedes-Benz S-Class',
  'Audi A8',
  'Lexus LS',
  'Jaguar XJ',
];

接着 input filter 

input.next('Tes'); // ['Tesla Model S', 'Tesla Model S+', 'Tesla Model S++']
await firstValueFrom(timer(2000));
input.next('Tesla Model'); // ['Tesla Model S+', 'Tesla Model S++']

由于第二轮的 input 是第一轮的延续,所以它的 result 可以从第一轮 result 中里 filter 出来。

input
  .pipe(
    switchScan<
      string,
      // 每一次返回当前的 search result 和之前的缓存 results
      { searchResult: SearchResult; cachedSearchResults: SearchResult[] },
      Observable<{ searchResult: SearchResult; cachedSearchResults: SearchResult[] }>
    >(
      ({ cachedSearchResults }, searchText) => {
        const currSearchText = searchText.toLowerCase();

        if (currSearchText === '') {
          // 没有 search 就没有 result
          return of({ searchResult: { searchText, cars: [] }, cachedSearchResults });
        }

        // 先找缓存看看
        const cachedSearchResult = cachedSearchResults.find(({ searchText }) =>
          currSearchText.startsWith(searchText.toLowerCase()),
        );
        if (cachedSearchResult) {
          // 有缓存就直接拿缓存做 local filter
          return of({
            searchResult: {
              searchText,
              cars: cachedSearchResult.cars.filter(car => car.toLowerCase().includes(currSearchText)),
            },
            cachedSearchResults,
          });
        }

        // 没有缓存才 ajax
        return timer(600).pipe(
          switchMap(() =>
            timer(500).pipe(
              map(() => {
                const searchResult: SearchResult = {
                  searchText,
                  cars: allCars.filter(car => car.toLowerCase().includes(searchText.toLowerCase())),
                };
                // 记得把 search result 添加到缓存里
                return { searchResult, cachedSearchResults: [...cachedSearchResults, searchResult] };
              }),
            ),
          ),
        );
      },
      { searchResult: { searchText: '', cars: [] }, cachedSearchResults: [] },
    ),
  )
  .subscribe(({ searchResult: { searchText, cars } }) => console.log('cars', [searchText, cars]));

第一轮需要 ajax,第二轮则不需要。

 

concatMap, switchMap, mergeMap, exhaustMap 小结

参考: RxJS 轉換類型 Operators (2) - switchMap / concatMap / mergeMap / exhaustMap

 

bufferCount

bufferCount 的作用是积累,然后发布。

const obs = timer(0, 1000);
obs.pipe(bufferCount(3)).subscribe(values => console.log(values));

效果

obs 发布,subscribe 不会立刻接收,因为中间的 bufferCoutn(3) 把值存起来了,一直存到 obs 发布第三次(因为 bufferCount 是 set 3),它才会把之前存的值合并成 array 一并发布,subscribe 接收到的是 array with 三个值。

startBufferEvery

bufferCount 还有第二个参数叫 startBufferEvery, 虽然冷门, 但也可以了解一下.

它的玩法是这样的

const obs = interval(1000);
obs
  .pipe(bufferCount(3, 2))
  .subscribe(values => console.log((performance.now() / 1000).toFixed(0) + 's', values));

bufferCount(3, 2) 表示每当 obs 发布 2 次, 它就转发一次,而且一次是发三个值

效果

两个知识点:

一, value 2, 4 是重复的, 因为要求 obs 每两次发布要转发三个值,这样还差一个值,需要拿之前的补。

二, 第一次发布等待了三秒, 第二次则是两秒, 因为要求三个值, 而第一次两秒钟收集时并不足够三个值, 而一到三秒中满足了三个值就立刻发布,后续的发布就能到之前的值充数,所以就没有缺值问题了。

 

bufferTime

const obs = timer(0, 1000);
obs.pipe(bufferTime(3000)).subscribe(values => console.log(values));

效果

我们直觉可能会认为 bufferTime 是 obs 发布后,先存着,等三秒后一起发。

但其实不是这样,它是另作一个三秒的 timer,然后每三秒就发布一次,如果这三秒中 obs 都没有发布,那就发布一个空 array。

总之,重点就是它每三秒都会发布一次,不管 obs 有没有发布。

 

buffer

buffer 的区别是, 我们可以控制缓存的值什么时候再发出去. 不只能 by time by count, 可以 by whatever

const obs = timer(0, 1000);
obs.pipe(buffer(fromEvent(document, 'click'))).subscribe(values => console.log(values));

当 document click 的时候把缓存一并发布,其余时间就一直积累。

 

bufferWhen

参考: Stack Overflow – What's the difference between the RxJS operators "buffer" and "bufferWhen"?

bufferWhen 和 buffer 有一个小区别. buffer 的参数是一个 Observable

bufferWhen 的参数是一个 () => Observable

obs
  .pipe(
    bufferWhen(
      () =>
        new Observable(subscriber => {
          console.log('Observable Init');
          document.addEventListener('click', () => {
            subscriber.next();
          });
          return () => {
            console.log('Observable Displose');
          };
        })
    )
  )
  .subscribe(values => console.log(values));

buffer 参数 Observable 只会被 subscribe 一次.

bufferWhen 参数返回的 Observable 在发布之后会重新被 subscribe

上面代码的效果是这样的 

click 之后它就 displose 然后立马在 subscribe。

 

bufferToggle

上面的 buffer operators 都只有一个 "发布时机". 

bufferToggle 有两个时机, 一个是 "发布时机", 另一个是其它 buffer operators 没有的 "开始缓存时机".

 参数一是开始缓存, 参数二是发布.

const obs = timer(0, 1000);
obs
  .pipe(bufferToggle(fromEvent(document, 'click'), () => fromEvent(document, 'contextmenu')))
  .subscribe(values => console.log(values));

效果

我在第三秒的时候 left click 了一下, 第 6 秒 right click 了一下, 所以得到了 [3, 4, 5]

接着在 第 10 秒 left click 了一下, 第 13 秒 right click 了一下, 得到了 [10, 11, 12]

这就是所谓的, 控制开始缓存和发布时机. 不在时机内的值将会丢失. 像 0, 1, 2, 6, 7, 8, 9 都接收不到.

利用这个机制,我们可以做出 buffer like debounce 功能

const subject = new Subject<number>();
const release$ = timer(3000);
const open$ = subject.pipe(throttle(() => release$));
subject.pipe(bufferToggle(open$, () => release$)).subscribe(values => console.log(values));

什么时候开始缓存?当 subject.next 的时候

同时要利用 throttle 限制它,一旦开始了就不再接收 subject.next,等待 release。

上面这个写法很粗糙,因为 subject 被 subscribe 了两次,会导致两个独立的流。

更严谨的写法应该是

export function bufferLikeDebounce<T>(release$: Observable<unknown>): OperatorFunction<T, T[]> {
  const sharedRelease$ = release$.pipe(share());
  return source$ => {
    // 如果 source$ 是带 value 的 (e.g. BehaviourSubject) 那就要用 shareReplay({ refCount: true, bufferSize: 1 })
    const sharedSource$ = source$.pipe(share());
    return sharedSource$.pipe(bufferToggle(sharedSource$.pipe(throttle(() => sharedRelease$)), () => sharedRelease$));
  };
}

需要注意的是,share 和 shareReplay 的不同,如果 source 是带值的(e.g. BehaviourSubject),而我们用 share 就错了。

因此第一次是 bufferToggle subscribe 会发布,会开始 buffer,但是源头是第二次 subscribe,没有 shareReplay 的话,它就不会再发布,这样就错了。

总之,如果 source$ 带值就用 shareReplay,没有带值就用 share,用错就会有 bug。

再不就自己写一个

export function bufferLikeDebounce<T>(release$: Observable<void>): OperatorFunction<T, T[]> {
  return source =>
    new Observable<T[]>(subscriber => {
      const bufferedValues: T[] = [];
      let buffering = false;
      const upStreamCompleteSubject = new Subject<void>();
      const upStreamErrorSubject = new Subject<void>();
      const downStreamUnsubscribeSubject = new Subject<void>();

      const sourceSubscription = source.subscribe({
        next: upstreamValue => {
          bufferedValues.push(upstreamValue);
          if (buffering) return;
          buffering = true;
          merge(release$, upStreamCompleteSubject)
            .pipe(take(1), takeUntil(merge(downStreamUnsubscribeSubject, upStreamErrorSubject)))
            .subscribe(() => {
              buffering = false;
              subscriber.next(bufferedValues.splice(0, bufferedValues.length));
            });
        },
        error: error => {
          upStreamErrorSubject.next();
          subscriber.error(error);
        },
        complete: () => {
          upStreamCompleteSubject.next();
          subscriber.complete();
        },
      });

      return () => {
        downStreamUnsubscribeSubject.next();
        sourceSubscription.unsubscribe();
      };
    });
}
View Code

简单明了。

 

buffer operators 小结

参考: 30 天精通 RxJS (12): Observable Operator - scan, buffer

常用到的是 bufferTime, bufferCount, buffer.

bufferWhen 和 bufferToggle 我目前都没有用过.

 

windowTime, windowCount, window, windowToggle, windowWhen

参考:

RxJS window() Transformation Operator

Stack Overflow – What does the `window` mean in RxJS?

30 天精通 RxJS(20): Observable Operators - window, windowToggle

window 和 buffer 基本上是一样的. 唯一的区别是 buffer 接收的是 Array. window 接收的是 Observable (类似于 from(Array))

我们来看一个对比, 感受一下它们的区别

const obs = timer(0, 1000);
obs.pipe(bufferTime(10000)).subscribe(values => console.log(values));

10 秒钟后会接收到 Array [0, 1, 2 ... 10]

换成 windowTime

const obs = timer(0, 1000);
obs
  .pipe(
    windowTime(10000),
    switchMap(v => v)
  )
  .subscribe(values => console.log(values));

1 秒钟后就会接收到 value 1

一个 10 秒后才接收, 一个第 1 秒就开始接收了, 这就是所谓的 immediately

那什么时候用 window 什么时候用 buffer 呢? 我不清楚, 但我自己的经验是绝大部分情况下用 buffer 就够了.

 

groupBy

和 array 的 groupBy 一个概念

const obs = new Observable<{ name: string; age: number }>(subscriber => {
  subscriber.next({ name: 'dada', age: 1 });
  subscriber.next({ name: 'derrick', age: 2 });
  subscriber.next({ name: 'dada', age: 3 });
  subscriber.next({ name: 'derrick', age: 4 });
});
obs.pipe(groupBy(v => v.name)).subscribe(g => {
  g.subscribe(v => console.log(g.key, v));
});

当 obs 发布新值的时候, groupBy 会依据 key 查看之前是否有创建过 GroupedObservable

如果没有就创建新的, 并且发布下去. 所以 subscribe 接收到的是 GroupedObservable 哦.

如果已经创建过了, 那么它就 .next 把值传下去.

上面的例子中, subscribe 会接收 2 次, 一个是 dada 的 GroupedObservable 另一个是 derrick 的 GroupedObservable

订阅这些 Observable 就可以获取到每次发布的对象了.

我在项目中没有用过 groupBy 一时也想不到什么情况可能会用到它.

 

没有介绍到的 Transformation Operators

expand

mergeScan

 

废弃了的 Transformation Operators

mapTo

switchMapTo

concatMapTo

mergeMapTo

pluck

partition

exhaust

 

一句话总结

map : Array map

scan : Array reduce

pairwise : 每次接收 “一对”, [prev value, curr value]

concatMap : concat(动态 Observables)

mergeMap : merge(动态 Observables)

switchMap : 自动 subscribe map 返回的 Observable, 当有新的 Observable 自动 unsubscribe 上一个 Observable

exhaustMap : switchMap unsubscribe old + subscribe new, exhaustMap subscribe old + skip new (until old complete)

switchScan : switchMap 引入 scan 概念

bufferTime : 缓存值, 到时机一并发送 array

bufferCount : 缓存到量, 一并发送

buffer : 用 notification observable 控制一并发送时机

bufferToggle : 控制开始缓存时机和发送时机

bufferWhen : notification observable 会被 resubscribe, buffer 不会

window operators : 和 buffer 一样, 唯一区别是它不接收 Array 而是 Observable 类似于 from(Array), 还有, 接收的时机不同, buffer 是等 array 满了才接收, window 是一开始就持续接收直到 complete.

groupBy : 把值进行分组, 发布 GroupedObservable

 

posted @ 2022-10-02 16:33  兴杰  阅读(229)  评论(0)    收藏  举报