在RxJS中创建一个打字计时器;跟踪打字时间

时间:2017-01-07 14:00:16

标签: javascript reactjs rx-java rxjs reactive-programming

这个问题是我之前提出的问题的延伸,你可以在这里找到:

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

也许我将引导您完成代码的逻辑:

  1. 我首先构建一个流来捕获每个打字事件。
  2. 对于这些事件中的每一个,我将使用switchMap来:( a)触发原始的“TYPING”事件,这样我们就不会失去它,并且(b)触发“IDLE”事件, 1秒后。您可以看到我将它们创建为单独的流,然后将它们合并在一起。这样,我得到一个流,它将指示输入框的“输入状态”。
  3. 我创建了第二个每秒发送一个事件的流。使用withLatestFrom,我将此流与先前的“输入状态”流组合在一起。现在它们组合在一起,我可以检查输入状态是“IDLE”还是“TYPING”。如果他们正在输入内容,我会将该事件的值设为1,否则为0
  4. 现在我有一个10的流,我所要做的就是用.scan()添加它们并将其渲染到DOM。
  5. 编写此功能的RxJS方式是什么?

    编辑:方法1 - 构建变更事件流

    基于@ 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);
    

    JSBin Live Demo

    编辑:方法2 - 当用户开始输入

    时,使用exhaustMap启动计数器

    基于多鲁斯的回答。

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

    JSBin Live Demo

2 个答案:

答案 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)))

对于此查询,我们可以挂钩您想要的显示事件,showTypingshowIdle等。我们还需要修复计时器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两次,现在我们不必考虑它是热还是冷。

Live demo

/*** 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>