这个问题是我之前提出的问题的延伸,你可以在这里找到:
How to use RxJS to display a "user is typing" indicator?
在成功跟踪用户是否正在键入之后,我需要能够将该特定状态用作时钟的触发器。
逻辑很简单,基本上我想在用户输入时运行一个时钟。但是当用户停止输入时,我需要暂停时钟。当用户再次开始输入时,时钟应该继续累积。
我已经能够让它工作,但它看起来像一团糟,我需要帮助重构它所以它不是一个意大利面条的球。这是代码的样子:
/*** Render Functions ***/
const showTyping = () =>
$('.typing').text('User is typing...');
const showIdle = () =>
$('.typing').text('');
const updateTimer = (x) =>
$('.timer').text(x);
/*** Program Logic ***/
const typing$ = Rx.Observable
.fromEvent($('#input'), 'input')
.switchMapTo(Rx.Observable
.never()
.startWith('TYPING')
.merge(Rx.Observable.timer(1000).mapTo('IDLE')))
.do(e => e === 'TYPING' ? showTyping() : showIdle());
const timer$ = Rx.Observable
.interval(1000)
.withLatestFrom(typing$)
.map(x => x[1] === 'TYPING' ? 1 : 0)
.scan((a, b) => a + b)
.do(console.log)
.subscribe(updateTimer)
以下是实时JSBin的链接:http://jsbin.com/lazeyov/edit?js,console,output
也许我将引导您完成代码的逻辑:
switchMap
来:( a)触发原始的“TYPING”事件,这样我们就不会失去它,并且(b)触发“IDLE”事件, 1秒后。您可以看到我将它们创建为单独的流,然后将它们合并在一起。这样,我得到一个流,它将指示输入框的“输入状态”。withLatestFrom
,我将此流与先前的“输入状态”流组合在一起。现在它们组合在一起,我可以检查输入状态是“IDLE”还是“TYPING”。如果他们正在输入内容,我会将该事件的值设为1
,否则为0
。1
和0
的流,我所要做的就是用.scan()
添加它们并将其渲染到DOM。编写此功能的RxJS方式是什么?
基于@ osln的回答。
/*** Helper Functions ***/
const showTyping = () => $('.typing').text('User is typing...');
const showIdle = () => $('.typing').text('');
const updateTimer = (x) => $('.timer').text(x);
const handleTypingStateChange = state =>
state === 1 ? showTyping() : showIdle();
/*** Program Logic ***/
const inputEvents$ = Rx.Observable.fromEvent($('#input'), 'input').share();
// streams to indicate when user is typing or has become idle
const typing$ = inputEvents$.mapTo(1);
const isIdle$ = inputEvents$.debounceTime(1000).mapTo(0);
// stream to emit "typing state" change-events
const typingState$ = Rx.Observable.merge(typing$, isIdle$)
.distinctUntilChanged()
.share();
// every second, sample from typingState$
// if user is typing, add 1, otherwise 0
const timer$ = Rx.Observable
.interval(1000)
.withLatestFrom(typingState$, (tick, typingState) => typingState)
.scan((a, b) => a + b, 0)
// subscribe to streams
timer$.subscribe(updateTimer);
typingState$.subscribe(handleTypingStateChange);
基于多鲁斯的回答。
/*** Helper Functions ***/
const showTyping = () => $('.typing').text('User is typing...');
const showIdle = () => $('.typing').text('');
const updateTimer = (x) => $('.timer').text(x);
/*** Program Logic ***/
// declare shared streams
const inputEvents$ = Rx.Observable.fromEvent($('#input'), 'input').share();
const idle$ = inputEvents$.debounceTime(1000).share();
// intermediate stream for counting until idle
const countUntilIdle$ = Rx.Observable
.interval(1000)
.startWith('start counter') // first tick required so we start watching for idle events right away
.takeUntil(idle$);
// build clock stream
const clock$ = inputEvents$
.exhaustMap(() => countUntilIdle$)
.scan((acc) => acc + 1, 0)
/*** Subscribe to Streams ***/
idle$.subscribe(showIdle);
inputEvents$.subscribe(showTyping);
clock$.subscribe(updateTimer);
答案 0 :(得分:4)
如果你想不断更新用户界面,我认为没有任何方法可以使用计时器 - 我可能通过更改事件启动计时器来编写流程略有不同 - 但是你当前的流似乎还可以因为它已经:
const inputEvents$ = Rx.Observable
.fromEvent($('#input'), 'input');
const typing$ = Rx.Observable.merge(
inputEvents$.mapTo('TYPING'),
inputEvents$.debounceTime(1000).mapTo('IDLE')
)
.distinctUntilChanged()
.do(e => e === 'TYPING' ? showTyping() : showIdle())
.publishReplay(1)
.refCount();
const isTyping$ = typing$
.map(e => e === "TYPING");
const timer$ = isTyping$
.switchMap(isTyping => isTyping ? Rx.Observable.interval(100) : Rx.Observable.never())
.scan(totalMs => (totalMs + 100), 0)
.subscribe(updateTimer);
直播here。
如果您不需要更新UI并且只想捕获输入的持续时间,您可以使用start-and-stop事件并将它们映射到这样的时间戳,例如:
const isTyping$ = typing$
.map(e => e === "TYPING");
const exactTimer$ = isTyping$
.map(() => +new Date())
.bufferCount(2)
.map((times) => times[1] - times[0])
.do(updateTimer)
.do(typedTime => console.log("User typed " + typedTime + "ms"))
.subscribe();
直播here。
答案 1 :(得分:1)
我注意到您的代码存在一些问题。它的要点是好的,但如果你使用不同的操作符,你可以更容易地做同样的事情。
首先使用switchMap
,每次新输入到达时,这是一个很好的运算符来启动新流。但是,您真正想要的是只要用户输入就继续当前计时器。这里一个更好的运算符是exhaustMap
因为exhaustMap
将保持已经有效的计时器直到它停止。如果用户没有键入1秒钟,我们就可以停止活动计时器。这可以通过.takeUntil(input.debounceTime(1000))
轻松完成。这将导致非常短的查询:
input.exhaustMap(() => Rx.Observable.timer(1000).takeUntil(input.debounceTime(1000)))
对于此查询,我们可以挂钩您想要的显示事件,showTyping
,showIdle
等。我们还需要修复计时器index
,因为它会在每次用户时重置停止打字。这可以使用map
中项目函数的第二个参数来完成,因为这是当前流中值的索引。
Rx.Observable.fromEvent($('#input'), 'input')
.publish(input => input
.exhaustMap(() => {
showTyping();
return Rx.Observable.interval(1000)
.takeUntil(input.startWith(0).debounceTime(1001))
.finally(showIdle);
})
).map((_, index) => index + 1) // zero based index, so we add one.
.subscribe(updateTimer);
注意我在这里使用了publish
,但由于来源很热,因此并不是严格需要的。无论如何推荐,因为我们使用input
两次,现在我们不必考虑它是热还是冷。
/*** Helper Functions ***/
const showTyping = () =>
$('.typing').text('User is typing...');
const showIdle = () =>
$('.typing').text('');
const updateTimer = (x) =>
$('.timer').text(x);
/*** Program Logic ***/
Rx.Observable.fromEvent($('#input'), 'input')
.publish(input => input
.exhaustMap(() => {
showTyping();
return Rx.Observable.interval(1000)
.takeUntil(input.startWith(0).debounceTime(1001))
.finally(showIdle);
})
).map((_, index) => index + 1) // zero based index, so we add one.
.subscribe(updateTimer);
<head>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
<script src="https://unpkg.com/@reactivex/rxjs@5.0.0-beta.12/dist/global/Rx.js"></script>
</head>
<body>
<div>
<div>Seconds spent typing: <span class="timer">0</span></div>
<input type="text" id="input">
<div class="typing"></div>
</div>
</body>