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