如何确定在去抖时间内发出的值的数量?

How can I determine the number of values has been emitted during the debounce time?

给定:

处理“放大”显示请求的 NgRx 效果。每次用户单击适当的按钮时它都会收到通知。

public readonly zoomIn$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(zoomIn),
        tap(() => {
          scale();
        }),
      ),
    { dispatch: false },
  );

注意:zoomIn 操作不能也不包含任何负载。仅将其视为触发器

问题:

重新绘制会消耗资源,在某些情况下会占用几秒钟才能获得新比例。所以如果你想连续放大几次,你将不得不等待。

解法:

通过使用 debounceTime 运算符推迟 scale() 函数的调用并等待用户多次点击。听起来不错。唯一的问题是 debounceTime 通知我们最新的值。我们需要的是一些被 debounceTime 运算符静音的值(用户的点击)。

从更一般的角度来看,任务听起来像:如何计算源流发出并被 debounceTime 运算符静音的值的计数?

我的解决方案 是创建一个实现目标的自定义 pipable 运算符。

我们需要什么?当然,我们需要一个debounceTime运算符。

.pipe(
  debounceTime(300),
)

然后我们应该计算已经发出的值的数量。有一个 scan 运算符,它看起来很像 well-known reduce 函数。我们将给它一个初始值,并将在每个从源流接收到的值上增加一个计数器。我们必须将它放在 debounceTime 运算符之前。它现在看起来像一个索引流。

.pipe(
  scan(acc => acc + 1, 0),
  debounceTime(300),
)

debounceTime通知我们最新索引时,我们如何知道muted values的个数?我们必须将它与之前发出的索引进行比较。可以使用 pairwise 运算符接收先前的值。然后使用 map 运算符来区分它们。

.pipe(
  scan(acc => acc + 1, 0),
  debounceTime(300),
  pairwise(),
  map(([previous, current]) => current - previous),
)

如果您在当前状态下尝试此操作,您会发现有些地方不对劲,这是第一次无法正常工作。问题在于 pairwise 运算符。它会发出成对的值(先前的和当前的),因此它会等到至少有两个值才开始发出成对的值。公平吗?是的?这就是为什么我们需要稍微作弊并使用 startWith 运算符提供第一个值(即 0)。

最终实现

/**
 * Emits a notification from the source Observable only after a particular time span has passed without another source emission,
 * with an exact number of values were emitted during that time.
 *
 * @param dueTime the timeout duration in milliseconds for the window of time required to wait for emission silence before emitting the most recent source value.
 * @returns MonoTypeOperatorFunction
 */
export const debounceCounter =
  (dueTime: number): MonoTypeOperatorFunction<number> =>
  (source: Observable<unknown>): Observable<number> =>
    new Observable(observer =>
      source
        .pipe(
          scan(acc => acc + 1, 0),
          debounceTime(dueTime),
          startWith(0),
          pairwise(),
          map(([previous, current]) => current - previous),
        )
        .subscribe({
          next: x => {
            observer.next(x);
          },
          error: err => {
            observer.error(err);
          },
          complete: () => {
            observer.complete();
          },
        }),
    );

用法示例

public readonly zoomIn$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(zoomIn),
        debounceCounter(300),
        tap(times => {
          // scale n-times
        }),
      ),
    { dispatch: false },
  );