首先了解 combineLatest 这个操作符的作用:
组合多个 Observable 以创建一个 Observable,其值是根据其每个输入 Observable 的最新值计算得出的。
其弹珠图如下图所示:
我们有一个限制值流和一个偏移值流。 我们使用 combineLatest 组合这些流以创建一个流,该流将在每次源流之一更改时具有一个新值。 然后我们使用 switchMap 根据这些值从后端获取数据以获取 pokemon$。 因为我们使用了switchMap,如果一个调用还没有结束,那么当一个新的调用通过改变limit或者offset来发起一个新的调用时,前一个调用就会被取消。
代码如下:
this.pokemon$ = combineLatest(limit$, offset$)
.pipe(
map(data => ({limit: data[0], offset: data[1]})),
switchMap(data => this.pokemonService.getPokemon(data.limit, data.offset)),
map((response: {results: Pokemon[]}) => response.results),
);
代码地址如下:
https://stackblitz.com/edit/a...
当我修改 limit 和 offset 为其他值之后,点击 reset 按钮,此时会观察到先后发起两个请求,并且第一个请求自动被 cancel 的情况:
通过单击重置按钮,我们通过同时重置限制和偏移值来更新我们的两个源流。 这个动作的效果是 combineLatest 创建的流触发了两次,因此启动了两个后端请求,另一方面,由于我们使用了 switchMap,立即取消了一个。
我们来单步拆解,以加深印象:
- combineLatest 保存所有源流的最后一个值。比如开始场景是,limit = 8,offset = 2)
- 单击重置按钮
- limit 设置为 5
- combineLatest 看到一个新值进入 limit 并发出一个新组合,limit = 5,offset = 2
- switchMap 获取这些值并订阅触发后端调用的流
偏移量设置为 0 - combineLatest 看到一个新的 offset 值,并发出一个新的组合,limit = 5,offset = 0
- switchMap 获取这些值,取消订阅(并因此取消)先前的请求并开始新的请求
在此流程中您可能没有预料到的是,无论何时设置 limit ,此更改都会在更改 offset 之前直接传播到 combineLatest.
如何避免这个行为
如果有一种方法可以确保在同一个调用堆栈中发生的更改(这是单击重置按钮时发生的情况)被丢弃以支持最后一次更改,我们可以解决我们的问题。
这意味着,当 combineLatest 在同一个调用堆栈中发出两个值时,当调用堆栈被清除时,最后一个值将被发送。
为此,我们可以在 combineLatest 之后直接利用值为 0 的 debounceTime。 这将确保只有最后一个值被传递给 switchMap,并且在调用堆栈被清除之后。
每当提到“在同一个调用堆栈中”时,都可以将其替换为“在事件循环的同一轮次中发生的更改”。
加上了 debounceTime(0) 之后的时序图:
- combineLatest 保存所有源流的最后一个值,开始场景是,limit = 8,offset = 2
- 单击重置按钮
- 限制设置为 5
- combineLatest 运算符看到一个新值进入 limit 并发出一个新组合,limit = 5,offset = 2
- debounceTime 运算符看到一个新值,并且(因为操作符的值为 0)将等待直到调用堆栈被清除以将其传递
- 偏移量设置为 0
- combineLatest 运算符看到一个新的 offset 值,并发出一个新的组合,limit = 5,offset = 0
- debounceTime 运算符再次看到一个新值,将丢弃旧值,并等待堆栈被清除以将其传递
- 调用堆栈被清除
- debounceTime 运算符没有看到新的值,将通过组合,limit = 5,offset = 0 向下游发送数据
- switchMap 操作符获取这些值并订阅触发后端调用的流
修复代码非常简单,加上一行代码即可:
debounceTime(0),
修复后的效果,点击 reset 按钮之后,只有一次 HTTP 请求发出了: