从简单的数字滚动到countUp代码

先来实现一个简单的数字翻滚功能。

简单版

  • 没有各种配置项和校验
  • 每次滚动数字+1
image

初始值其实是0,在录视频的时候没录到。

代码很简单,原理就是利用requestAnimationFrame改变dom的内容:

const end = 636;

useEffect(() => {
  let number = 0;
  const el = document.querySelector('#count');
  let raf: number;
  
  const count = () => {
    if(number < end) {
        number++; 
    }
    if(number < end) raf = requestAnimationFrame(count);
    else cancelAnimationFrame(raf);
    
    number = number >= end ? end : number;
    if(el) el.innerHTML = number + '';
  }
  
   count();
},[]);


return <div id="count"></div>

进阶版

控制时间,每秒数字才加1


图片.png

原理是利用requestAnimationFrame会给回调函数传递一个与performance.now()相同的值,它表示 开始requestAnimationFrame() 去执行回调函数的时刻,通过对比前后两次的差值来实现:

  const end = 60;

  useEffect(() => {
    let number = 0;
    const el = document.querySelector('#count');
    let raf: number;
    let lastTimestamp: number;
    const count = (timestamp: number) => {
      if (!lastTimestamp) lastTimestamp = timestamp;
      else if (timestamp - lastTimestamp > 1000 + Number.EPSILON) {
        lastTimestamp = timestamp;
        if (number < end) {
          number++;
        }
      }

      if (number < end) raf = requestAnimationFrame(count);
      else cancelAnimationFrame(raf);

      number = number >= end ? end : number;
      if (el) el.innerHTML = number + '';
    };

    count(performance.now());
  }, []);

  return <div id="count"></div>;

其实countUp的核心原理也是用requestAnimation来实现的,在此基础上封装了各种参数配置、回调函数、校验、开始、暂停等。

countUp代码阅读

工具函数介绍

resetDuration

重置开始时间、持续时间和剩余时间

resetDuration() {
     this.startTime = null;
   // 从这里可以看出options的duration单位是秒
   this.duration = Number(this.options.duration) * 1000;
   // 剩余时间
   this.remaining = this.duration;
}

printValue

在目标节点展示数字,这里主要是根据目标节点的类型来更改内容:

  printValue(val: number) {
    const result = this.formattingFn(val);
    
    if(this.el.tagName === 'INPUT') {
        const input = this.el as HTMLInputElement;
        input.value = result;
    } else if (this.el.tagName === 'text' || this.el.tagName === 'tspan') {
      this.el.textContent = result;
    } else {
      this.el.innerHTML = result;
    }
}

formatNumber

默认的格式化数字方法,这里有7个属性需要了解:
● decimalPlaces:保留几位小数,默认是0
● decimal:小数点,默认是字符.
● useGrouping:是否对整数部分按千分位处理,默认是true
● separator:千分位分隔符号,默认是字符,
● numerals:把整数和小数替换掉,例如x1 = "1234", numerals = ['0','4','3','2','1'], x1替换结果是 "4321"
● prefix:前缀,位于负数符号后,默认无
● suffix:后缀,位于整个数字后,默认无

formatNumber = (num: number): string => {
    const neg = (num < 0) ? '-' : '';
    let result: string,
      x: string[],
      x1: string,
      x2: string,
      x3: string;
    result = Math.abs(num).toFixed(this.options.decimalPlaces);
    result += '';
    x = result.split('.');
    
    // x1是整数部分
    x1 = x[0];
    // decimal默认是小数点. 可以由使用者设置
    // x2 是小数部分(带小数点)
    x2 = x.length > 1 ? this.options.decimal + x[1] : '';
    // useGrouping:是否对整数部分按千分位处理
    if (this.options.useGrouping) {
      x3 = '';
      for (let i = 0, len = x1.length; i < len; ++i) {
        if (i !== 0 && (i % 3) === 0) {
          x3 = this.options.separator + x3;
        }
        x3 = x1[len - i - 1] + x3;
      }
      x1 = x3;
    }
    // 把整数和小数替换掉,例如x1 = "1234", numerals = ['0','4','3','2','1'], x1替换结果是 "4321"。
    if (this.options.numerals && this.options.numerals.length) {
      x1 = x1.replace(/[0-9]/g, (w) => this.options.numerals[+w]);
      x2 = x2.replace(/[0-9]/g, (w) => this.options.numerals[+w]);
    }
   
    return neg + this.options.prefix + x1 + x2 + this.options.suffix;
  }

