• 智能计数器
    • 原生 JS
      • HTML
  • Angular 版本
    • HTML
  • 使用到的操作符

    智能计数器

    一个在页面上带有动态更新数字效果的有趣元素就是智能计数器,也可以称之为里程表效果。不采用上下跳数的方式,而是快速地清点到期望的数字,这能达到一种很酷的效果。能做到这点的流行库的其中一个就是由 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>

    使用到的操作符

    • fromEvent
    • map
    • mapTo
    • scan
    • startWith
    • switchMap
    • takeWhile