RxJS 6 页面不活动时暂停或缓冲可观察

RxJS 6 Pause or buffer observable when the page is not active

所以我有一个流,比方说字母,我需要所有字母按正确的顺序将它们组合成一个词。一切正常,直到用户更改选项卡、最小化浏览器或切换应用程序 - 行为几乎与我使用 setTimeout() 相同 - 弄乱订单、丢失物品等。我试图通过使用 bufferWhen()bufferToggle()takeUntil()publish()connect() 但没有成功。我也考虑过使用 delayWhen,但它已被弃用并且可能不适合,因为它会立即停止流。我应该使用哪些功能以及如何使用?这是我的代码:

export class MyComponent implements AfterViewInit {
  private visibilityChange$ = fromEvent(document, 'visibilitychange').pipe(startWith('visible'), shareReplay({ refCount: true, bufferSize: 1 }));
  private show$ = this.visibilityChange$.pipe(filter(() => document.visibilityState === 'visible'));
  private hide$ = this.visibilityChange$.pipe(filter(() => document.visibilityState === 'hidden'));

  public ngAfterViewInit() {
    const lettersStream$ = zip( // add delay for each letter
            from(['w', 'o', 'r', 'd']),
            interval(1000))
           // pause when hide$ fires, resume when show$
          .pipe(map(([letter, delayTime]) => letter))
          .subscribe(console.log);
  }
}

我在stackblitz上做了一个演示 - 我想要的是(在标签不活动时停止写作)如何写在屏幕上。

对用例有点困惑,但这可能会解决它:

首先改为这样做:

private isVisible$ = this.visibilityChange$.pipe(
                       filter(() => document.visibilityState === 'visible'), 
                       distinctUntilChanged()); // just a safety operator

然后这样做:

const lettersStream$ = this.isVisible$.pipe(
        switchMap((isVisible) => (isVisible)
          ? zip( // add delay for each letter
              from(['w', 'o', 'r', 'd']),
              interval(1000))
            .pipe(map(([letter, delayTime]) => letter))
          : NEVER
        )
      ).subscribe(console.log);

每次可见性改变时就切换地图,如果可见则订阅源,如果不可见则什么也不做。

对于这个人为设计的示例,行为会有点不稳定,因为 from() 将始终发出相同的序列,但对于真正的非静态源,它应该按预期工作。

因为我在 RxJS Snake Game 中做过类似的 pause/unpause 事情,我会帮助你举个例子。

想法是有一个 interval(1000) 作为真理的来源,这意味着一切都将基于它。因此,基于我们需要停止在可见性隐藏时发出事件并在可见性显示时继续发出事件这一事实,我们的目标变成了使此间隔可暂停。最后,为了让事情变得更简单,我们可以在可见性隐藏时停止收听源间隔,并在可见性显示到来时再次开始收听。现在让我们来看看具体的实现:

您也可以在 RxJS Pause Observable 上使用修改后的 StackBlitz 演示代码。

import { of, interval, fromEvent, timer, from, zip, never } from 'rxjs';
import { delayWhen, tap, withLatestFrom, concatMap, take, startWith, distinctUntilChanged, switchMap, shareReplay, filter, map, finalize } from 'rxjs/operators';

console.log('-------------------------------------- STARTING ----------------------------')

class MyComponent {
  private visibilityChange$ = fromEvent(document, 'visibilitychange')
    .pipe(
      map(x => document.visibilityState),
      startWith('visible'),
      shareReplay(1)
    );

  private isVisible$ = this.visibilityChange$.pipe(
    map(x => x === 'visible'),
    distinctUntilChanged(),
  );

  constructor() {
    const intervalTime = 1000;
    const source$ = from('word or two'.split(''));
    /** should remove these .pipe(
        concatMap(ch => interval(intervalTime).pipe(map(_ => ch), take(1)))
      );*/

    const pausableInterval$ = this.isVisible$.pipe(
      switchMap(visible => visible ? interval(intervalTime) : never()),
    )

    const lettersStream$ = zip(pausableInterval$, source$).pipe(
      map(([tick, letter]) => letter),
    ).subscribe(letter => {
      this.writeLetter(letter);
    });
  }

  private writeLetter(letter: string) {
    if (letter === ' ') letter = '\u00A0'; // fix for spaces
    document.body.innerText += letter;
  }
}

const component = new MyComponent();

这是来自 StackBlitz 的准确代码,我复制到这里是为了更好地为您解释。

现在让我们为您分解有趣的部分:

  1. 看看 visibilityChange$isVisible$。它们稍作修改,因此第一个发出基于 document.visibilityState 的字符串值 'visible''hidden'。当 document.visibilityState 等于 'visible'.

  2. 时,第二个发出 true
  3. 看看source$。它将发出一个字母,然后在 concatMapintervaltake(1) 的帮助下等待 1 秒,然后执行此过程,直到文本中没有剩余字符。

  4. 看看pausableInterval$。基于将根据 document.visibilityState 变化的 this.isVisible$,我们的 pausableInterval$ 将每秒发射物品或由于 never().[=35= 而根本不会发射任何东西]

  5. 终于看到lettersStream$了。在 zip() 的帮助下,我们将压缩 pausableInterval$source$,因此我们将从源中获得一个字母,从暂停间隔中获得一个刻度。如果 pausableInterval$ 由于可见性变化而停止发射,zip 也会等待,因为它需要两个 Observables 一起发射才能将事件发送到订阅者。