在 RxJS 中制作打字计时器;跟踪打字时间

IT技术 javascript reactjs rx-java rxjs reactive-programming
2021-05-18 20:33:03

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

如何使用 RxJS 显示“用户正在输入”指示器?

在成功跟踪用户是否正在打字之后,我需要能够使用该特定状态作为时钟的触发器。

逻辑很简单,本质上我希望在用户打字时运行一个时钟。但是当用户停止打字时,我需要时钟暂停。当用户再次开始打字时,时钟应继续累积。

我已经能够让它工作,但它看起来一团糟,我需要帮助重构它,所以它不是意大利面条球。下面是代码的样子:

/*** 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) 1 秒后触发“IDLE”事件。您可以看到我将它们创建为单独的流,然后将它们合并在一起。这样,我得到一个流,将指示输入框的“输入状态”。
  3. 我创建了第二个流,每秒发送一个事件。使用withLatestFrom,我将此流与之前的“输入状态”流结合起来。现在它们组合在一起,我可以检查打字状态是“空闲”还是“打字”。如果他们正在打字,我给事件一个值1,否则一个0
  4. 现在我有一个1s 和0s,我所要做的就是将它们添加回来并将其.scan()呈现给 DOM。

编写此功能的 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 现场演示

编辑:方法 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 现场演示

2个回答

如果您想不断更新 UI,我认为没有任何方法可以使用计时器 - 我可能通过更改事件启动计时器来编写流有点不同 - 但您当前的流似乎也可以,因为它是已经:

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

在这里


如果您不需要更新 UI 并且只想捕获输入的持续时间,您可以使用开始和停止事件并将它们映射到这样的时间戳,例如:

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

在这里

我注意到您的代码存在一些问题。它的要点很好,但是如果您使用不同的运算符,则可以更轻松地完成相同的操作。

首先使用switchMap,这是一个很好的运算符,可以在每次新输入到达时启动一个新流。但是,您真正想要的是在用户输入时继续当前计时器。这里更好的操作员是exhaustMap因为exhaustMap将保持已经激活的计时器直到它停止。如果用户在 1 秒内没有打字,我们可以停止活动计时器。使用.takeUntil(input.debounceTime(1000)). 这将导致非常短的查询:

input.exhaustMap(() => Rx.Observable.timer(1000).takeUntil(input.debounceTime(1000)))

对于这个查询,我们可以挂钩您想要的显示事件showTypingshowIdle等等。我们还需要修复计时器index,因为它会在用户每次停止输入时重置。这可以通过使用项目函数 in 的第二个参数来完成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>