這是上周工作中寫到的一個功能,大概的效果就是頁面中有幾處數字,統計公司的一些業務信息,需要在第一次出現的時候,做一個從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
評論區,有關於這個問題跟實現的交流,有興趣的可以查看。