智能计数器

一个在页面上带有动态更新数字效果的有趣元素就是智能计数器,也可以称之为里程表效果。不采用上下跳数的方式,而是快速地清点到期望的数字,这能达到一种很酷的效果。能做到这点的流行库的其中一个就是由 Hubspot 所写的 odometer 。让我们来看看如何使用短短几行 RxJS 代码来实现类似的效果。

智能计数器 - 图1

原生 JS

( JSBin |
JSFiddle )

  1. // 工具函数
  2. const takeUntilFunc = (endRange, currentNumber) => {
  3. return endRange > currentNumber
  4. ? val => val <= endRange
  5. : val => val >= endRange;
  6. };
  7. const positiveOrNegative = (endRange, currentNumber) => {
  8. return endRange > currentNumber ? 1 : -1;
  9. };
  10. const updateHTML = id => val => (document.getElementById(id).innerHTML = val);
  11. // 显示
  12. const input = document.getElementById('range');
  13. const updateButton = document.getElementById('update');
  14. const subscription = (function(currentNumber) {
  15. return fromEvent(updateButton, 'click').pipe(
  16. map(_ => parseInt(input.value)),
  17. switchMap(endRange => {
  18. return timer(0, 20).pipe(
  19. mapTo(positiveOrNegative(endRange, currentNumber)),
  20. startWith(currentNumber),
  21. scan((acc, curr) => acc + curr),
  22. takeWhile(takeUntilFunc(endRange, currentNumber));
  23. )
  24. }),
  25. tap(v => (currentNumber = v)),
  26. startWith(currentNumber)
  27. )
  28. .subscribe(updateHTML('display'));
  29. })(0);
HTML
  1. <input id="range" type="number">
  2. <button id="update">Update</button>
  3. <h3 id="display">0</h3>

我们可以轻易地获取我们的原生 JS 所写的智能计数器并将其包装在任何流行的基于 UI 库中。下面是 Angular 版本的智能计数器,它接收一个更新结束范围的 Input 输入属性并执行适当的转换。

Angular 版本

(
StackBlitz
)

  1. import { Component, Input, OnDestroy } from '@angular/core';
  2. import { Subject } from 'rxjs/Subject';
  3. import { timer } from 'rxjs/observable/timer';
  4. import { switchMap, startWith, scan, takeWhile, takeUntil, mapTo } from 'rxjs/operators';
  5. @Component({
  6. selector: 'number-tracker',
  7. template: `
  8. <h3> {{ currentNumber }}</h3>
  9. `
  10. })
  11. export class NumberTrackerComponent implements OnDestroy {
  12. @Input()
  13. set end(endRange: number) {
  14. this._counterSub$.next(endRange);
  15. }
  16. @Input() countInterval = 20;
  17. public currentNumber = 0;
  18. private _counterSub$ = new Subject();
  19. private _onDestroy$ = new Subject();
  20. constructor() {
  21. this._counterSub$
  22. .pipe(
  23. switchMap(endRange => {
  24. return timer(0, this.countInterval).pipe(
  25. mapTo(this.positiveOrNegative(endRange, this.currentNumber)),
  26. startWith(this.currentNumber),
  27. scan((acc: number, curr: number) => acc + curr),
  28. takeWhile(this.isApproachingRange(endRange, this.currentNumber))
  29. )
  30. }),
  31. takeUntil(this._onDestroy$)
  32. )
  33. .subscribe((val: number) => this.currentNumber = val);
  34. }
  35. private positiveOrNegative(endRange, currentNumber) {
  36. return endRange > currentNumber ? 1 : -1;
  37. }
  38. private isApproachingRange(endRange, currentNumber) {
  39. return endRange > currentNumber
  40. ? val => val <= endRange
  41. : val => val >= endRange;
  42. }
  43. ngOnDestroy() {
  44. this._onDestroy$.next();
  45. this._onDestroy$.complete();
  46. }
  47. }
HTML
  1. <p>
  2. <input type="number"
  3. (keyup.enter)="counterNumber = vanillaInput.value"
  4. #vanillaInput>
  5. <button
  6. (click)="counterNumber = vanillaInput.value">
  7. Update number
  8. </button>
  9. </p>
  10. <number-tracker [end]="counterNumber"></number-tracker>

使用到的操作符