学习rxjs 2.运算符

30 阅读4分钟

运算符是什么

运算符是rxjs的核心能力.它们是一系列函数,可以对流进行转化、时序控制等.

有的运算符接受流作为参数 得到一个新流

另一部分运算符则通过这样的方式进行串联

stream$.pipe(op1(),op2())

这样的操作可以得到一个受运算符控制的新流

常见的运算符

提醒:rxjs的流不只是数据流 可以理解为事件、promise、数组等一系列概念的统一封装 "某个流发出数据"实际可能指的是"某次事件触发" "某个Promise完成"等

数据转化

  • map
stream$.pipe(map((v) => `${v}`))

将每个上游数据映射为新数据

  • scan
stream$
  .pipe(
    scan((result, currentValue, index) => {
      return result + currentValue
    }, 0),
  )

类似数组的reduce 进行值的累加
scan会将每次的result向外发出
如果不提供初始值 则直接从第二个数据开始执行函数 result是第一个数据 index是1

  • pairwise
from([1, 2, 3])
  .pipe(pairwise())

将上一个值和当前值组成数组
订阅这个流会得到[1,2] [2,3].如果数组只有一个元素 则不会得到结果.

  • bufferTime / bufferCount / buffer
    管道运算符 可以缓存上游数据并打包为数组发出
    bufferTime是缓存一定毫秒 bufferCount是缓存一定数量; buffer则接受一个流close$, 每当close$输出一个值 都清空当前缓存

多个流的数据组合

  • combineLatest
combineLatest([a$, b$])

在数组内任一流发出数据后 如果所有流都至少发出过一次数据 将这些流最新数据打包为数组发出

  • zip
zip([a$, b$])

将所有流的第1/2/.../n个数据组合为数组输出.同一数组内 输出的数据索引相同

  • forkJoin 所有流的最后一个值(或者说 在所有流结束后做某些事)
  • withLatestFrom
stream$.pipe(withLatestFrom(a$, b$))

主流stream$输出数据后 如果副流a$ b$都至少输出了一次数据 将主流数据与副流的最新数据打包为数组输出.

如果副流里有一个没输出数据 则此次数据会被忽略 如果需要不错过每次数据 使用combineLatest即可

需要注意的是 withLatestFrom的结束仅与主流有关 而combineLatest则需要接受的所有流都结束

  • race 接受一个流数组.所有流中最先发出数据的会成为唯一的数据源 其余被忽略

流类型的数据

rxjs的流对象也可以作为另一个流传输的数据.

从上游接受流类型的数据,订阅该流并将其中数据依次输出的过程,就叫流的扁平化.

这其实和数组扁平化很像,因为数组的扁平化是将Array<Array<number>>变为Array<number>,而流的扁平化是将Observable<Observable<number>>变为Observable<number>.

注意:扁平化运算符不仅可以接受流 也能接受数组、promise等流式数据源 会被自动转化为流再扁平化

  • concat
concat(a$, b$)

这个运算符会先将a$扁平化(依次输出其中的数据)直至其结束 然后按顺序逐个扁平化后续的流

适用于串行任务 例如多个必须按照顺序执行的网络请求

  • merge
merge(a$, b$)

这个运算符会同时扁平化接受的所有流.即同时订阅参数里的所有流,无论哪个流发出数据都立刻向外输出.

适用于并行任务 例如多个同时上传的文件

  • concatAll / concatMap
    concatAll是管道运算符 可以将上游流类型数据以类似concat的方式扁平化 适用场景也与之类似
declare const doTask: (t: number) => Promise<void>

of(1, 2).pipe(
  map((t) => defer(() => doTask(t))),
  concatAll()
)
// 这里用defer包了一层 以确保当前任务执行时 上一个任务已经执行完毕
// defer也能接受流式数据源

concatMapmapconcatAll的简写 上述示例可以简写为

of(1, 2).pipe(
  concatMap((t) => defer(() => doTask(t)))
)
  • mergeAll / mergeMap / mergeScan
    mergeAllmerge的管道运算符版本 会将上游流数据同时扁平化.
    mergeMapmapmergeAll的简写,mergeScanscanmergeAll的简写.

它们都可以接受一个数字 用于控制最大活跃自流数量(类似于最大并发数)

  • exhaustAll / exhaustMap
    exhaustAll会订阅上游的流类型数据并将其扁平化.在当前订阅的流未完成时 接收到新的上游数据会被忽略.
    适用于旧任务进行时丢弃新任务的场景 例如多次点提交 在第一次提交结束前都不生效
    exhaustMapmapexhaustAll的简写
    exhaust系列没有静态运算符

  • switchAll / switchMap / switchScan
    switchAll会订阅上游的流类型数据.在接受到新的上游数据后,会结束当前流的订阅,改为订阅新的流.
    适用于新任务出现时立刻丢弃旧任务的场景 例如按照搜索框内容进行联想
    switchMap / switchScan是简写
    switch系列没有静态运算符

  • windowTime / windowCount / window
    window系列与buffer类似 也会缓存数据.
    与buffer不同的是 window会将这些数据打包为一个流

  • groupBy

