【轉載】函數節流

轉載自AlloyTeam:http://www.alloyteam.com/2012/11/javascript-throttle/ 

什麼是函數節流?

介紹前,先說下背景。在前端開發中,有時會爲頁面綁定resize事件,或者爲一個頁面元素綁定拖拽事件(其核心就是綁定mousemove),這種事件有一個特色,就是用戶沒必要特意搗亂,他在一個正常的操做中,都有可能在一個短的時間內觸發很是屢次事件綁定程序。而你們知道,DOM操做時很消耗性能的,這個時候,若是你爲這些事件綁定一些操做DOM節點的操做的話,那就會引起大量的計算,在用戶看來,頁面可能就一時間沒有響應,這個頁面一會兒變卡了變慢了。甚至在IE下,若是你綁定的resize事件進行較多DOM操做,其高頻率可能直接就使得瀏覽器崩潰。javascript

怎麼解決?函數節流就是一種辦法。話說第一次接觸函數節流(throttle),仍是在看impress源代碼的時候,impress在播放的時候,若是窗口大小發生改變(resize),它會對總體進行縮放(scale),使得每一幀都完整顯示在屏幕上:html

impress在resize的時候自動適應

稍微留心,你會發現,當你改變窗體大小的時候,無論你怎麼拉,怎麼拽,都沒有馬上生效,而是在你改變完大小後的一下子,它的內容才進行縮放適應。看了源代碼,它用的就是函數節流的方法。前端

函數節流,簡單地講,就是讓一個函數沒法在很短的時間間隔內連續調用,只有當上一次函數執行後過了你規定的時間間隔,才能進行下一次該函數的調用。以impress上面的例子講,就是讓縮放內容的操做在你不斷改變窗口大小的時候不會執行,只有你停下來一下子,纔會開始執行。java

 

函數節流的原理

函數節流的原理挺簡單的,估計你們都想到了,那就是定時器。當我觸發一個時間時,先setTimout讓這個事件延遲一會再執行,若是在這個時間間隔內又觸發了事件,那咱們就clear掉原來的定時器,再setTimeout一個新的定時器延遲一會執行,就這樣。chrome

 

代碼實現

明白了原理,那就能夠在代碼裏用上了,但每次都要手動去新建清除定時器畢竟麻煩,因而須要封裝。在《JavaScript高級程序設計》一書有介紹函數節流,裏面封裝了這樣一個函數節流函數:sublime-text

它把定時器ID存爲函數的一個屬性(= =我的的世界觀不喜歡這種寫法)。而調用的時候就直接寫瀏覽器

這樣兩次函數調用之間至少間隔100ms。性能優化

而impress用的是另外一個封裝函數:閉包

它使用閉包的方法造成一個私有的做用域來存放定時器變量timer。而調用方法爲app

兩種方法各有優劣,前一個封裝函數的優點在把上下文變量當作函數參數,直接能夠定製執行函數的this變量;後一個函數優點在於把延遲時間當作變量(固然,前一個函數很容易作這個拓展),並且我的以爲使用閉包代碼結構會更優,且易於拓展定製其餘私有變量,缺點就是雖然使用apply把調用throttle時的this上下文傳給執行函數,但畢竟不夠靈活。

 

接下來是?

接下來就討論怎麼更好地封裝?這多沒意思啊,接下來討論下怎樣拓展深化函數節流。

函數節流讓一個函數只有在你不斷觸發後停下來歇會纔開始執行,中間你操做得太快它直接無視你。這樣作就有點太絕了。resize通常還好,但假如你寫一個拖拽元素位置的程序,而後直接使用函數節流,那恭喜你,你會發現你拖動時元素是不動的,你拖完了,它直接閃到終點去。

其實函數節流的出發點,就是讓一個函數不要執行得太頻繁,減小一些過快的調用來節流。當你改變瀏覽器大小,瀏覽器觸發resize事件的時間間隔是多少?我不清楚,我的猜想是16ms(每秒64次),反正跟mousemove同樣很是太頻繁,一個很小的時間段內一定執行,這是瀏覽器設好的,你沒法直接改。而真正的節流應該是在可接受的範圍內儘可能延長這個調用時間,也就是咱們本身控制這個執行頻率,讓函數減小調用以達到減小計算、提高性能的目的。假如原來是16ms執行一次,咱們若是發現resize時每50ms一次也能夠接受,那確定用50ms作時間間隔好一點。

而上面介紹的函數節流,它這個頻率就不是50ms之類的,它就是無窮大,只要你能不間斷resize,刷個幾年它也一次都不執行處理函數。咱們能夠對上面的節流函數作拓展:

在這個拓展後的節流函數升級版,咱們能夠設置第三個參數,即必然觸發執行的時間間隔。若是用下面的方法調用

則意味着,50ms的間隔內連續觸發的調用,後一個調用會把前一個調用的等待處理掉,但每隔100ms至少執行一次。原理也很簡單,打時間tag,一開始記錄第一次調用的時間戳,而後每次調用函數都去拿最新的時間跟記錄時間比,超出給定的時間就執行一次,更新記錄時間。

狠擊這裏查看測試頁面

