我是响应式编程的新手,很好奇是否有更优雅的方式来实现去抖动的分块异步队列。你问什么是去抖动分块异步队列?好吧,也许它有一个更好的名字,但这个想法是在一段时间内可以多次调用异步函数。我们首先要消除对函数的调用,并将所有这些参数合并为一个参数。其次,该异步函数的所有执行都应以 FIFO 方式进行序列化。
这是我的实现,没有流、可观察或响应式编程,正如我所见:
const debouncedChunkedQueue = <T>(
fn: (items: T[]) => Promise<void> | void,
delay = 1000
) => {
let items: T[] = [];
let started = false;
const start = async () => {
started = true;
while (items.length) {
await sleep(delay);
const chunk = items.splice(0, items.length);
await fn(chunk);
}
started = false;
};
const push = (item: T) => {
items.push(item);
if (!started) start();
};
return { push };
};
https://codesandbox.io/s/priceless-sanne-dkrkw?file=/src/index.ts:87-550
答案 0 :(得分:2)
我以 1:1 的方式遵循了您当前的实现,这导致代码比使用简单的 bufferTime
实现的代码更复杂。
您可以在 RxViz 中测试以下代码。
const { BehaviorSubject, fromEvent, of } = Rx;
const { buffer, delay, first, tap, mergeMap, exhaustMap } = RxOperators;
// Source of the queue - click to emit event
const items = fromEvent(document, 'click');
// a "pushback" source
const processing = new BehaviorSubject(false);
items.pipe(
// we resubscribe to items to start only when an item is there
buffer(
// we take items again so we only
// start if there are any items
// to be processed and then we wait
// 1 second
items.pipe(
// we do not start processing until
// pending async call completes
exhaustMap(() =>
processing.pipe(
// wait for processing end
first(val => !val),
// wait for 1 second
delay(1000),
),
),
),
),
tap(() => processing.next(true)),
// basic mergeMap is okay, since we control that the next value
// will come no sooner than this is completed
mergeMap(items =>
// async fn simulation
of(items.length).pipe(delay(300 + Math.random() * 500)),
),
tap(() => processing.next(false)),
);
不过,我们必须将状态保持在可观察范围之外,这真是太可惜了。
您可以在stackblitz
中测试以下代码import { BehaviorSubject, Subject, of } from "rxjs";
import {
buffer,
delay,
first,
tap,
mergeMap,
exhaustMap
} from "rxjs/operators";
const debouncedChunkedQueue = <T>(
fn: (items: T[]) => Promise<void> | void,
delayMs = 1000
) => {
// Source of the queue - click to emit event
const items = new Subject<T>();
// a "pushback" source
const processing = new BehaviorSubject(false);
items
.pipe(
// we resubscribe to items to start only when an item is there
buffer(
// we take items again so we only
// start if there are any items
// to be processed and then we wait
// 1 second
items.pipe(
// we do not start processing until
// pending async call completes
exhaustMap(() =>
processing.pipe(
// wait for processing end
first(val => !val),
// wait for 1 second
delay(delayMs)
)
)
)
),
tap(() => processing.next(true)),
// basic mergeMap is okay, since we control that the next value
// will come no sooner than this is completed
mergeMap(
items =>
// TODO: Make sure to catch errors from the fn if you want the queue to recover
// async fn simulation
fn(items) || of(null)
),
tap(() => processing.next(false))
)
.subscribe();
return { push: (item: T) => items.next(item) };
};
答案 1 :(得分:2)
所以,我制作了一个可观察的怪物。
它比您的实现复杂得多,但在防御方面,它也更加强大。
这是一个自定义的 RxJS 运算符,应该可以与 RxJS 库的其余部分无缝协作。例如,如果您正在运行 HTTP 调用并收到错误,则可以使用 retry
、retryWhen
、catchError
等来处理错误并响应每个调用或整个调用操作员。
您可以在此操作符之前和之后链接其他操作符(以及其他可观察对象),以在数据使用之前或返回之后对其进行操作。
如果你决定提前取消你的函数调用,你可以只是错误源或取消订阅可观察对象(就像任何其他操作符一样)。如果可能,它会自行清理并取消任何中途操作(Vanilla Promises 无法取消)。
无论如何,是的,如果您只是为返回 void 的异步函数缓冲值,那么它是一个庞然大物,而且可能有点矫枉过正。
这是:
bufferedExhaustMap
运算符此运算符根据最小缓冲区长度(以毫秒为单位的时间)和最小缓冲区大小来缓冲值。无论是否满足最小时间和缓冲区大小,它始终会耗尽当前预计的 observable。
这与exhaustMap非常相似,除了它在等待当前内部(投影)可观察对象完成时缓冲值而不是丢弃值。
/***
* ObservableInput works with an Array, an array-like
* object, a Promise, an iterable object, or an Observable-like object.
*/
function bufferedExhaustMap<T,R>(
project: (v:T[]) => ObservableInput<R>,
minBufferLength = 0,
minBufferCount = 1
): OperatorFunction<T,R> {
return s => new Observable(observer => {
const idle = new BehaviorSubject<boolean>(true);
const setIdle = (b) => () => idle.next(b);
const buffer = (() => {
const bufS = new Subject<(v:T[]) => T[]>();
return {
output: bufS.pipe(
scan((acc, curr) => curr(acc), [])
),
nextVal: (v:T) => bufS.next(curr => [...curr, v]),
clear: () => bufS.next(_ => [])
}
})();
const subProject = combineLatest(
idle,
idle.pipe(
filter(v => !v),
startWith(true),
switchMap(_ => timer(minBufferLength).pipe(
mapTo(true),
startWith(false)
))
),
buffer.output
).pipe(
filter(([idle, bufferedByTime, buffer]) =>
idle && bufferedByTime && buffer.length >= minBufferCount
),
tap(setIdle(false)),
tap(buffer.clear),
map(x => x[2]),
map(project),
mergeMap(projected => from(projected).pipe(
finalize(setIdle(true))
))
).subscribe(observer);
const subSource = s.subscribe({
next: buffer.nextVal,
complete: observer.complete.bind(observer),
error: e => {
subProject.unsubscribe();
observer.error(e);
}
});
return {
unsubscribe: () => {
subProject.unsubscribe();
subSource.unsubscribe();
}
}
});
}
debouncedChunkedQueue
这是我将如何使用我构建的运算符实现您的功能。
function debouncedChunkedQueue<T>(
fn: (items: T[]) => Promise<void>,
delay = 1000
){
const processSubject = new Subject<T>();
processSubject.pipe(
bufferedExhaustMap(fn, delay)
).subscribe();
return {
push : processSubject.next.bind(processSubject)
}
}
虽然这确实抽象了 RxJS。相反,我会更倾向于使用反应式风格,并按原样使用运算符。无论你的承诺对你的程序状态做了什么(设置值、转换对象等),让它返回该信息并相应地反应。
如果这对您来说听起来很痛苦,那么您最好不要使用这种方法。那也不错!各有各的。 :)
bufferedExhaustMap
受到这里的另一个答案(由 kvetis 提供)的启发,我使用 bufferWhen
重新实现了这个运算符,并将其扩展为包含最小缓冲区大小。
这也会取消订阅源,而不是在缓冲时通过 exhaustMap
忽略源。这应该(理论上)更快,但我不确定它在实践中是否重要。
/***
* Buffers, then projects buffered source values to an Observable which is merged in
* the output Observable only if the previous projected Observable has completed.
***/
function bufferedExhaustMap<T,R>(
project: (v:T[]) => ObservableInput<R>,
minBufferLength = 0,
minBufferCount = 1
): OperatorFunction<T,R> {
return s => defer(() => {
const idle = new BehaviorSubject(true);
const setIdle = (b) => () => idle.next(b);
const shared = s.pipe(share());
const nextBufferTime = () => forkJoin(
shared.pipe(take(minBufferCount)),
timer(minBufferLength),
idle.pipe(first(v => v))
).pipe(
tap(setIdle(false))
);
return shared.pipe(
bufferWhen(nextBufferTime),
map(project),
mergeMap(projected => from(projected).pipe(
finalize(setIdle(true))
))
);
});
}
也许最复杂的位是 nextBufferTime
工厂函数。它创建了一个 observable,当缓冲区应该被清除时发出,然后完成。
它通过合并 3 个 observable 并仅在所有三个 observable 完成后才完成。这样,内部 observable 完成算作满足条件。
minBufferCount
个值。minBufferLength
的计时器已过。idle == true
)。const nextBufferTime = () => forkJoin(
// Take minBufferCount from source then complete
shared.pipe(take(minBufferCount)),
// Wait minBufferLength milliseconds and then complete
timer(minBufferLength),
// Wait until idle emits true, then complete
idle.pipe(first(v => v))
)
答案 2 :(得分:1)
我们希望首先消除对函数的调用,并将所有这些参数合并为一个参数。其次,该异步函数的所有执行都应以 FIFO 方式进行序列化。
我认为您已经很好地描述了如何使用流来编写它。
根据这句话,我相信你可以写一个自定义操作符,如下所示:
export function debouncedChunkedQueue<T>(
fn: (items: T[]) => Observable<void>,
delay = 1000
) {
return (obs$: Observable<T>) =>
obs$.pipe(
bufferTime(delay),
map(items => fn(items) || EMPTY),
concatAll()
);
}
为了演示这应该如何工作,我做了以下工作:
function mockApiCall<T>(items: T[]): Observable<void> {
console.log(`mock API call for the items`, items);
return of(undefined).pipe(delay(500));
}
interval(100)
.pipe(debouncedChunkedQueue(mockApiCall))
.subscribe(console.log);
输出(这里的前 3 个结果)如下:
mock API call for the items
[0, 1, 2, 3, 4, 5, 6, 7, 8]
mock API call for the items
[9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
mock API call for the items
[19, 20, 21, 22, 23, 24, 25, 26, 27, 28]
这是一个现场演示:
https://stackblitz.com/edit/rxjs-qweh1g?devtoolsheight=60
顺便解释一下:
bufferTime
允许您在指定时间内将数据累积到数组中map
创建一个 cold observable,一旦我们订阅它就会调用回调。所以基本上我们最终得到一个 observable 的 observableconcatAll
让我们通过订阅一个 observable 来打开 observable 的 observable 以确保它们按顺序运行