stream$.pipe(groupBy((v) => v)).subscribe((v) => {
  v.subscribe(console.log)
})

可以对上游数据进行分组 分组结果也是一个流.
每当出现一个新的组别,就输出一个新的自流,后续同组的上游数据就不再向外输出,而是输出到对应子流中.子流有一个key属性 可以判断它所属的类别

数据筛选

  • filter
from([1, 2])
  .pipe(filter((v) => v !== 1))

类似数组的filter 按值进行筛选

  • take系列

    • take take(n)取上游n个值后取消订阅
    • takeLast takeLast(n) 上游最后n个数据
    • takeWhile takeWhile(fn) 取值直至fn返回false 然后取消订阅
    • takeUntil takeUntil(close$)取值直到close$输出数据然后取消订阅
  • skip系列 具有与take系列相似的四个函数skipXXX 可以跳过符合条件的若干值

  • first / last

stream$
  .pipe(first())
  
stream$
  .pipe(first(v=>v.id>0))

first的第一个参数不传或者是null/undefined时 它会取流的第一个数据;如果是一个返回布尔值的函数 此时它会取第一个满足条件的数据
第二个参数是defaultValue 如果流结束时没有返回数据 就会返回这个值 若不传就会抛出异常
lastfirst类似

  • elementAt 取流的第n个数据
stream$
  .pipe(elementAt(1)) // 取第一个数据

它的第二个参数也是默认值 用于上游数据不足的情况 无默认值则抛异常

  • distinct / distinctUntilChanged 用于去重.distinct对于整个流的数据去重,distinctUntilChanged则过滤与上个数据相同的数据

  • delay / delayWhen 为每个上游数据延迟一定时间/延迟到指定流发出数据.可以出现后发先至的情况

  • debounceTime / debounce / throttleTime / throttle 针对上游数据的防抖节流.可以选择用时间控制或给出一个流控制

  • auditTime / audit 另一种形式的节流.audit会在窗口结束后发出其中的最后一个数据 而throttle则是发出一个数据后再创建窗口

  • sampleTime / sample 定期/在指定流发出数据后 从主流中发出最新的数据(抽样调查)

错误处理

上游一旦抛出错误 下游便会触发onError 并且订阅会立刻结束.因此map时需要注意可能的异常并将其捕获 并在其后用filter过滤空值.

非map引起的异常可以考虑使用以下的运算符

  • catchError 管道运算符.捕获一个异常 并返回一个备用流
  • onErrorResumeNext
onErrorResumeNext(a$, b$)

一个流报错后 自动切换下一个流

  • retry 错误重试.
stream$.pipe(retry(3)) // 直接指定次数

stream$.pipe({
  delay: () => a$ // a$发出数据后重试
})

stream$.pipe({
  delay: () => EMPTY // 没有数据直接完成的话 直接将整个管道置为已完成 不抛出异常
})

stream$.pipe({
  delay: () => EMPTY // 没有数据直接完成的话 直接将整个管道置为已完成 不抛出异常
})

可以直接指定次数 也可以在 在该流发出数据时进行重试.需要注意的是 重试的是整个管道 就是.pipe之前的那个 会重新订阅

统计类

这类管道运算符会在上游数据结束后发出一个总结性质的数据

  • toArray 所有上游数据的数组
  • count 数据总数
  • reduce 类似数组reduce
  • max / min 字面意思
  • every 类似数组every
  • find / findIndex 字面意思
  • isEmpty 字面意思

其他

  • tap 执行副作用 不改变流数据
  • finalize 流结束或出错后执行函数 流版本的.finally
  • timeout 超时报错
  • timeoutWith 超时替换备用流
  • startWith / endWith
    startWith在上游传输数据之前 先发出指定的一个或多个数据.
    endWith则是在上游结束之后 发出指定的数据.
  • timeInterval 为数据加上与上一次数据的间隔时间
  • timestamp 为数据添加时间戳
  • repeat
stream$.pipe(repeat(3)) // 传入时间

stream$.pipe(repeat({
    count: 1 // 默认是无限
    delay: ()=>a$ // 传入一个流 在其发出数据时重试
}))

重复次订阅整个管道.可以传入次数或者流控制.传入次数表示重复总次数 而不是额外的重复次数.