RxJS mergeMap() with original order
有什么方法可以按照外部 observable 的原始顺序消耗
更详细的解释
让我们看看两个合并映射运算符:
-
mergeMap ...它接受一个映射回调,以及可以同时运行多少个内部可观察对象:
1
2
3of(1, 2, 3, 4, 5, 6).pipe(
mergeMap(number => api.get('/double', { number }), 3)
);在此处查看实际操作:https://codepen.io/JosephSilber/pen/YzwVYNb?editors=1010
这将分别触发
1 、2 和3 的 3 个并行请求。一旦其中一个请求完成,它将触发另一个对4 的请求。以此类推,始终保持 3 个并发请求,直到处理完所有值。但是,由于先前的请求可能在后续请求之前完成,因此生成的值可能会乱序。所以代替:
1[2, 4, 6, 8, 10, 12]...我们实际上可能得??到:
1[4, 2, 8, 10, 6, 12] // or any other permutation -
concatMap ...输入
concatMap 。此运算符确保所有可观察对象都按原始顺序连接,因此:1
2
3of(1, 2, 3, 4, 5, 6).pipe(
concatMap(number => api.get('/double', { number }))
);...总是会产生:
1[2, 4, 6, 8, 10, 12]在此处查看实际操作:https://codepen.io/JosephSilber/pen/OJMmzpy?editors=1010
这是我们想要的,但现在请求不会并行运行。正如文档所说:
concatMap is equivalent tomergeMap withconcurrency parameter set to1 .
所以回到问题:是否有可能获得
我的具体问题
上面对这个问题进行了抽象的描述。当您知道手头的实际问题时,有时会更容易推断问题,所以这里是:
我有一份需要发货的订单清单:
1 | const orderNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; |
我有一个实际发送订单的
1 | const shipOrder = orderNumber => api.shipOrder(orderNumber); |
API 最多只能同时处理 5 个订单发货,所以我使用
1 2 3 | from(orderNumbers).pipe( mergeMap(orderNumber => shipOrder(orderNumber), 5) ); |
订单发货后,我们需要打印其发货标签。我有一个
1 2 3 | from(orderNumbers) .pipe(mergeMap(orderNumber => shipOrder(orderNumber), 5)) .pipe(orderNumber => printShippingLabel(orderNumber)); |
这可行,但现在运输标签打印乱序,因为
这可能吗?
可视化
有关问题的可视化,请参见此处:https://codepen.io/JosephSilber/pen/YzwVYZb?editors=1010
您可以看到,较早的订单在发货之前就已打印出来。
我确实设法部分解决了它,所以我将它发布在这里作为我自己问题的答案。
我还是很想知道处理这种情况的规范方法。
一个复杂的解决方案
创建一个自定义运算符,该运算符接受具有索引键的值(Typescript 用语中的
将原始列表映射为嵌入了
将其传递给我们的自定义
将这些值映射回它们的原始值。
这就是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | function sortByIndex() { return observable => { return Observable.create(subscriber => { const buffer = new Map(); let current = 0; return observable.subscribe({ next: value => { if (current != value.index) { buffer.set(value.index, value); } else { subscriber.next(value); while (buffer.has(++current)) { subscriber.next(buffer.get(current)); buffer.delete(current); } } }, complete: value => subscriber.complete(), }); }); }; } |
使用
1 2 3 4 5 6 7 8 9 | of(1, 2, 3, 4, 5, 6).pipe( map((number, index) => ({ number, index })), mergeMap(async ({ number, index }) => { const doubled = await api.get('/double', { number }); return { index, number: doubled }; }, 3), sortByIndex(), map(({ number }) => number) ); |
在此处查看实际操作:https://codepen.io/JosephSilber/pen/zYrwpNj?editors=1010
创建
事实上,有了这个
1 2 3 4 5 6 7 8 9 10 11 12 13 | function concurrentConcat(mapper, parallel) { return observable => { return observable.pipe( mergeMap( mapper, (_, value, index) => ({ value, index }), parallel ), sortByIndex(), map(({ value }) => value) ); }; } |
然后我们可以使用这个
1 2 3 | of(1, 2, 3, 4, 5, 6).pipe( concurrentConcat(number => api.get('/double', { number }), 3), ); |
在此处查看实际操作:https://codepen.io/JosephSilber/pen/pogPpRP?editors=1010
所以要解决我原来的订单发货问题:
1 2 3 | from(orderNumbers) .pipe(concurrentConcat(orderNumber => shipOrder(orderNumber), maxConcurrent)) .subscribe(orderNumber => printShippingLabel(orderNumber)); |
在此处查看实际操作:https://codepen.io/JosephSilber/pen/rNxmpWp?editors=1010
您可以看到,即使后来的订单可能会在较早的订单之前发货,但标签始终按原始顺序打印。
结论
这个解决方案甚至不完整(因为它不处理发出多个值的内部可观察对象),但它需要一堆自定义代码。这是一个常见的问题,我觉得必须有一种更简单(内置)的方法来解决这个问题:|
你可以使用这个操作符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | const DONE = Symbol("DONE"); const DONE$ = of(DONE); const sortedMergeMap = <I, O>( mapper: (i: I) => ObservableInput<O>, concurrent = 1 ) => (source$: Observable) => source$.pipe( mergeMap( (value, idx) => concat(mapper(value), DONE$).pipe(map(x => [x, idx] as const)), concurrent ), scan( (acc, [value, idx]) => { if (idx === acc.currentIdx) { if (value === DONE) { let currentIdx = idx; const valuesToEmit = []; do { currentIdx++; const nextValues = acc.buffer.get(currentIdx); if (!nextValues) { break; } valuesToEmit.push(...nextValues); acc.buffer.delete(currentIdx); } while (valuesToEmit[valuesToEmit.length - 1] === DONE); return { ...acc, currentIdx, valuesToEmit: valuesToEmit.filter(x => x !== DONE) as O[] }; } else { return { ...acc, valuesToEmit: [value] }; } } else { if (!acc.buffer.has(idx)) { acc.buffer.set(idx, []); } acc.buffer.get(idx)!.push(value); if (acc.valuesToEmit.length > 0) { acc.valuesToEmit = []; } return acc; } }, { currentIdx: 0, valuesToEmit: [] as O[], buffer: new Map<number, (O | typeof DONE)[]>([[0, []]]) } ), mergeMap(scannedValues => scannedValues.valuesToEmit) ); |
你想要的是这样的:
1 2 3 | from(orderNumbers) .pipe(map(shipOrder), concatAll()) .subscribe(printShippingLabel) |
解释:
管道中的第一个运算符是map。它立即为每个值调用 shipOrder(因此后续值可能会启动并行请求)。
第二个运算符 concatAll 将解析后的值按正确的顺序排列。
(我简化了代码;concatAll() 等价于 concatMap(identity)。)