我有一个带有一个按钮的简单组件,该按钮可以启动和暂停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);
}
}
我需要再添加一个功能。我需要使用基于流中值的条件自动暂停流(this.active$.next(false)
)。例如,如果最新值为5的倍数,则暂停流。
您对此有任何想法吗?
这是stackblitz https://stackblitz.com/edit/angular-6hjznn
上的一个可运行示例答案 0 :(得分:10)
您可以在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
将在给定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)
);
})
}
答案 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));
答案 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;
}
})
)
}
}
答案 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
它使用与bufferToggle
和windowToggle
相同的方法,看起来主要区别在于您需要share
暂停/活动主题-