淺談javascript的函式節流
什麼是函式節流?
介紹前,先說下背景。在前端開發中,有時會為頁面繫結resize事件,或者為一個頁面元素繫結拖拽事件(其核心就是繫結mousemove),這種事件有一個特點,就是使用者不必特地搗亂,他在一個正常的操作中,都有可能在一個短的時間內觸發非常多次事件繫結程式。而大家知道,DOM操作時很消耗效能的,這個時候,如果你為這些事件繫結一些操作DOM節點的操作的話,那就會引發大量的計算,在使用者看來,頁面可能就一時間沒有響應,這個頁面一下子變卡了變慢了。甚至在IE下,如果你繫結的resize事件進行較多DOM操作,其高頻率可能直接就使得瀏覽器崩潰。
怎麼解決?函式節流就是一種辦法。話說第一次接觸函式節流(throttle),還是在看impress原始碼的時候,impress在播放的時候,如果視窗大小發生改變(resize),它會對整體進行縮放(scale),使得每一幀都完整顯示在螢幕上:
稍微留心,你會發現,當你改變窗體大小的時候,不管你怎麼拉,怎麼拽,都沒有立刻生效,而是在你改變完大小後的一會兒,它的內容才進行縮放適應。看了原始碼,它用的就是函式節流的方法。
函式節流,簡單地講,就是讓一個函式無法在很短的時間間隔內連續呼叫,只有當上一次函式執行後過了你規定的時間間隔,才能進行下一次該函式的呼叫。以impress上面的例子講,就是讓縮放內容的操作在你不斷改變視窗大小的時候不會執行,只有你停下來一會兒,才會開始執行。
函式節流的原理
函式節流的原理挺簡單的,估計大家都想到了,那就是定時器。當我觸發一個時間時,先setTimout讓這個事件延遲一會再執行,如果在這個時間間隔內又觸發了事件,那我們就clear掉原來的定時器,再setTimeout一個新的定時器延遲一會執行,就這樣。
程式碼實現
明白了原理,那就可以在程式碼裡用上了,但每次都要手動去新建清除定時器畢竟麻煩,於是需要封裝。在《JavaScript高階程式設計》一書有介紹函式節流,裡面封裝了這樣一個函式節流函式:
function throttle(method, context) { clearTimeout(methor.tId); method.tId = setTimeout(function(){ method.call(context); }, 100); }
它把定時器ID存為函式的一個屬性(= =個人的世界觀不喜歡這種寫法)。而呼叫的時候就直接寫
window.onresize = function(){ throttle(myFunc); }
這樣兩次函式呼叫之間至少間隔100ms。
而impress用的是另一個封裝函式:
var throttle = function(fn, delay){ var timer = null; return function(){ var context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function(){ fn.apply(context, args); }, delay); }; };
它使用閉包的方法形成一個私有的作用域來存放定時器變數timer。而呼叫方法為
window.onresize = throttle(myFunc, 100);
兩種方法各有優劣,前一個封裝函式的優勢在把上下文變數當做函式引數,直接可以定製執行函式的this變數;後一個函式優勢在於把延遲時間當做變數(當然,前一個函式很容易做這個擴充),而且個人覺得使用閉包程式碼結構會更優,且易於擴充定製其他私有變數,缺點就是雖然使用apply把呼叫throttle時的this上下文傳給執行函式,但畢竟不夠靈活。
接下來是?
接下來就討論怎麼更好地封裝?這多沒意思啊,接下來討論下怎樣擴充深化函式節流。
函式節流讓一個函式只有在你不斷觸發後停下來歇會才開始執行,中間你操作得太快它直接無視你。這樣做就有點太絕了。resize一般還好,但假如你寫一個拖拽元素位置的程式,然後直接使用函式節流,那恭喜你,你會發現你拖動時元素是不動的,你拖完了,它直接閃到終點去。
其實函式節流的出發點,就是讓一個函式不要執行得太頻繁,減少一些過快的呼叫來節流。當你改變瀏覽器大小,瀏覽器觸發resize事件的時間間隔是多少?我不清楚,個人猜測是16ms(每秒64次),反正跟mousemove一樣非常太頻繁,一個很小的時間段內必定執行,這是瀏覽器設好的,你無法直接改。而真正的節流應該是在可接受的範圍內儘量延長這個呼叫時間,也就是我們自己控制這個執行頻率,讓函式減少呼叫以達到減少計算、提升效能的目的。假如原來是16ms執行一次,我們如果發現resize時每50ms一次也可以接受,那肯定用50ms做時間間隔好一點。
而上面介紹的函式節流,它這個頻率就不是50ms之類的,它就是無窮大,只要你能不間斷resize,刷個幾年它也一次都不執行處理函式。我們可以對上面的節流函式做擴充:
var throttleV2 = function(fn, delay, mustRunDelay){ var timer = null; var t_start; return function(){ var context = this, args = arguments, t_curr = +new Date(); clearTimeout(timer); if(!t_start){ t_start = t_curr; } if(t_curr - t_start >= mustRunDelay){ fn.apply(context, args); t_start = t_curr; } else { timer = setTimeout(function(){ fn.apply(context, args); }, delay); } }; };
在這個擴充後的節流函式升級版,我們可以設定第三個引數,即必然觸發執行的時間間隔。如果用下面的方法呼叫
window.onresize = throttleV2(myFunc, 50, 100);
則意味著,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,只試了兩三個鍾,不敢妄言)。
最後我們可以整合了第二、三種方法,封裝成一個函式,其實第二種方法也就是第三種方法的特例而已。還可以以hash物件封裝引數:執行函式、上下文、延遲、必須執行的時間間隔。這比較簡單就不在這裡貼出來了。
相關文章
- 淺談js函式節流和函式防抖JS函式
- 淺談JavaScript的防抖與節流JavaScript
- 淺談 JavaScript 中的防抖與節流(一)JavaScript
- javascript函式節流是什麼JavaScript函式
- JavaScript中函式防抖、節流JavaScript函式
- 淺聊函式防抖與節流函式
- javascript之函式防抖與節流JavaScript函式
- 淺談Swift中的函式式Swift函式
- 淺談生成函式函式
- 淺談eval函式函式
- 淺談Kotlin中的函式Kotlin函式
- 函式節流與函式防抖函式
- 函式防抖和函式節流函式
- 函式的防抖和節流函式
- 函式的防抖與節流函式
- 理解Underscore中的節流函式函式
- 區分函式防抖&函式節流函式
- JS函式節流和函式防抖JS函式
- 淺談尤拉函式函式
- 【進階 6-3 期】深入淺出節流函式 throttle函式
- 函式防抖和節流函式
- 函式節流和防抖函式
- JS函式節流,去抖JS函式
- 節流函式怎麼寫?函式
- 淺談JavaScript正規表示式JavaScript
- 淺談php count()函式方法PHP函式
- 淺談C語言中函式的使用C語言函式
- 深入理解函式節流與函式防抖函式
- js 函式防抖和節流JS函式
- JS專題之節流函式JS函式
- js防抖 和節流函式JS函式
- js函式中的節流和防抖JS函式
- requestAnimationFrame實現一幀的函式節流requestAnimationFrame函式
- 淺談匿名函式和閉包函式
- 【js】什麼是函式節流與函式去抖JS函式
- 一圖秒懂函式防抖和函式節流函式
- JavaScript淺析 -- 定時器和節流防抖JavaScript定時器
- 淺談JavaScriptJavaScript
- 淺談JavaScript中的thisJavaScript