可暂停的RxJS流

时间:2019-06-25 05:53:56

标签: javascript angular rxjs

我有一个带有一个按钮的简单组件,该按钮可以启动和暂停RxJS计时器生成的数字流。

import { Component, OnInit } from '@angular/core';
import { BehaviorSubject, Observable, timer, merge } from 'rxjs';
import { filter, bufferToggle, windowToggle, mergeMap, mergeAll, share } from 'rxjs/operators';

@Component({
  selector: 'my-app',
  template: `<button (click)="toggle()">{{ (active$ | async) ? 'Pause' : 'Play' }}</button>`,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
  active$ = new BehaviorSubject<boolean>(true);

  ngOnInit(): void {
    const on$ = this.active$.pipe(filter(v => v));
    const off$ = this.active$.pipe(filter(v => !v));

    const stream$ = timer(500, 500).pipe(share());

    const out$ = merge(
      stream$.pipe(
        bufferToggle(off$, () => on$),
        mergeAll(),
      ),
      stream$.pipe(
        windowToggle(on$, () => off$),
        mergeAll(),
      ),
    );

    out$.subscribe(v => console.log(v));
  }

  toggle(): void {
    this.active$.next(!this.active$.value);
  }
}

这很好用。 enter image description here

我需要再添加一个功能。我需要使用基于流中值的条件自动暂停流(this.active$.next(false))。例如,如果最新值为5的倍数,则暂停流。

您对此有任何想法吗?

这是stackblitz https://stackblitz.com/edit/angular-6hjznn

上的一个可运行示例

6 个答案:

答案 0 :(得分:10)

扩展bufferToggle / windowToggle方法

您可以在bufferToggle之后将自定义缓冲区(数组)添加到操作员队列。当bufferToggle发出时,将这些值附加到您的自定义缓冲区。然后从自定义缓冲区中获取值,直到缓冲区中的某个元素与暂停条件匹配为止。发射这些值并暂停流。

可操作的运算符

export function pausable<T, O>(
  on$: Observable<any>, // when on$ emits 'pausable' will emit values from the buffer and all incoming values 
  off$: Observable<O>, // when off$ emits 'pausable' will stop emitting and buffer incoming values
  haltCondition: (value: T) => boolean, // if 'haltCondition' returns true for a value in the stream the stream will be paused
  pause: () => void, // pauses the stream by triggering the given on$ and off$ observables
  spread: boolean = true // if true values from the buffer will emitted separately, if 'false' values from the buffer will be emitted in an array
) {
  return (source: Observable<T>) => defer(() => { // defer is used so that each subscription gets its own buffer
    let buffer: T[] = [];
    return merge(
      source.pipe(
        bufferToggle(off$, () => on$),
        tap(values => buffer = buffer.concat(values)), // append values to your custom buffer
        map(_ => buffer.findIndex(haltCondition)), // find the index of the first element that matches the halt condition
        tap(haltIndex => haltIndex >= 0 ? pause() : null), // pause the stream when a value matching the halt condition was found
        map(haltIndex => buffer.splice(0, haltIndex === -1 ? customBuffer.length : haltIndex + 1)), // get all values from your custom buffer until a haltCondition is met
        mergeMap(toEmit => spread ? from(toEmit) : toEmit.length > 0 ? of(toEmit) : EMPTY) // optional value spread (what your mergeAll did)
      ),
      source.pipe(
        windowToggle(on$, () => off$),
        mergeMap(x => x),
        tap(value => haltCondition(value) ? pause() : null), // pause the stream when an unbuffered value matches the halt condition
      ),
    );
  });
}

用法

ngOnInit(): void {
  this.active$ = new BehaviorSubject<boolean>(true);
  const on$ = this.active$.pipe(filter(v => v));
  const off$ = this.active$.pipe(filter(v => !v));
  timer(500, 500).pipe(
    share(),
    pausable(on$, off$, v => this.active$.value && this.pauseOn(v), () => this.active$.next(false), false)
  ).subscribe(console.log);
}

pauseOn(value: number): boolean { return value % 10 === 0; }

https://stackblitz.com/edit/angular-vq7xsh

pausable运算符将发出与暂停条件匹配的值,然后此后直接停止流。可以根据您的特定需求进行调整,例如只需使用较少的输入参数进行简化,或者将share合并到pausable中。


完全自定义的缓冲区

考虑到上面的pausable运算符使用了很多输入参数,我实现了第二个运算符,它使用完全自定义的缓冲区,并且只有一个可观察到的输入可以打开或关闭缓冲区,类似于Brandon's approach

bufferIf运算符

bufferIf将在给定condition发出true时缓冲传入的值,并在condition为{{1}时从缓冲区中发出所有值或传递新值}。

false

用法

export function bufferIf<T>(condition: Observable<boolean>) {
  return (source: Observable<T>) => defer(() => {
    const buffer: T[] = [];
    let paused = false;
    let sourceTerminated = false;
    return merge( // add a custon streamId to values from the source and the condition so that they can be differentiated later on
      source.pipe(map(v => [v, 0]), finalize(() => sourceTerminated = true)),
      condition.pipe(map(v => [v, 1]))
    ).pipe( // add values from the source to the buffer or set the paused variable
      tap(([value, streamId]) => streamId === 0 ? buffer.push(value as T) : paused = value as boolean), 
      switchMap(_ => new Observable<T>(s => {
        setTimeout(() => { // map to a stream of values taken from the buffer, setTimeout is used so that a subscriber to the condition outside of this function gets the values in the correct order (also see Brandons answer & comments)
          while (buffer.length > 0 && !paused) s.next(buffer.shift())
        }, 0)
      })), // complete the stream when the source terminated and the buffer is empty
      takeWhile(_ => !sourceTerminated || buffer.length > 0, true) 
    );
  })
} 

https://stackblitz.com/edit/angular-v5ruib

答案 1 :(得分:7)

这是一个自定义的暂停运算符,它将在暂停信号为true时在缓冲区中累积值,并在false时逐个发出它们。

使用简单的tap运算符将其组合起来,以便在该值达到特定条件时切换行为主体暂停信号,并且您有一些东西会在按钮单击时暂停,并且在该值满足条件时也会暂停(多个在这种情况下为12):

这里是pause运算符:

function pause<T>(pauseSignal: Observable<boolean>) {
  return (source: Observable<T>) => Observable.create(observer => {
    const buffer = [];
    let paused = false;
    let error;
    let isComplete = false;

    function notify() {
      while (!paused && buffer.length) {
        const value = buffer.shift();
        observer.next(value);
      }

      if (!buffer.length && error) {
        observer.error(error);
      }

      if (!buffer.length && isComplete) {
        observer.complete();
      }
    }

    const subscription = pauseSignal.subscribe(
      p => {
        paused = !p;
        setTimeout(notify, 0);
      },
      e => {
        error = e;
        setTimeout(notify, 0);
      },
      () => {});

    subscription.add(source.subscribe(
      v => {
        buffer.push(v);
        notify();
      },
      e => {
        error = e;
        notify();
      },
      () => {
        isComplete = true;
        notify();
      }
    ));

    return subscription;
  });
}

这是它的用法:

const CONDITION = x => (x > 0) && ((x % 12) === 0); // is multiple
this.active$ = new BehaviorSubject<boolean>(true);
const stream$ = timer(500, 500);
const out$ = stream$.pipe(
  pause(this.active$),
  tap(value => {
    if (CONDITION(value)) {
      this.active$.next(false);
    }
  }));

this.d = out$.subscribe(v => console.log(v));

还有一个可行的示例:https://stackblitz.com/edit/angular-bvxnbf

答案 2 :(得分:3)

这是一种简单的方法。将timer()用作发射器,并分别增加计数。这使您可以直接进行控制。

export class AppComponent implements OnInit {
  active = true;
  out$: Observable<number>;

  count = 0;

  ngOnInit(): void {

    const stream$ = timer(500, 500);

    this.out$ = stream$.pipe(
      filter(v => this.active),
      map(v => {
        this.count += 1;
        return this.count;
      }),
      tap(v => {
        if (this.count % 5 === 0) {
          this.active = false;
        }
      })
    )
  }

}

https://github.com/pennersr/django-allauth/pull/1410/files

答案 3 :(得分:3)

我假设所需的行为与获取计时器本身发出的值无关,并且不是暂停对正在进行的流的通知(在您的示例中,即使我们没有看到计时器也继续运行)值),暂停时实际上可以停止发射。

