数字限时增长效果实现:numberGrow.js
这是上周工作中写到的一个功能,大概的效果就是页面中有几处数字,统计公司的一些业务信息,需要在第一次出现的时候,做一个从0开始增长,大概2秒自动增长到真实数值,并停止增长的效果。这个问题的重点在于解决如何保证不同大小的数字都在2秒左右的时间自动增长完成,以及还有考虑延迟初始化的处理。后面这一点是为了保证,只有当数字第一次进入浏览器可视区域的时候,才会看到效果,因为这些数字有可能不在首屏的内容内,必须保证当用户滚动操作将数字显示出来的那一刻才能看到效果。本文分享我自己的实现思路,要是您有更好的方法,欢迎指点与修正 : )
demo地址:
http://liuyunzhuge.github.io/blog/numerGrow/dist/html/demo.html
代码地址:
https://github.com/liuyunzhuge/blog/tree/master/numerGrow
代码运行说明见git项目内的readme.md。
本文内容有补充修改,详见本文最后的补充说明!
1. 实现思路
先来看看如何保证大小不同的数字都在规定的时间内都能增长完成。
这个问题可以类比到曾经物理课的一些知识,就是速度路程与时间的关系。在这个问题中,最终的数字大小代表路程,单位时间内每个数字增长的值代表速度。由于路程不同,要走的时间相同,所以每个数字的增长速度也就不会相同。要解决这个问题,只要求出速度即可,因为时间和路程都是已知的。
但是在物理里面,速度的基本单位都是以秒或者小时为单位的,比如3m/s,30km/h,在程序里面显然是不能用秒或者小时的,因为这些单位太大了,而且要解决我们的问题,显然要用到计时器,计时器的单位是毫秒,所以在计算速度的时候,要以ms为单位。比如要显示的数值如果是100,规定的时间为2s,也就是2000ms,那么每ms要增加的数值就是100/2000,根据这个设想,可以得出如下的程序实现:
function NumberGrow(element, options) { options = options || {}; var $this = $(element), time = options.time || $this.data('time'),//总时间 num = options.num || $this.data('value'),//要显示的真实数值 step = num / (time * 1000),//每1ms增加的数值 start = 0,//计数器 interval,//定时器 old = 0; //step为每1ms增加的数值 interval = setInterval(function () { start = start + step; if (start >= num) { clearInterval(interval); interval = undefined; start = num; } var t = Math.floor(start); //t未发生改变的话就直接返回 //避免调用text函数,提高DOM性能 if (t == old) { return; } old = t; $this.text(old); }, 1); }
这个实现虽然从理论上是可行的,但是实际运行的时候,会发现这个增长的效果会远远超过规定的时间,原因可能在于setInterval里面的函数执行也是需要耗费时间的,而且不一定能在定时器的间隔内就执行完,所以这些额外执行的时间跟每次执行的间隔累计起来就会超过规定的时间。要解决这个问题,我想到了一篇文章里面提到的关于帧率的问题:
http://www.ruanyifeng.com/blog/2015/09/web-page-performance-in-depth.html
如果网页动画能够做到每秒60帧,就会跟显示器同步刷新,达到最佳的视觉效果。这意味着,一秒之内进行60次重新渲染,每次重新渲染的时间不能超过16.66毫秒。
我们可以把setInterval的每次执行都看成是一帧,然后把setInterval的执行间隔改成16,只要它的回调函数执行时间不超过16ms,那么这个计时器累计运行的时间就只跟间隔时间有关系,而跟回调函数的执行时间没有关系,因为回调函数是在回调间隔时间内执行完的!这就是解决前面问题的关键:
1)把计时器的间隔改成16ms
2)把速度从每ms增加的数值改成每16ms增加的数值
最终正确的实现如下(对应的代码是https://github.com/liuyunzhuge/blog/blob/master/numerGrow/src/js/mod/numberGrow.js):
function NumberGrow(element, options) { options = options || {}; var $this = $(element), time = options.time || $this.data('time'),//总时间 num = options.num || $this.data('value'),//要显示的真实数值 step = num * 16 / (time * 1000),//每16ms增加的数值 start = 0,//计数器 interval,//定时器 old = 0; //每帧不能超过16ms,所以理想的interval间隔为16ms //step为每16ms增加的数值 interval = setInterval(function () { start = start + step; if (start >= num) { clearInterval(interval); interval = undefined; start = num; } var t = Math.floor(start); //t未发生改变的话就直接返回 //避免调用text函数,提高DOM性能 if (t == old) { return; } old = t; $this.text(old); }, 16); }
基于这个实现去测试,会发现最终的运行结果与规定的时间只有几十ms的差别,基本上已经达到我们的要求了。这几十毫秒的差距,我觉得来自于浏览器对于setInterval的管理,如果想要十分精准地在规定时间内完成这个效果,我还没有想到好的方法,希望有这个思路的朋友愿意分享出来。
事实上,定时器的间隔不用16,用8, 9, 10, 18, 20, 24也都可以,效果跟16差不多,因为定时器的回调函数执行在浏览器正常的情况下肯定不需要8ms,里面啥都没干呢。。。用8, 9, 10, 18, 20, 24还是16的区别在于数字变化的速度看起来不一样而已,间隔越小变化越快,间隔越大变化越慢,所以给人的视觉体验不同。用16是因为它比较接近于16.66ms这个数值。
以上部分是关于如何保证大小不同的数字都在规定的时间内都能增长完成的说明,下面来看看如何做滚动时的懒加载。
我的思路考虑地相对简单,借助滚动事件,监听各个元素是否完全进入浏览器的可视区域,只有当它完全在浏览器可视区域的时候才初始化,并且只执行一次,当某个类型的组件全部都初始化以后,还会做一个destroy的处理,以便提供页面性能。
这部分的实现对应的代码是:https://github.com/liuyunzhuge/blog/blob/master/numerGrow/src/js/mod/scrollLazyInit.js。
其中有几个关键点可以再在博客里说明一下:
1)options
scrollLazyInit提供了两个option,一个ns,表示命名空间,用来注册scroll事件,因为这个组件可能不只有numberGrow才会用到,页面当中其它耗时的组件也可以利用这个组件来做简单的懒初始,有了这个个ns就可以管理不同的组件了;还有一个delay就是滚动回调节流时的间隔,一般不会用到。
在使用scrollLazyInit的时候,必须先实例化才能使用,实例化的时候可以传递ns和delay参数:
2)add方法
每个srollLazy的实例都有两个实例方法,其中一个就是add方法,用来将要延迟初始化的功能添加到scrollLazy来管理:
add方法有两个参数,第一个是要延迟初始化的dom元素,要用它来判断是否完全进入可视区域,第二个是当元素完全进入可视区域时回调,在这个回调里面来做组件初始化,就如上图所示。
3)start方法
每个scrollLazy实例的另外一个实例方法就是start,这个其实就是添加滚动监听而已。在把所有的延迟初始化的组件都add完之后,再调用这个方法即可:
由于它的实现并不复杂,而且也不属于本文重点,原本这一部分功能是在numberGrow里面的,后来考虑到职责分离,才单独写成了另外一个组件,代码只有60行,相信您的能力,肯定能直接看明白源码。
另外还值得一说的是,这个scrollLazy还有优化的地方,就是在判断初始化的时机这一块,因为目前是判断元素完全进入可视区域的时候才初始化,这对于一些高度很小的元素来说,没有问题,但是对于高度可能超过可视区域的元素来说,肯定是不行的,所以在使用的时候要注意这个点。
2. 使用说明
这个功能在使用的时候,可以直接通过data属性来注册,因为这种效果型的功能,基本上都没有业务逻辑,不必要放到跟业务逻辑相关的js里面去,所以只要在html上注册即可:
第一个data-ride=”numberGrow”不能省,因为在numberGrow.js里面,是通过这个属性来找到需要自动注册的元素的。后面的value和time分别表示要增长的真实数值和增长的有效时间。
3. 本文小结
本文介绍了自己关于一个简单的网页效果的实现思路,因为觉得那个类比物理中的速度时间路程的点比较有趣,所以把它分享出来,希望对您有所参考价值,谢谢阅读:)
补充于2016-06-02
本文中提供的实现有瑕疵,虽然在demo中看到的运行结果跟预期一致,但是存在问题,这个问题要感谢 上位者的怜悯在评论中帮我指正出来,并提出了一个更佳的实现方式。这个问题是:本文的实现思路,会受到代码执行时间的影响,这个代码执行时间包括定时器回调函数的执行时间以及页面中其它js代码的执行时间,代码执行时间越长,会导致最终的效果持续时间越偏离规定的运行时间。
虽然从思路上来说,本文的想法很好,类比了物理中速度与时间以及路程的关系,但是这毕竟是代码执行环境,无法保证“匀速”增长。更好的实现方式是,上位者的怜悯在评论区中提出的按照比例来计算当前数值的方法,计算公式是:当前值 = 总数值 * (已经运行的时间 / 总时间)。
我把他的代码重新整理了一下,并封装为了numberGrowBetter.js,放在js/mod文件夹下,源码请查看:
https://github.com/liuyunzhuge/blog/blob/master/numerGrow/src/js/mod/numberGrowBetter.js
这个实现对应的demo:
http://liuyunzhuge.github.io/blog/numerGrow/dist/html/demo2.html
评论区,有关于这个问题跟实现的交流,有兴趣的可以查看。