开始

首先,新建一个CountUp实例:

const countUp = new CountUp('count', 5234);

if(!countUp.error) {
    countUp.start(); 
}

在判断error不存在后,执行start开始滚动数字。
接下来看看start方法内部。

start

如果新建实例参数没有duration,则会直接打印endVal。如果有duration(默认是2000),则执行数字滚动动画:

start(callback?: (args?: any) => any) {
    if (this.error) {
      return;
    }
    this.callback = callback;
    if (this.duration > 0) {
      this.determineDirectionAndSmartEasing();
      this.paused = false;
      this.rAF = requestAnimationFrame(this.count);
    } else {
      this.printValue(this.endVal);
    }
 }

注意到start有个回调函数,这个是执行完数字翻滚后会执行的函数。

所以接下来的重点是determineDirectionAndSmartEasing和count这两个方法。

determineDirectionAndSmartEasing

这个词的中文意思是"确认方向和智能缓动"。"确认方向"大致可以推动出这个方法是在确认数字是向上滚(小于end)还是向下滚(大于end),"智能缓动"则是给滚动添加一些条件、判断等。

private determineDirectionAndSmartEasing() {
        const end = (this.finalEndVal) ? this.finalEndVal : this.endVal;
  
    // 是否向下滚动
    this.countDown = (this.startVal > end);
    const animateAmount = end - this.startVal;
  
    // smartEasingThreshold是智能缓动阈值,默认999。只有剩余值大于smartEasingThreshold才会继续执行
    if (Math.abs(animateAmount) > this.options.smartEasingThreshold) {
      this.finalEndVal = end;
      const up = (this.countDown) ? 1 : -1;
      this.endVal = end + (up * this.options.smartEasingAmount);
      this.duration = this.duration / 2;
    } else {
      this.endVal = end;
      this.finalEndVal = null;
    }
  
    // ...
  }

接下来,看看count方法。

count

从命名上推测count是"计算"的意思。所以推断这个方法是计算当前值,然后打印出当前值。

count = (timestamp: number) => {
    if(!this.startTime) {
    this.startTime = timestamp; 
  }
  
  const progress = timestamp - this.startTime;
  this.remaining = this.duration - progress;
  
  // ...
  
  // 滚动方向
  if (this.countDown) {
        this.frameVal =
        this.startVal -
          (this.startVal - this.endVal) * (progress / this.duration);
   } 
   else {
        this.frameVal =
        this.startVal +
          (this.endVal - this.startVal) * (progress / this.duration);
   }
  
    // 边界条件
    if (this.countDown) {
      this.frameVal = this.frameVal < this.endVal ? this.endVal : this.frameVal;
    } else {
      this.frameVal = this.frameVal > this.endVal ? this.endVal : this.frameVal;
    }
  
    // decimal
    this.frameVal = Number(this.frameVal.toFixed(this.options.decimalPlaces));

    // 在节点上打印数字
    this.printValue(this.frameVal);

    // 判断是否继续
    if (progress < this.duration) {
      this.rAF = requestAnimationFrame(this.count);
    } else if (this.finalEndVal !== null) {
      // smart easing
      this.update(this.finalEndVal);
    } else {
      if (this.callback) {
        this.callback();
      }
    }
}

第一次执行count,给this.startTime赋值。progress等于0,更新this.remaining。
之后的每次执行,progress会越来越接近this.duration,直到大于this.duration。
接着判断是否有this.finalEndVal,有才执行 this.update(this.finalEndVal),没有则结束。

总结

刚开始看countUp源码的时候,发现代码300行左右,想着应该能很快看完。后面发现代码内涉及到数学运算比较复杂,更坑的是参数命名太简写了,就像下面这个,完全不懂这个方法在做什么:

easeOutExpo = (t: number, b: number, c: number, d: number): number =>
    (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;

所以在看源码的时候可以借助实例和断点调试来分析代码。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容