【问题标题】:Why mapTo changes only one time?为什么 mapTo 只改变一次?
【发布时间】:2021-05-18 10:19:18
【问题描述】:

我正在制作秒表,当我想第二次重置时钟时,它没有改变。 第一次点击设置h:0,m:0,s:0,再次点击不设置h:0,m:0,s:0,秒表继续。

const events$ = merge(
    fromEvent(startBtn, 'click').pipe(mapTo({count: true})),
    click$.pipe(mapTo({count: false})), 
    fromEvent(resetBtn, 'click').pipe(mapTo({time: {h: 0, m: 0, s: 0}})) // there is reseting
    )
    
const stopWatch$ = events$.pipe(
    startWith({count: false, time: {h: 0, m: 0, s: 0}}), 
    scan((state, curr) => (Object.assign(Object.assign({}, state), curr)), {}), 
    switchMap((state) => state.count
    ? interval(1000)
        .pipe(
            tap(_ => {
                if (state.time.s > 59) {
                    state.time.s = 0
                    state.time.m++
                }
                if (state.time.s > 59) {
                    state.time.s = 0
                    state.time.h++
                }
                const {h, m, s} = state.time
                secondsField.innerHTML = s + 1
                minuitesField.innerHTML = m
                hours.innerHTML = h
                state.time.s++
            }),
        )
    : EMPTY)
stopWatch$.subscribe()

【问题讨论】:

  • 我怀疑这种情况正在发生,因为您每次调用 mapTo() 时都使用相同的对象实例。尝试使用map(() => ({time: {h: 0, m: 0, s: 0}})) instad。
  • @martin 很好用,非常感谢

标签: rxjs rxjs-observables rxjs-pipeable-operators


【解决方案1】:

问题

您正在使用可变状态并将其更新为 observable 发出的事件的副作用(这就是 tap 所做的)。

一般来说,创建间接改变创建它们的流的副作用是一个坏主意。因此,创建日志或显示值不太可能导致问题,但改变对象然后将其注入流回难以维护/扩展。

一种修复:

创建一个新对象。

// fromEvent(resetBtn, 'click').pipe(mapTo({time: {h: 0, m: 0, s: 0}}))
fromEvent(resetBtn, 'click').pipe(map(_ => ({time: {h: 0, m: 0, s: 0}})))

这应该可行,尽管它是一种创可贴的解决方案。

预制解决方案

这是我不久前制作的秒表。这是它的工作原理。你可以通过给它一个 control$ observable 来创建一个秒表(我在这个例子中使用了一个名为 controller 的主题)。

当control$ 发出"START" 时,秒表启动,当它发出"STOP" 时,秒表停止,当它发出"RESET" 时,秒表将计数器设置回零。当control$ 出错、完成或发出"END" 时,秒表出错或完成。

function createStopwatch(control$: Observable<string>, interval = 1000): Observable<number>{
  return defer(() => {
    let toggle: boolean = false;
    let count: number = 0;

    const ticker = () => {
      return timer(0, interval).pipe(
        map(x => count++)
      )
    }

    return control$.pipe(
      catchError(_ => of("END")),
      s => concat(s, of("END")),
      filter(control => 
        control === "START" ||
        control === "STOP" ||
        control === "RESET" ||
        control === "END"
      ),
      switchMap(control => {
        if(control === "START" && !toggle){
          toggle = true;
          return ticker();
        }else if(control === "STOP" && toggle){
          toggle = false;
          return EMPTY;
        }else if(control === "RESET"){
          count = 0;
          if(toggle){
            return ticker();
          }
        }
        return EMPTY;
      })
    );
  });
}

// Adapted to your code :)

const controller = new Subject<string>();
const seconds$ = createStopwatch(controller);

fromEvent(startBtn, 'click').pipe(mapTo("START")).subscribe(controller);
fromEvent(resetBtn, 'click').pipe(mapTo("RESET")).subscribe(controller);

seconds$.subscribe(seconds => {
  secondsField.innerHTML = seconds % 60;
  minuitesField.innerHTML = Math.floor(seconds / 60) % 60;
  hours.innerHTML = Math.floor(seconds / 3600);
});

作为奖励,您可能会看到如何在不重置的情况下制作一个Stops 这个计时器的按钮。

没有主题

这是一种更惯用的反应方式来做到这一点。它通过直接合并 DOM 事件(中间没有主题)为秒表生成 control$。

这确实剥夺了您编写 controller.next("RESET"); 之类的东西以随意将您自己的价值注入流的能力。 或 controller.complete(); 当您的应用使用秒表完成时(尽管您可能会通过其他事件自动执行此操作)。

...
// Adapted to your code :)

createStopwatch(merge(
  fromEvent(startBtn, 'click').pipe(mapTo("START")),
  fromEvent(resetBtn, 'click').pipe(mapTo("RESET"))
)).subscribe(seconds => {
  secondsField.innerHTML = seconds % 60;
  minuitesField.innerHTML = Math.floor(seconds / 60) % 60;
  hours.innerHTML = Math.floor(seconds / 3600);
});

【讨论】:

  • 非常感谢,不胜感激 :)
猜你喜欢
  • 2021-02-05
  • 1970-01-01
  • 1970-01-01
  • 2019-05-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-25
相关资源
最近更新 更多