JS中的函數節流throttle詳解和優化
在前端開發中,有時會為頁面綁定resize事件,或者為一個頁面元素綁定拖拽事件(mousemove),這種事件有一個特點,在一個正常的操作中,有可能在一個短的時間內觸發非常多次事件綁定程序。
DOM操作時很消耗性能的,如果你為這些事件綁定一些操作DOM節點的操作的話,那就會引發大量的計算,在用戶看來,頁面可能就一時間沒有響應,這個頁面一下子變卡了變慢了。在IE下,如果你綁定的resize事件進行較多DOM操作可能直接就崩潰了。
怎么解決?函數節流(throttle)就是一種辦法。
函數節流的原理
某些代碼不可以在沒有間斷的情況連續重復執行。
第一次調用函數,創建一個定時器,在指定的時間間隔之后運行代碼。
當第二次調用該函數時,它會清除前一次的定時器並設置另一個。
如果前一個定時器已經執行過了,這個操作就沒有任何意義。
然而,如果前一個定時器尚未執行,其實就是將其替換為一個新的定時器。
目的是只有在執行函數的請求停止了一段時間之后才執行。
簡單點說就是:當觸發一個事件時,先setTimout讓這個事件延遲一會再執行,如果在這個時間間隔內又觸發了事件,那我們就clear掉原來的定時器,再setTimeout一個新的定時器延遲一會執行。
實際使用過程中考慮到用戶體驗問題,會在這個基礎上做一些優化。
一個簡單實現的例子:
函數實現方式:
function throttle(method, context) { clearTimeout(methor.tId); method.tId = setTimeout(function(){ method.call(context); }, 100); }
調用方法:
window.onresize = function(){ throttle(myFunc); }
---------------
閉包實現方式:
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); }; };
調用方法:
window.onresize = throttle(myFunc, 50);
-------------
上面的函數節流有個問題是:只要你在50ms內不間斷resize,myFunc就一次都不執行處理函數。
對上面的節流函數做拓展:
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至少執行一次。
一開始記錄第一次調用的時間戳,然后每次調用函數都去拿最新的時間跟記錄時間比,超出給定的時間就執行一次,更新記錄時間。
-----------------
例子場景2:實現常見的搜索功能
1.沒有使用函數節流的情況下,為input綁定keyup事件處理函數,在控制台輸出我輸入的內容。
每按下一個鍵盤鍵,就輸出了一次。短短的一些內容,輸出了15次,如果每一次都是一次ajax查詢請求的話就發了14個請求了。在性能上的消耗可想而知。
假如我不斷地輸入,輸入了很多內容,但是我每兩次之間的輸入間隔都小於自己設置的delay值,那么,這個queryData搜索函數就一直得不到調用。實際上,我們更希望的是,當達到某個時間值時,一定要執行一次這個搜索函數。所以,就有了函數節流的改進模式。
函數節流增強版
HTML:
<input id="search" type="text" name="search">
JS:
<script> function queryData(text){ console.log("搜索:" + text); } var input = document.getElementById("search"); input.addEventListener("keyup", function(event){ throttle(queryData, null, 500, this.value,1000); // throttle(queryData, null, 500, this.value); // queryData(this.value); }); function throttle(fn,context,delay,text,mustApplyTime){ clearTimeout(fn.timer); fn._cur=Date.now(); //記錄當前時間 if(!fn._start){ //若該函數是第一次調用,則直接設置_start,即開始時間,為_cur,即此刻的時間 fn._start=fn._cur; } if(fn._cur-fn._start>mustApplyTime){ //當前時間與上一次函數被執行的時間作差,與mustApplyTime比較,若大於,則必須執行一次函數,若小於,則重新設置計時器 fn.call(context,text); fn._start=fn._cur; }else{ fn.timer=setTimeout(function(){ fn.call(context,text); },delay); } } </script>
顯然,連續的輸入,到一定時間間隔之后,queryData函數必然會被調用,但是又不是頻繁的調用。達到了節流的目的,又不會影響用戶體驗。
進一步的優化可以在調用throttle函數之前,先對輸入的內容進行判斷,若其值為空、值不變不調用。