我的解决方案受到Stopwatch recipe

的启发

下面的解决方案使用两个单独的按钮进行播放和暂停,但是您可以根据需要进行调整。我们将(ViewChild)按钮传递到组件的ngAfterViewInit钩子中的服务,然后订阅流。

// pausable.component.ts
  ngAfterViewInit() {
    this.pausableService.initPausableStream(this.start.nativeElement, this.pause.nativeElement);

    this.pausableService.counter$
      .pipe(takeUntil(this.unsubscribe$)) // don't forget to unsubscribe :)
      .subscribe((state: State) => {
        console.log(state.value); // whatever you need
    });
  }
// pausable.service.ts
import { Injectable } from '@angular/core';

import { merge, fromEvent, Subject, interval, NEVER } from 'rxjs';
import { mapTo, startWith, scan, switchMap, tap, map } from 'rxjs/operators';

export interface State {
  active: boolean;
  value: number;
}

@Injectable({
  providedIn: 'root'
})
export class PausableService {

  public counter$;

  constructor() { }

  initPausableStream(start: HTMLElement, pause: HTMLElement) {

    // convenience functions to map an element click to a result
    const fromClick = (el: HTMLElement) => fromEvent(el, 'click');
    const clickMapTo = (el: HTMLElement, obj: {}) => fromClick(el).pipe(mapTo(obj));

    const pauseByCondition$ = new Subject();
    const pauseCondition = (state: State): boolean => state.value % 5 === 0 && state.value !== 0;

    // define the events that may trigger a change
    const events$ = merge(
      clickMapTo(start, { active: true }),
      clickMapTo(pause, { active: false }),
      pauseByCondition$.pipe(mapTo({ active: false }))
    );

    // switch the counter stream based on events
    this.counter$ = events$.pipe(
      startWith({ active: true, value: 0 }),
      scan((state: State, curr) => ({ ...state, ...curr }), {}),
      switchMap((state: State) => state.active
        ? interval(500).pipe(
          tap(_ => ++state.value),
          map(_ => state))
        : NEVER),
      tap((state: State) => {
        if (pauseCondition(state)) {
          pauseByCondition$.next(); // trigger pause
        }
      })
    );
  }

}

答案 4 :(得分:3)

使用一个windowToggle并使用active.next(false)就可以这么简单 工作示例:https://stackblitz.com/edit/angular-pdw7kw

 defer(() => {
      let count = 0;
      return stream$.pipe(
        windowToggle(on$, () => off$),
        exhaustMap(obs => obs),
        mergeMap(_ => {
          if ((++count) % 5 === 0) {
            this.active$.next(false)
            return never()
          }
          return of(count)
        }),
      )
    }).subscribe(console.log)

答案 5 :(得分:0)

您的示例实际上非常接近正在解决的解决方案,不需要新的自定义运算符。

请参阅此处的“缓冲”部分:

https://medium.com/@kddsky/pauseable-observables-in-rxjs-58ce2b8c7dfd

这里是工作示例:

https://thinkrx.io/gist/cef1572743cbf3f46105ec2ba56228cd

它使用与bufferTogglewindowToggle相同的方法,看起来主要区别在于您需要share暂停/活动主题-