为什么 mapTo 只改变一次?

Why mapTo changes only one time?

我正在制作一个秒表,当我想第二次重置时钟时,它没有改变。 第一次点击设置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()

问题

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

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

一种修复方法:

创建一个新对象。

// 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 的 Subject)。

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