简体   繁体   中英

RXJS - start a timer only when idle?

I'm using a stream which is throttled when I scroll the window.
While throttling (as long as scrolling), it emits values to the console.

However , when stream is idle (user is not scrolling the window) - I want a timer to kick in. However - if the user starts scrolling again - I don't want that timer to emit values.

Currently I'm doing this :

  const observable = Rx.Observable.fromEvent(window, 'scroll');

  const subscriber = observable
      .throttleTime(300 )
      .map(() => 'throttle')
      .merge(Rx.Observable.interval(1000).map(() => 'tick') )
      .subscribe(
          (x) => {
            console.log('Next: event!', x);
          },
          (err) => {
            console.log('Error: %s', err);
          },
          () => {
            console.log('Completed');
          });

The problem is that , while scrolling - I see both "throttle" AND "tick" ( I should only see "throttle")

Think of this from another POV. A job always has to run. If I scroll - that throttled scroll - should invoke the job. If I don't scroll - a timer should kick in and start doing the job . (and stops if user start scrolling again).

Question:
How can I start a timer after an idle time of not scrolling ?

PLNKR

You can use debounceTime to detect periods without scrolling.

const scroll = Rx.Observable.fromEvent(window, 'scroll')
  .throttleTime(300)
  .mapTo(false);
const noscroll = Rx.Observable.fromEvent(window, 'scroll')
  .startWith(0) // init with no scroll.
  .debounceTime(300) // detect no scroll after 300 ms.
  .mapTo(true);
scroll.merge(noscroll)
  .switchMap(e => e ? Rx.Observable.interval(1000).mapTo("Tick!") : Rx.Observable.of("Scroll!"))  
  // start the interval if there was no scroll. Stop the interval if there was a scroll.
  .subscribe(updateTimer)

Another problem with your code is using merge that will keep both sources subscribed, instead i use switchMap (a sibling of mergeMap ) that will subscribe to the inner observable each time a new event is emitted, but also unsubscribe the previous inner source if another event is emitted from the source.

Re: "another POV" part of the question: You can replace Rx.Observable.interval(1000) in switchMap with the job. Scrolling will cancel/unsubscribe the job (as empty is emitted), if there is no more scrolling, the job will start again.

Live demo

I'd do it like this:

const scroll$ = Rx.Observable.fromEvent(window, 'scroll')
    .throttleTime(300 /* ms */)
    .publish();

scroll$.connect();

const subscriber = scroll$
    .map(() => 'throttle')
    .race(Rx.Observable.interval(1000).map(() => 'tick'))
    .take(1)
    .repeat()
    .subscribe(
        (x) => {
          console.log('Next: event!', x);
        },
        (err) => {
          console.log('Error: %s', err);
        },
        () => {
          console.log('Completed');
        });

This uses the race() operator to subscribe only to the Observable that emits first which is the 1s interval or the scroll event. Right after that I want to start this again with another interval so I use take(1).repeat() .

I also had to turn the scroll$ Observable into a hot Observable to keep the throttleTime() running among the repeated subscriptions.

Your updated demo: https://plnkr.co/edit/sWzSm32uoOQ1hOKigo4s?p=preview

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM