什麼是函數節流?
函數節流簡單的來說就是不想讓該函數在很短的時間內連續被調用,比如我們最常見的是窗口縮放的時候,經常會執行一些其他的操作函數,比如發一個ajax請求等等事情,那麼這時候視窗縮放的時候,有可能連續發多個請求,這並不是我們想要的,或者是說我們常見的滑鼠移入移出tab切換效果,有時候連續且移動的很快的時候,會有閃爍的效果,這時候我們就可以使用函數節流來操作。大家都知道,DOM的操作會很消耗或影響效能的,如果是說在視窗縮放的時候,為元素綁定大量的dom操作的話,會引發大量的連續計算,比如在IE下,過多的DOM操作會影響瀏覽器效能,甚至在嚴重的情況下,會造成瀏覽器崩潰的發生。這時候我們就可以使用函數節流來最佳化程式碼了~
函數節流的基本原理:
使用定時器,先延時該函數的執行,例如使用setTomeout()這個函數延遲一段時間後執行函數,如果在該時間段內也觸發了其他事件,我們可以使用清除方法clearTimeout()來清除該定時器,再setTimeout()一個新的定時器延遲一會兒執行。
最近在某團隊忙於一個項目,有這麼一個頁面,採用傳統模式開發(吐槽它為什麼不用React),它的DOM操作比較多,然後性能是比較差的,尤其當你縮放窗口時,可怕的事情發生了,出現了卡頓,甚至瀏覽器癱瘓。為什麼呢?
由於該頁面的DOM操作非常多,故視窗縮放每一幀時都會觸發函數的執行,連續的重新DOM操作,這樣對瀏覽器的開銷是非常大的。既然在視窗縮放時,會讓瀏覽器重新計算DOM,那麼我們為什麼不可以讓DOM的計算延時呢,讓視窗停止縮放後才重新計,這樣不就節省了瀏覽器的開銷,達到優化的效果了嗎?
知識準備
1. setTimeout(code,millisec) 當然就是本文的主角了。
setTimeout() 方法用於在指定的毫秒數後呼叫函數或計算表達式。
code必需。要呼叫的函數後要執行的 JavaScript 程式碼字串。
millisec必需。執行程式碼前需等待的毫秒數。
提示:setTimeout() 只執行 code 一次。如果要多次調用,請使用 setInterval() 或讓 code 本身再次調用 setTimeout()。
廣泛應用於定時器,輪播圖,動畫效果,自動滾動等等。
2. clearTimeout(id_of_setTimeout)
參數 id_of_settimeout由 setTimeout() 傳回的 ID 值。該值標識要取消的延遲執行程式碼區塊。
3. fun.apply(thisArg[, argsArray])
apply() 方法在指定 this 值和參數(參數以數組或類別數組物件的形式存在)的情況下呼叫某個函數
該函數的語法與call()方法幾乎相同,唯一的區別在於,call()方法接受的是一個參數列表,而apply()接受的是一個包含多個參數數組的(或類別數組對象)。
參數
thisArg
在 fun 函數執行時指定的 this 值。要注意的是,指定的this 值不一定是該函數執行時真正的this 值,如果這個函數處於非嚴格模式下,則指定為null 或undefined 時會自動指向全域物件(瀏覽器中就是window對象),同時值為原始值(數字,字串,布林值)的this 會指向該原始值的自動包裝物件。
argsArray
一個陣列或類別數組對象,其中的數組元素將作為單獨的參數傳給 fun 函數。如果該參數的值為null 或 undefined,則表示不需要傳入任何參數。從ECMAScript 5 開始可以使用類別數組物件。
在呼叫一個存在的函數時,你可以為其指定一個 this 物件。 this 指當前對象,也就是正在呼叫這個函數的對象。 使用 apply, 你可以只寫一次這個方法然後在另一個物件中繼承它,而不用在新物件中重複寫該方法。
4. fun.call(thisArg[, arg1[, arg2[, ...]]])
該 方法在使用一個指定的this值和若干個指定的參數值的前提下呼叫某個函數或方法.
參數
thisArg
在fun函數運行時指定的this值。需要注意的是,指定的this值不一定是該函數執行時真正的this值,如果這個函數處於非嚴格模式下,則指定為null和undefined的this值會自動指向全域物件(瀏覽器中就是window物件),同時值為原始值(數字,字串,布林值)的this會指向該原始值的自動包裝物件。
arg1, arg2, ...
指定的參數清單。
當一個函數呼叫時,可以賦值一個不同的 this 物件。 this 引用目前對象,即 call 方法的第一個參數。透過 call 方法,你可以在一個物件上借用另一個物件上的方法,例如Object.prototype.toString.call([]),就是一個Array物件借用了Object物件上的方法。
作用:
使用call方法呼叫父建構子
使用call方法呼叫匿名函數
使用call方法呼叫匿名函數並且指定上下文的'this'
這裡插個題外話:
apply 與 call() 非常相似,不同之處在於提供參數的方式。 apply 使用參數數組而不是一組參數列表。 apply 可以使用陣列字面量(array literal),如fun.apply(this, ['eat', 'bananas']),或陣列對象, 如fun.apply(this, new Array('eat', 'bananas' ))。你也可以使用 arguments 物件作為 argsArray 參數。 arguments 是一個函數的局部變數。 它可以被用作被呼叫物件的所有未指定的參數。 這樣,你在使用apply函數的時候就不需要知道被呼叫物件的所有參數。 你可以使用arguments來把所有的參數傳遞給被呼叫物件。 被呼叫物件接下來就負責處理這些參數。
從 ECMAScript 第5版開始,可以使用任何種類的類別數組對象,就是說只要有一個 length 屬性和[0...length) 範圍的整數屬性。例如現在可以使用 NodeList 或一個自己定義的類似 {'length': 2, '0': 'eat', '1': 'bananas'} 形式的物件。
call, apply方法區別是,從第二個參數起, call方法參數將依次傳遞給借用的方法作參數, 而apply 直接將這些參數放到一個數組中再傳遞, 最後借用方法的參數列表是一樣的.
應用場景:當參數明確時可用call, 當參數不明確時可用apply給合arguments
現在先給一個例子
總所皆知,onscolll,onresize等是非常耗性能,窗口縮放時打印數字。
var count = ; window.onresize = function () { count++; console.log(count); }
在chrome瀏覽器中伸縮瀏覽器視窗大小,列印如下
這顯然不是我們想要的,那如果我們換成ajax請求的話,那麼就會縮放一次視窗會連續觸發多次ajax請求,下面我們試著使用函數節流的操作試試看;當然加個settimeout()的定時器就好了,
第一種封裝方法
var count = ; function oCount() { count++; console.log(count); } window.onresize = function () { delayFun(oCount) }; function delayFun(method, thisArg) { clearTimeout(method.props); method.props = setTimeout(function () { method.call(thisArg) }, ) }
第二種封裝方法
建構一個閉包,使用閉包的方式形成一個私有的作用域來存放定時器timer, timer是透過傳遞參數的形式引入的。
var count = ; function oCount() { count++; console.log(count); } var funs= delayFun(oCount,); window.onresize = function () { funs() }; function delayFun(func, wait) { var timer = null; return function () { var context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function () { func.apply(context, args); }, wait) }; }
對第二種方法最佳化一下,效能會更好
這裡返回一個函數,如果它被不間斷地調用,它將不會被執行。該函數在停止呼叫 N 毫秒後,再次呼叫它才會執行。如果有傳遞 ‘immediate' 參數,則會馬上將函數排程到執行佇列中,而不會延遲。
function delayFun (func, wait, immediate) { var timeout; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; }; // 用法 var myEfficientFn = delayFun (function() { // 所有繁重的操作 }, ); window.addEventListener('resize', myEfficientFn);
函數不允許回呼函數在指定時間內執行多於一次。當為一個會頻繁觸發的事件指派一個回呼函數時,該函數顯得特別重要。
setTimeout這麼厲害,那我們是可以在專案中大量使用嗎?
我個人是不建議的,在我們業務中,基本上是禁止在業務邏輯中使用setTimeout的,因為我所看到的很多使用方式都是一些問題好解決,setTimeout作為一個hack的方式。
例如,當一個實例還沒有初始化的前,我們就使用這個實例,錯誤的解決方法是使用實例時加個setTimeout,確保實例先初始化。
為什麼錯誤?這裡其實就是用hack的手段
第一是埋下了坑,打亂模組的生命週期
第二是出現問題時,setTimeout其實是很難調試的。
我認為正確的使用方式是,看看生命週期(可參考《關於軟體的生命週期 》),把實例化提到使用前執行。
有關JS中setTimeout的巧妙用法前端函數節流,小編就給大家介紹到這裡,希望對大家有幫助!