【问题标题】:Why is my RxJS countdown clock not displaying?为什么我的 RxJS 倒计时时钟不显示?
【发布时间】:2020-09-03 10:20:17
【问题描述】:

我正在开发一个 Angular 9 测验应用程序,我正在使用 RxJS 作为倒数计时器(在容器\记分板\time\time.component.ts 中)并且计时器似乎没有显示。 stopTimer() 函数应该在计时器停止的秒数上停止计时器。选择正确答案后计时器应停止,并且计时器应在问题之间重置。每个问题所用的时间应保存到 elapsedTimes 数组中。请在 Stackblitz 上的 TimeComponent 中查看我的计时器代码:https://stackblitz.com/edit/angular-9-quiz-app。谢谢。

下面的代码是我第一次使用 RxJS 构建倒计时时钟。我最近的代码在 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();
  }

我在 TimerService 中使用了 stopTimer() 和 pauseTimer(),因此我可以从不同的组件调用它们。

【问题讨论】:

  • 在问题中输入minimal reproducible example
  • 你能用文字写下你的要求吗?
  • 是的,我希望倒计时时钟从 20 秒减少到 0 秒。选择正确答案后,时钟应在剩余秒数处停止,每个问题的已用时间应保存在 elapsedTimes 数组中,以便以后计算总完成时间。对于以下每个问题,时钟应重置为 20 秒。用户对每个问题的回答和经过的时间应存储在其自己的数组中(使用 Result 接口)并传递给 ResultsComponent。
  • 倒计时时钟无法正常工作。有人可以帮忙吗?
  • 通过分叉一个新的堆栈闪电战来修复您的堆栈闪电战。在下面找到我的答案。

标签: angular rxjs


【解决方案1】:

我已经修复了你的 stackblitz。根据您对 timerservice 方法的调用,您的计时器将运行

并且无需在拆卸逻辑中设置经过时间。它会在计时器停止时自动推送。

Stackblitz 网址:- https://stackblitz.com/edit/angular-9-quiz-app-er3pjn

时间倒计时方法:-

  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)));

在记分板组件参数订阅中,我已经重置了计时器,所以只要问题发生变化,它就会重置。

【讨论】:

  • 谢谢,但是一旦到达每个新问题的问题路线,我需要时间连续运行,时间应该从 20 秒开始。此外,选择正确的答案时,时间应该停止。不确定我知道如何用主题替换按钮可观察对象。我不需要在 UI 中显示按钮。
  • 也不确定如何整合后面的“myTearDownLogic”函数中的逻辑。
  • @integral100x 使用新的 stackblitz 更新了解决方案。请检查。请再次阅读整个解决方案。
  • 太好了,谢谢!我在 ResultsComponent 中控制台记录了 elapsedTimes 数组,但我得到了重复的时间值,并且 completionTime 未定义。我更新了我的 Stackblitz。
  • 如果时间低于10秒,timeLeft之前应该有一个0,我试过 0:0{{ timeLeft$ | ts 文件中的异步 }} 和 timeLeft = Number($timeLeft)
【解决方案2】:

与每个复杂问题一样,您必须将其分解为更小的、易于消化的问题。

因此,我创建了一个 StackBlitz 演示来重新创建基本功能:

  • 启动计时器
  • 暂时停止(标记时间戳);例如,当所有答案都被选择
  • 从最后一个时间戳继续
  • 重置计时器
  • 停止计时器

下面是代码:

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)

让我们来看看每个相关的部分。


concat(
  start$.pipe(first()),
  reset$
).pipe(switchMapTo(timer(...)))

只有当timer 之前没有启动过(start$.pipe(first()))或用户想要重置所有内容(reset$)时,timer 才会从 0 开始计数。

concat(a$, b$) 确保 b$ 不能发射,除非 a$ 完成。


timer(0, 1000)
  .pipe(
    takeUntil(markTimestamp$),
    repeatWhen(
      completeSbj => completeSbj.pipe(switchMapTo(
        continueFromLastTimestamp$.pipe(first())
      ))
    ),
    scan((acc, crt) => acc + 1000, 0)
  )

我们希望计时器在markTimestamp$ 发出之前一直处于活动状态。发生这种情况时,源 (timer(0, 1000)) 将被取消订阅。使用repeatWhen,我们可以决定何时重新订阅timer。也就是说,当continueFromLastTimestamp$.pipe(first()) 发出时。请务必使用first(),否则可能会多次重新订阅源。

scan((acc, crt) => acc + 1000, 0) 放在repeatWhen 之后可确保最后一个时间戳不会丢失。例如,计时器可能从X 开始,在X+5 用户触发markTimestamp$,然后在X + 100 用户触发continueFromLastTimestamp$。发生这种情况时,计时器将发出X+6


takeUntil(stop$),
repeatWhen(completeSbj => completeSbj.pipe(switchMapTo(start$.pipe(skip(1), first()))))

stop$ 发出之前,计时器应该处于活动状态。然后,只有当用户再次触发start$ 时,它才能重新启动。使用skip(1) 是因为我们没有使用start$shareReplayfirst() 使用的ReplaySubject 缓存的值,因为source 应该是re - 订阅 仅一次


【讨论】:

  • 好的,我已将代码添加到我的 TimerComponent,但不知道如何在我的应用程序中使用它...正确答案,并且认为我不需要操作 DOM。
  • 我对上面代码的意图是提供一些可以构建的东西,或者至少是一个想法。例如,如果您希望计时器减少,请将最大值添加到扫描的第二个参数,然后在 cb 函数中根据您想要的数量减少。如果选择了所有正确答案,您可能希望停止计时器,这意味着您可能会有一个 obs 会在发生这种情况时发出。
  • 所以扫描看起来像:scan((acc, crt) => acc - 1000, 20000)?不确定如何将观察者绑定到模板。
  • 我试过了:
    0:{{ src$ | async }}
    表示“TimerComponent”类型上不存在属性“src$”。
  • 我在控制台中收到这些错误:ERROR TypeError: Invalid event target AND Error: InvalidPipeArgument: '[object Object]' for pipe 'AsyncPipe'
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-19
相关资源
最近更新 更多