运算符的工作原理
所有运算符 包括静态运算符和管道运算符 都会订阅上游的流 并生成一个新流供下游订阅
它们订阅和取消订阅的行为仅局限于自己的上下游 但通常会影响到整个管道
.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),
)