问题
I am working on an Angular 9 quiz app and I'm using RxJS for the countdown timer (in containers\scoreboard\time\time.component.ts) and the timer doesn't seem to be displaying. The stopTimer() function should stop the timer on the number of seconds on which it is stopped. The timer should stop after the correct answer(s) are selected and the timer should reset in between questions. The time elapsed per question should be saved into the elapsedTimes array. Please see my code for the timer in the TimeComponent on Stackblitz: https://stackblitz.com/edit/angular-9-quiz-app. Thank you.
The code below is my first start with building a countdown clock with RxJS. My most recent code is on Stackblitz.
countdownClock() {
this.timer = interval(1000)
.pipe(
takeUntil(this.isPause),
takeUntil(this.isStop)
);
this.timerObserver = {
next: (_: number) => {
this.timePerQuestion -= 1;
...
}
};
this.timer.subscribe(this.timerObserver);
}
goOn() {
this.timer.subscribe(this.timerObserver);
}
pauseTimer() {
this.isPause.next();
// setTimeout(() => this.goOn(), 1000)
}
stopTimer() {
this.timePerQuestion = 0;
this.isStop.next();
}
I use stopTimer() and pauseTimer() in my TimerService so I can call them from a different component.
回答1:
i have fixed your stackblitz. Based on your call of timerservice methods, your timer will behave
And no need to set elapsed time in teardown logic. it will automatically push whenver timer will stop.
Stackblitz Url :- https://stackblitz.com/edit/angular-9-quiz-app-er3pjn
Time countdown method :-
countdownClock() {
const $ = document.querySelector.bind(document);
const start$ = this.timerService.isStart.asObservable().pipe(shareReplay(1));
const reset$ = this.timerService.isReset.asObservable();
const stop$ = this.timerService.isStop.asObservable();
const markTimestamp$ = fromEvent($("#mark"), "click");
const continueFromLastTimestamp$ = fromEvent($("#continue"), "click");
start$.subscribe((data) => console.log(data));
this.timeLeft$ = concat(start$.pipe(first()), reset$).pipe(
switchMapTo(
timer(0, 1000).pipe(
scan((acc, crt) => acc > 0? acc - 1: acc, this.timePerQuestion),
)
),
takeUntil(stop$.pipe(skip(1))),
repeatWhen(completeSbj =>
completeSbj.pipe(
switchMapTo(
start$.pipe(
skip(1),
first()
)
)
)
)
).pipe(tap((value)=>this.timerService.setElapsed(this.timePerQuestion-value)));
In scoreboard component params subscribe i have reset the timer, so it will reset whenever question will change.
回答2:
As with every complex problem, you have to break it into smaller, digestible problems.
Therefore, I created a StackBlitz demo that recreates the basic functionalities:
- start a timer
- stop temporarily (mark timestamp); for example when all answers would be selected
- continue from the last timestamp
- reset timer
- stop timer
Here's the code for this:
const $ = document.querySelector.bind(document);
const start$ = fromEvent($('#start'), 'click').pipe(shareReplay(1));
const reset$ = fromEvent($('#reset'), 'click');
const stop$ = fromEvent($('#stop'), 'click');
const markTimestamp$ = fromEvent($('#mark'), 'click');
const continueFromLastTimestamp$ = fromEvent($('#continue'), 'click');
const src$ = concat(
start$.pipe(first()),
reset$
).pipe(
switchMapTo(
timer(0, 1000)
.pipe(
takeUntil(markTimestamp$),
repeatWhen(
completeSbj => completeSbj.pipe(switchMapTo(
continueFromLastTimestamp$.pipe(first())
))
),
scan((acc, crt) => acc + 1000, 0)
)
),
takeUntil(stop$),
repeatWhen(completeSbj => completeSbj.pipe(switchMapTo(start$.pipe(skip(1), first()))))
).subscribe(console.log)
Let's go through each relevant part.
concat(
start$.pipe(first()),
reset$
).pipe(switchMapTo(timer(...)))
The timer
will start to count from 0 only if it hasn't started before(start$.pipe(first())
) or the user wants to reset everything(reset$
).
concat(a$, b$)
makes sure that that b$
can't emit unless a$
completes.
timer(0, 1000)
.pipe(
takeUntil(markTimestamp$),
repeatWhen(
completeSbj => completeSbj.pipe(switchMapTo(
continueFromLastTimestamp$.pipe(first())
))
),
scan((acc, crt) => acc + 1000, 0)
)
We want the timer to be active until markTimestamp$
emits. When this happens, the source(timer(0, 1000)
) will be unsubscribed. With repeatWhen
we can decide when should the timer
be resubscribed. That is, when continueFromLastTimestamp$.pipe(first())
emits. It's important that we use first()
, otherwise the source might be re-subscribed multiple times.
Placing scan((acc, crt) => acc + 1000, 0)
after repeatWhen
ensures that the last timestamp won't be lost. For example, the timer might start at X
, at X+5
the user triggers markTimestamp$
and then at X + 100
the user triggers continueFromLastTimestamp$
. What this happens, the timer will emit X+6
.
takeUntil(stop$),
repeatWhen(completeSbj => completeSbj.pipe(switchMapTo(start$.pipe(skip(1), first()))))
The timer should be active until stop$
emits. Then, it can restart only if the user triggers start$
again. skip(1)
is used because we don't the cached value by the ReplaySubject
used by start$
's shareReplay
and first()
is used because the source should be re-subscribed only once.
来源:https://stackoverflow.com/questions/61855688/why-is-my-rxjs-countdown-clock-not-displaying