到如今爲止呢,當咱們在開發中遇到相似的問題,一個函數可能很是頻繁地調用,咱們有了幾個選擇:一呢,仍是用原來的寫法,頻繁執行就頻繁執行吧,哥的電腦好;二是用原始的函數節流;三則是用函數節流升級版。不是說第一種就很差,這要看實際項目的要求,有些就是對實時性要求高。而若是要求沒那麼苛刻,咱們能夠視具體狀況使用第二種或第三種方法,理論上第二種方法執行的函數調用最少,性能應該節省最多,而第三種方法則更加地靈活,你能夠在性能與體驗上探索一個平衡點。

 

你怎麼了,性能

(原諒我,寫得有點長 = = ,文章主體還剩最後這一節。)

咱們常常說我優化了代碼了,如今的代碼更高效了,但貌似不多有人去測試,性能是否真的提高了,提高了多少。固然,前端性能測試的不完善、不夠體系化也是緣由之一,但咱們也要有一種嚴謹的態度。上面介紹了三種方法,理論上來講呢,第一種方法執行的運算最多,性能理應最差(運算過多過頻,內存、cpu佔用高,頁面變卡),而第二種應該是性能最好,第三種就是一種居中的方案。

爲了給讀者一個更確切的分析,因而我對三種方法作了一次蛋疼的性能測試。。。我選擇的是拖拽一個頁面元素位置的應用場景,爲了讓性能優化更明顯一點,拖拽的是一個iframe,iframe裏面加載的是騰訊首頁(通常門戶網站的首頁都夠重量級的),這樣在拖拽的過程當中會不斷觸發瀏覽器的重繪。至於怎麼看性能,我打開的是chrome的調試面板的時間線標籤,裏面有memory監視。對於性能的評價標準,我選的是內存佔用。

因而長達兩三個小時的性能測試開始了。。。

 

很快我就發現,chrome的性能優化得太好了,個人第一種測試方案三種方法之間有性能差別,但這個差別實在不明顯,並且每一輪的測試都有波動,並且每次測試還很難保證測試的背景條件(如開始時的內存佔用狀況),第一組測試結果以下:

第一種方法:函數節流-首輪測試-第一種方法

第二種方法:函數節流-首輪測試-第二種方法

第三種方法:函數節流-首輪測試-第三種方法

能夠發現,這些小差別很難斷定哪一種方法更好。

 

因而有了新一輪測試。不夠重量化?好吧,我每次mousemove的處理函數中,都觸發iframe的從新加載;測試數據有瞬時波動?此次我一個測試測60秒,看一分鐘的整體狀況;測試條件不夠統一?我規定在60秒裏面mouse up 6次,其餘時間各類move。

因而有了第二組圖片(其實作了不少組圖片,這裏只選出比較有表明性的一組,其餘幾組相似)

第一種方法:函數節流-二輪測試-第一種方法

第二種方法:函數節流-二輪測試-第二種方法

第三種方法:函數節流-二輪測試-第三種方法

看錯了?我一開始也這麼認爲,但測試了幾回都發現,第一種方法正如預料中的佔資源,第二種方法居然不是理論上的性能最優,最優的是第三種方法!

仔細分析。第一種方法因爲不斷地mousemove,不斷更新位置的同時從新加載iframe的內容,因此內存佔用不斷增長。第二種方法,即原始的函數節流,能夠從截圖看出內存佔用有多處平坦區域,這是由於在mousemove的過程當中,因爲時間間隔短,不觸發處理函數,因此內存也就有一段平滑期,幾乎沒有增加,但在mouseup的時候就出現小高峯。第三種方法呢,因爲代碼寫了每200ms必須執行一次,因而就有很明顯的高峯週期。

爲何第三種方法會比第二種方法佔用內存更小呢?我的認爲,這跟內存回收有關,有可能chrmoe在這方面真的優化得太多(。。。)。不斷地每隔一個小時間段地新建定時器,使得內存一直得不到釋放。而使用第三種方法,從代碼結構能夠看出,當到了指定的mustRunDelay必須執行處理函數的時候,是不執行新建定時器的,便是說在當即執行以後,有那麼一小段時間空隙,定時器是被clear的,只有在下一次進入函數的時候纔會從新設置。而chrome呢,就趁這段時間間隙回收垃圾,因而每個小高峯後面都有一段瞬時的「下坡」。

固然,這只是個人推測,期待讀者有更獨到的見解。

重度測試頁面(我的測試的時候是沒有切換器的,每次代碼選了一種模式,而後就關閉瀏覽器,從新打開頁面來測試,以保證運行時不受到別的模式的影響。這裏提供的測試頁面僅供參考)

 

後語

(這是後語,不算正文的小節)

上面就是我對函數節流的認識和探索了,時間有限,探索得不夠深也寫得不夠好。我的建議,在實際項目開發中,若是要用到函數節流來優化代碼的話,函數節流升級版更加地靈活,且在一些狀況下內存佔用具備明顯的優點(我只試了chrome,只試了兩三個鍾,不敢妄言)。

最後咱們能夠整合了第2、三種方法,封裝成一個函數,其實第二種方法也就是第三種方法的特例而已。還能夠以hash對象封裝參數:執行函數、上下文、延遲、必須執行的時間間隔。這比較簡單就不在這裏貼出來了。

相關文章
相關標籤/搜索