学习rxjs 3.部分复杂运算符的详细解释

63 阅读2分钟

运算符的工作原理

所有运算符 包括静态运算符和管道运算符 都会订阅上游的流 并生成一个新流供下游订阅

它们订阅和取消订阅的行为仅局限于自己的上下游 但通常会影响到整个管道

.pipe一般生成的是冷流 多次订阅也会多次执行管道中的内容 并多次订阅源流.这一点可以使用多播运算符进行规避

上游的流一定要及时结束 异步获取数据的可以使用take(1)等

多播运算符

多播运算符会将同一数据流向多个订阅者广播 主要涉及改变冷流的行为 因为冷流每次订阅都会执行构造时的数据 部分场合下这是不可接受的

在管道中 多播运算符不会将下方的订阅再次向上传递 而是在自己订阅源流、维护数据 并将同一份数据发布给所有订阅者

多播操作符的位置影响其功能 它会将来自上游的数据共享 其后面的内容在多次订阅时依旧会执行

  • share 将当前流改为热流.所有订阅共享同一份数据源
  • shareReplay
stream$.pipe(shareReplay()) // 缓存所有历史记录 订阅时立刻播放

stream$.pipe(shareReplay(1)) // 缓存一个

stream$.pipe(shareReplay({
  bufferSize: 1 // 缓存数量 默认无限
  refCount: false // 在没有订阅时 不清空缓存 不取消订阅上游.缓存过多可能导致内存溢出
}))

shareReplay可以用于将某个一次性的网络请求存下来

retry

stream$.pipe(retry(3)) // 直接指定次数

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

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

stream$.pipe({
  delay: () => timer(0) // 立刻重试
})

stream$.pipe({
  delay: (e) => {
    if(e.xxx){
        return a$
    }
    throw e // 有条件的重试 不符合条件直接抛出异常即可 会触发订阅者的error
  }
})

retry的作用是 在接受异常后 重新订阅上游或将异常抛出

take系列

stream$.pipe(take(3), concatMap(async v=>{
    await sleep(1000)
    return v
}))

在take足够数据后 会取消对上游的订阅

concatMap这类的运算符也不会再次执行 即使它可能连第一次都没执行完 因为此时流已经被关闭了.

因此在这样的情形中

stream$.pipe(takeUntil(abort$), concatMap(doSomething))

如果用户点击取消 触发abort$ 后续的doSomething便不会执行(但是已经创建的网络请求不会取消 除非是ajax创建的)

运算符的顺序

stream$.pipe(
    retry(3),
    replayShare(1)
)

一般来说 按照takeUntil 多播运算符的顺序比较合理
在上游出错后 先由retry重试 取得合法的数据 然后将合法数据对外多播

takeUntil相对特殊 它会取消上游订阅和下游的订阅 一般会影响到上下游所有的流 因此一般情况下位置并不关键 但多播运算符是比较特殊的 在自己的下游取消订阅后 多播运算符可以选择不取消自身上游的订阅 因此需要按照需求放置二者的位置

一般情况下 retry->takeUntil->多播运算符的顺序是比较合理的

withLatestFrom 和 combineLatest

stream$.pipe(withLatestFrom(a$))

每当stream$发出数据 如果a$已有数据 将其组合 否则忽略这次数据
这里的重点在于withLatestFrom是不会等待a$发出数据的
也就是说这样写一定是错误的

const p$ = from(Promise.resolve(1))
const v$ = of(1)
v$.pipe(withLatestFrom(p$)).subscribe(console.log) // 一定不会有数据

尤其是把异步函数(例如网络请求)转化为流的情形 通常会遇到这种问题 改为combineLatest即可

但是需要注意的是combineLatest接受的流中 任何一个发出了数据 combineLatest也会发出数据;所有流结束后 combineLatest才能结束.这与withLatestFrom是差别极大的 应用场景也不同

如果既需要一个流立刻产出数据 又需要其不干扰主流 可以用startWith赋一个默认值 也可以自定义Observable对象

catchError

catchError只接受来自上游的错误 提供给它的备用流也仅仅是输送到下游而已 不会对管道本身造成影响
如果想抛出异常 直接throw出去即可

流未完成的统计

对于数据类型的流的统计

统计是否空

stream$.pipe(take(1), isEmpty())

统计当前的所有数据

const currentSum$ = stream$.pipe(
  scan((result, current) => result + current, 0),
  shareReplay(1),
)