在平常開發或者面試中,防抖與節流應該都是屬於高頻出現的點。這篇文章主要是基於冴羽(後續用他代稱)大神的兩篇文章 防抖 與 節流來寫的。由於本身在看他文章的時候也對其中的代碼產生了一些困惑,有一些卡住的地方,因此想把本身遇到的問題都拋出來,一步步的去理解。 文中具體的場景demo以他的爲例,就不單獨在舉場景例子了。git
持續觸發事件不執行,等到事件中止觸發後n秒纔去執行函數。github
// 初版
const debounce = function(func, delay) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, delay);
}
}
複製代碼
初版沒什麼難點,當用戶持續觸發就一直清除計時器,當他最後一次觸發後,會生成一個計時器,同時計時器中的方法將在delay
秒執行。面試
新增需求:不等到事件中止觸發後才執行,但願當即執行函數。而後等到中止觸發n秒後,才從新觸發執行。bash
先來拆分需求:app
當即執行函數很容易實現func.apply(context, args)
便可。可是不可能當用戶持續觸發的時候一直去調用func
這個函數,因此這裏想到須要一個字段來判斷什麼時候可以去執行func
函數。ide
// 第二版
const debounce = function (func, delay) {
let timer,
callNow = true; // 是否當即執行函數的標識
return function () {
const context = this;
const args = arguments;
if (timer) clearTimeout(timer);
if(callNow) {
func.apply(context, args); // 觸發事件當即執行
callNow = false; // 將標識設置爲false,保證後續在delay秒內觸發事件都沒法執行函數。
} else {
timer = setTimeout(() => {
callNow = true; // 過delay秒後才能再次觸發函數執行。
}, delay)
}
}
}
複製代碼
新增需求:加個immediate
參數來判斷是否馬上執行。函數
其實經過上面那個簡化版,此次加個參數字段來區分就很好實現了。優化
const debounce2 = function (func, delay, immediate = false) {
let timer,
callNow = true;
return function () {
const context = this;
const args = arguments;
if (timer) clearTimeout(timer);
if (immediate) {
if(callNow) func.apply(context, args); // 觸發事件當即執行
callNow = false;
timer = setTimeout(() => {
callNow = true; // 過n秒後才能再次觸發函數執行。
}, delay)
} else {
timer = setTimeout(() => {
func.apply(context, args);
}, delay)
}
}
}
複製代碼
getUserAction
函數多是有返回值的,因此這裏也須要返回函數的結果。但當immediate
爲false
的時候,由於setTimeout
的緣故,在最後return
的時候值會一直是undefined
。因此只在immediate
爲true
的時候返回函數的執行結果。ui
const getUserAction = function(e) {
this.innerHTML = count++;
return 'Function Value';
}
const debounce = function (func, delay, immediate = false) {
let timer,
result,
callNow = true;
return function () {
const context = this;
const args = arguments;
if (timer) clearTimeout(timer);
if (immediate) {
if(callNow) result = func.apply(context, args);
callNow = false;
timer = setTimeout(() => {
callNow = true; // 過n秒後才能再次觸發函數執行。
}, delay)
} else {
timer = setTimeout(() => {
func.apply(context, args);
}, delay)
}
return result;
}
}
// demo test
const setUseAction = debounce(getUserAction, 2000, true);
// 展現函數返回值
box.addEventListener('mousemove', function (e) {
const result = setUseAction.call(this, e);
console.log('result', result);
})
複製代碼
但願可以取消
debounce
函數,可讓用戶執行此方法(cancel)後,取消防抖,當用戶再次去觸發時,就能夠又馬上執行了。this
需求思考:取消防抖,其實說白了就是清除掉以前存在的計時器。這樣當用戶再次觸發的時候就能馬上執行函數啦。嘿嘿😝是否是很簡單啊!
const debounce = function (func, delay, immediate = false) {
let timer,
result,
callNow = true;
const debounced = function () {
const context = this;
const args = arguments;
if (timer) clearTimeout(timer);
if (immediate) {
if(callNow) result = func.apply(context, args);
callNow = false;
timer = setTimeout(() => {
callNow = true; // 過n秒後才能再次觸發函數執行。
}, delay)
} else {
timer = setTimeout(() => {
func.apply(context, args);
}, delay)
}
return result;
};
debounced.cancel = function(){
clearTimeout(timer);
timer = null;
}
}
複製代碼
通過這樣的一系列拆分是否是頓時以爲防抖也就那麼回事嘛,並無多難~
節流的兩種主流實現方式:1.時間戳; 2.設置定時器。
觸發事件時,取出當前的時間戳,而後減去以前的時間戳(最開始設置爲0)。若大於設置的時間週期,則執行函數,同時更新時間戳爲當前的時間戳。若小於,則不執行。
const throttle = function(func, delay) {
let prev = 0; // 將初始的時間戳設爲0,保證第一次觸發就必定執行函數
return function(){
const context = this;
const args = arguments;
const now = +new Date();
if (now - prev > delay) {
func.apply(context, args);
prev = now;
}
}
}
複製代碼
每過delay
秒會執行一次函數,可是當最後一次觸發的時間少於delay
,則now - prev < delay
,致使最後一次觸發並無執行函數。
觸發事件時,設置一個定時器。當再次觸發事件時,若定時器存在就不執行;直到定時器內部方法執行完,而後清空定時器,設置下一個定時器。
const throttle = function(func, delay){
let timer;
return function(){
const context = this;
const args = arguments;
if (!timer) {
timer = setTimeout(() => {
timer = null; // delay秒重置timer值爲null,爲了從新設置一個新的定時器。
func.apply(context, args);
}, delay);
}
}
}
複製代碼
當首次觸發事件的時候不會執行函數。
這版要實現兩個需求:
這裏先貼下他的代碼。
說實話剛看到這段代碼的時候我本身也是懵的,後面仔細思考了一下子才徹底想通。這邊我將本身如何理解這段代碼的思路寫下來,幫助你們層層實現這個需求。
先看第二個需求(中止觸發事件後依然再執行一次事件),其實說白了就是延遲執行事件,此時我就會先想到這塊要用上setTimeout
。可是有一個問題在於setTimeout
的第二個參數延遲多少秒後觸發呢?假設每3s執行一次函數,執行了3次,我在第9.5的時候中止觸發事件。那麼後續將要過多少秒才能執行這最後一次觸發對應的事件呢?(12 - 9.5 = 2.5s)
// 僞代碼片斷以下
const throttle1 = function(func, delay){
let timer,
prev = 0;
return function(){
const context = this;
const args = arguments;
const now = +new Date();
const remaining = delay - (now - prev); // 關鍵點:剩餘時間
// 設置!timer條件是爲了防止在已有定時器的狀況下,再次觸發事件又去生成一個新的定時器。
if (remaining > 0 && !timer) {
timer = setTimeout(() => {
prev = +new Date();
timer = null;
func.apply(context, args);
}, remaining)
}
}
}
複製代碼
再來看第一個需求(首次觸發事件當即執行),想要首次觸發只須要將prev
設爲0,這樣就能確保在第一次的時候delay - (now - prev)
的值必定是小於0的。
// 僞代碼片斷以下
const throttle2 = function(func, delay){
let timer,
prev = 0;
return function(){
const context = this;
const args = arguments;
const now = +new Date();
const remaining = delay - (now - prev); // 關鍵點:下次觸發 func 剩餘時間
// 設置!timer條件是爲了在已有定時器的狀況下,再次觸發事件又去新生成了一個定時器。
if (remaining <= 0) {
// 這段代碼的實際意義?
if (timer) {
clearTimeout(timer);
timer = null;
}
prev = now;
func.apply(context, args);
}
}
}
複製代碼
完整版本
const throttle = function(func, delay) {
let timer,
prev = 0;
return function(){
const context = this;
const args = arguments;
const now = +new Date();
const remaining = delay - (now - prev);
if (remaining <= 0) {
prev = now;
func.apply(context, args);
} else if(!timer) {
timer = setTimeout(() => {
prev = +new Date();
timer = null;
func.apply(context, args);
}, remaining)
}
}
}
複製代碼
如今基於上面兩段代碼來模擬操做下(假設delay值爲3):
remaining
值小於0,直接執行func
函數同時更新prev
的值(prev = now
)。remaining
值爲2且timer
值爲undefined
。此時會設置一個定時器(2s後執行),定時器中的代碼將會在2s後執行(更新prev
值;執行func
函數;重置timer
的值)。remaining
值爲1且timer
有值,此時不會走進任何分支,即不會發生任何事情。remaining
值爲0且timer
值爲null,此時更新prev
的值,將timer
設置爲null且執行func
函數。remaining
值爲1且timer
值爲null,這個時候又會重複上面 過1s後觸發 的步驟,生成一個新的定時器,定時器中的代碼將在2s後執行。remaining
值爲2.8且timer
值爲null,生成一個新的定時器,而且定時器中的代碼將在2.8s後執行。不知道你們會不會有這樣的疑問,我9.2s時中止觸發了,而後我10s的時候又再次觸發那會不會多產生新的定時器呢? 其實這個操做和上面的第二步與第三步相似,當10s再次觸發的時候,雖然remaining
的值爲2,可是此時timer
是有值的,因此並不會進入任何一條分支,即不會發生任何事。
不知道通過我這一拆分講解,各位觀衆老爺有沒有對上面截圖的代碼更清晰了一點呢😊?
有時候但願無頭有尾或者有尾無頭。經過設置options做爲第三個參數,而後根據傳的值進行判斷想要的效果。leading:false 表示禁用第一次執行; trailing:false 表示禁用中止觸發的回調。
老規矩先看下他的代碼,當初剛看這版代碼的時候我產生了以下幾點疑問:。
later
函數中,不直接寫previous = new Date().getTime()
,而寫成previous =options.leading === false ? 0 : new Date().getTime()
呢?;if (!timeout) context = args = null
這段代碼呢?if (timeout) {
clearTimeout(timeout);
timeout = null;
}
複製代碼
先將需求拆分下,先來看看設置leading = false
如何實現禁用第一次執行的。這裏能夠想到致使首次觸發就執行的關鍵就在於remaining
的值小於0,那麼其實只要想辦法在首次觸發的時候保證remaining
的值大於0就好啦!(將prev的初始值設置等於now的值便可)
const throttle = function(func, delay, option = {}) {
let timer,
prev = 0;
return function(){
const context = this;
const args = arguments;
const now = +new Date();
// 首次觸發時將prev值設置等於now值,禁止首次觸發執行函數
if (!prev && option.leading === false) {
prev = now; // 確保首次觸發時remaining的值大於0.
}
const remaining = delay - (now - prev);
if (remaining <= 0) {
prev = now;
func.apply(context, args);
} else if(!timer) {
timer = setTimeout(() => {
prev = option.leading === false ? 0 : +new Date(); // 這裏爲何這樣作,下面會解釋到。
timer = null;
func.apply(context, args);
}, remaining)
}
}
}
複製代碼
再看trailing = false
是如何禁用中止觸發的回調。一樣思考下致使中止觸發後還會再一次執行的緣由在哪?其實就在於remaining
的值是大於0,當它大於0時,就會去產生一個計時器,從而致使就算中止了觸發仍然能在remaining
秒後執行函數。因此只須要在產生計時器代碼的條件判斷上加上option.trailing !== false
就能夠禁止中止觸發的回調啦。
const throttle = function(func, delay, option = {}) {
let timer,
prev = 0;
return function(){
const context = this;
const args = arguments;
const now = +new Date();
if (!prev && option.leading === false) {
prev = now;
}
const remaining = delay - (now - prev);
if (remaining <= 0) {
prev = now;
func.apply(context, args);
// 當option.trailing值被設置爲false時,永遠走不進這條分支,也就不會產生計時器。
} else if(!timer && option.trailing !== false) {
timer = setTimeout(() => {
prev = option.leading === false ? 0 : +new Date();
timer = null;
func.apply(context, args);
}, remaining)
}
}
}
複製代碼
爲何要將prev = option.leading === false ? 0 : +new Date()
,而不是prev = +new Date()
。其實關鍵點在於當prev = 0
時,觸發事件時就必定會執行if(!pre && option.leading === false) prev = now
這段代碼,進而可以確保remaining
的值恆大於0,即用戶無論下一次是何時再次觸發事件時,都能保證代碼走到else if
這條分支。舉個場景解釋下(delay爲3s)~
remaining
值大於0,因此會產生一個定時器且3秒後執行定時器內部代碼。prev = +new Date()
,同時假設用戶過了10s後再次去觸發事件,由於如今prev
有值,且deay - (now - prev)
少於0(由於這時now-prev的值爲10,大於3),因此會走入if(remaining <= 0)
分支,這個時候就會當即執行func
函數。這樣就不符合需求所說的首次觸發(注意這裏的首次觸發並不僅是指第一次觸發,若是後續離開了觸發區域,過段時間再去觸發,也仍是被看成了首次觸發。這個點必定要明白)不執行函數啦。prev = option.leading === false ? 0 : +new Date()
,過10s後prev
的值早已經爲0,這時用戶再次去觸發事件,會執行prev = now
這段代碼,因此此時能確保remaining
的值大於0,這樣就可以保證用戶再次首次觸發事件時不會執行函數啦。而是生成一個定時器,3s後執行定時器中的方法。將context = args = null
主要是爲了釋放內存,由於JavaScript
有自動垃圾收集機制,會找出那些再也不繼續使用的值,而後釋放掉其佔用的內存。垃圾收集器每隔固定的時間段就會執行一次釋放操做。
其實這一點我到如今也不是很肯定。我的猜測這樣作是爲了防止定時器中的代碼timeout = null
並無在指定時間內馬上執行(即timeout仍有值),感受這段代碼就是處理這種極端情況下的,可以確保timeout
的值必定會被置爲null。
以上就是我對於防抖與節流的理解。接下來會出一篇 防抖與節流實戰篇。 但願你們能在評論區中一塊兒討論起來,有任何好的idea也能夠拋出來哦😬~