利用JavaScript實現(xiàn)防抖節(jié)流函數(shù)的示例代碼
最近在看紅樓夢,看的詩詞多了,時不時的也想來一句...
這幾天剛看看到了underscore.js
的防抖和節(jié)流的部分,正好又去復(fù)習(xí)了這部分內(nèi)容,于是又重新整理一下相關(guān)的知識點。
在開發(fā)中我們經(jīng)常會遇到一些高頻操作,比如:鼠標移動,滑動窗口,鍵盤輸入等等,節(jié)流和防抖就是對此類事件進行優(yōu)化,降低觸發(fā)的頻率,以達到提高性能的目的。
可以看到短短的幾秒鐘,觸發(fā)的事件的次數(shù)是非常驚人的。
防抖
簡單來說防抖就是無論觸發(fā)多少次事件,但是我一定在事件觸發(fā)后 n 秒后才執(zhí)行,也就是最后一次觸發(fā)完畢 n 秒后才執(zhí)行,如果在 n 秒前又觸發(fā)了,那么以新的事件的時間為準,重新開始計算時間。
那么如何實現(xiàn)一個基本的防抖函數(shù)呢?
基本實現(xiàn)
根據(jù)防抖的原理可知,我們可以設(shè)置一個定時器,當每次觸發(fā)事件但是沒有到達設(shè)置的時間時,都會重新設(shè)置定時器。
const debounce = function(func, wait) { let timeout return function() { // 再次觸發(fā)事件則刪除上一個定時器,重新設(shè)置 clearTimeout(timeout) timeout = setTimeout(func, wait); } }
這樣我們就寫出了一個最基本版的防抖函數(shù)。可以看到觸發(fā)次數(shù)已經(jīng)大大降低。
this & arguments
盡管上面已經(jīng)實現(xiàn)了一個基本的防抖函數(shù),但是依然是不完善的,比如在setTimeout
中的this
指向是無法正確的獲取的,setTimeout
中的this
指向 Window
對象!
我們可以在執(zhí)行定時器之前進行重置this
:
const debounce = function(func, wait) { let timeout return function() { // 保存this let context = this // 新增 clearTimeout(timeout) timeout = setTimeout(function() { func.apply(context) // 新增 }, wait); } }
再比如我們?nèi)绾卧谧远x的函數(shù)進行傳參呢,如果我們想在func
函數(shù)中傳遞event
對象,目前的實現(xiàn)顯然是無法正確進行獲取參數(shù)的,再來修改一下:
const debounce = function(func, wait) { let timeout return function() { let context = this // 新增 // 保存參數(shù) let args = arguments // 新增 clearTimeout(timeout) timeout = setTimeout(function() { func.apply(context, args) // 修改 }, wait); } }
至此一個基本的防抖函數(shù)就已經(jīng)實現(xiàn)了,這個函數(shù)已經(jīng)很是非常完善了。
立即執(zhí)行
接下來再增加一個功能,如果我們不希望非要等到事件停止觸發(fā)后才執(zhí)行,希望立刻執(zhí)行函數(shù),然后等到停止觸發(fā) n 秒后,才重新觸發(fā)執(zhí)行。
那么這個功能怎么做呢,其實可以這樣想,我們可以傳入一個參數(shù)immediate
,代表是否想要立即執(zhí)行,如果傳遞了immediate
,則立即執(zhí)行一次函數(shù),然后設(shè)置一個定時器,時間截止后將定時器設(shè)置為null
,下次進入函數(shù)時先判斷定時器是否為null
,然后決定是否再次執(zhí)行。
const debounce = function(func, wait, immediate) { let res, timeout, context, args; const debounced = function() { context = this args = arguments // 如果已經(jīng)設(shè)置了setTimeout,則重新進行設(shè)置 if(timeout) clearTimeout(timeout) // 判斷是否為立即執(zhí)行 if(immediate) { let runNow = !timeout // 設(shè)置定時器,指定時間后設(shè)置為null timeout = setTimeout(function() { timeout = null }, wait) // 如果timeout已經(jīng)為null(已到期),則執(zhí)行函數(shù) // 保存執(zhí)行結(jié)果,用于函數(shù)返回 if(runNow) res = func.apply(context, args) } else { // 如果沒有設(shè)置立即執(zhí)行,則設(shè)置定時器 timeout = setTimeout(function() { func.apply(context, args) }, wait) } return res } return debounced }
其實上面的實現(xiàn)是兩種完全不同的觸發(fā)方式,先來看一下流程圖:
黑色箭頭為觸發(fā)動作,紅色箭頭為執(zhí)行動作。
非立即執(zhí)行
立即執(zhí)行
來看一下執(zhí)行流程: 首先如果immediate
為true的情況:
第一次執(zhí)行:timeout
為null
,則runNow
為true
,然后設(shè)置一個定時器,在指定的時間后設(shè)置timeout
為null
,這也就代表設(shè)置執(zhí)行的間隔時間,最后判斷runNow
是否執(zhí)行函數(shù)。
第二次執(zhí)行:
- 情況一:已超過設(shè)置時間:如果第二次觸發(fā)執(zhí)行已經(jīng)超過設(shè)置的時間,此時
timeout
已經(jīng)被定時器設(shè)置為null
,那么進入debounced
函數(shù)后,runNow
為true
,重新設(shè)置定時器,然后執(zhí)行函數(shù)。 - 情況二:未超過設(shè)置時間:因為沒有超過設(shè)置時間,所以
timeout
并未被定時器設(shè)置為null
,那么runNow
為false
,由于timeout
的定時器已經(jīng)被清除,所以重置定時器,不會執(zhí)行函數(shù)。
再來看一下immediate
為false
的情況:
其實這種情況和我們之前設(shè)置的是一樣的,沒有超過設(shè)置時間,則重置定時器,定時器在到達指定時間后自動執(zhí)行一次函數(shù)。
兩者之間最大的區(qū)別是:立即執(zhí)行的功能會在第一次觸發(fā)函數(shù)的時候執(zhí)行一次,下次觸發(fā)如果已到達設(shè)置時間,則直接執(zhí)行一次。而非立即執(zhí)行的功能第一次觸發(fā)函數(shù)時只會設(shè)置一個定時器,時間到達后自動執(zhí)行,如果在設(shè)置時間內(nèi)觸發(fā)只會重置定時器,永遠不會立即執(zhí)行函數(shù)。
取消
再增加一個需求:如果想要取消debounce
函數(shù)怎么辦,比如 debounce
的時間間隔是 10 秒鐘,immediate
為 true
,這樣只有等 10 秒后才能重新觸發(fā)事件,如果有一個取消功能,點擊后取消防抖,再去觸發(fā),就可以立刻執(zhí)行了。
debounced.cancel = function() { // 刪除定時器 clearTimeout(timeout); // 設(shè)置timeout為null timeout = null; };
只需要將定時器清除,設(shè)置timeout
為null
即可,因為如果immediate
為 true
會直接執(zhí)行一次函數(shù),然后重新設(shè)置定時器
完整實現(xiàn)
最后完整的防抖函數(shù)如下:
function debounce(func, wait, immediate) { let res, timeout, context, args; const debounced = function () { context = this; args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { var runNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (runNow) res = func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } return res; }; debounced.cancel = function() { clearTimeout(timeout); timeout = null; }; return debounced; }
節(jié)流
節(jié)流也是用于減少觸發(fā)執(zhí)行的手段之一,但是思路和防抖是完全不一樣的,
如果持續(xù)觸發(fā)事件,每隔一段時間,只執(zhí)行一次事件。也就是只按照設(shè)置的時間作為時間段,到達指定的時間后觸發(fā)函數(shù)就會執(zhí)行。沒有到達指定的時間,無論如何觸發(fā)函數(shù)都不會執(zhí)行。
也就是沒到點,無論你怎么撩,我都巋然不動
目前有兩種實現(xiàn)方式:使用時間戳和設(shè)置定時器。
時間戳
當觸發(fā)函數(shù)的時候,使用當前的時間戳與上一次觸發(fā)函數(shù)所保存的時間戳相減,然后對比設(shè)置定時器的時間,決定是否執(zhí)行函數(shù)。
const throttle = function(func, wait) { let previous = 0, context, args; return function() { context = this args = arguments // 獲取當前時間戳 let now = +new Date() // 判斷當前時間戳與上一次觸發(fā)的時間差值是否大于等于指定時間 if((now - previous) >= wait) { func.apply(context, args) // 更新時間戳 previous = now } } }
值得注意的是:js中可以在某個元素前使用 '+' 號,這個操作是將該元素轉(zhuǎn)換成Number
類型,如果轉(zhuǎn)換失敗,那么將得到 NaN
。
+new Date()
將會調(diào)用 Date.prototype
上的 valueOf()
方法,根據(jù)MDN,Date.prototype.value
方法等同于Date.prototype.getTime()
。
console.log(+new Date('2022-08-17')); console.log(new Date('2022-08-17').getTime()); console.log(new Date('2022-08-17').valueOf()); console.log(new Date('2022-08-17') * 1); // 結(jié)果都是相同的
設(shè)置定時器
設(shè)置定時器的實現(xiàn)思路是:在第一次觸發(fā)時設(shè)置一個定時器,在指定時間之后設(shè)置變量為null
,下次觸發(fā)函數(shù)判斷變量是否為null
,來決定是否執(zhí)行函數(shù)。
const throttle = function(func, wait) { let timeout, context, args; return function() { context = this args = arguments // 允許執(zhí)行 if(!timeout) { // 設(shè)置定時器,到達時間后設(shè)置timeout為null timeout = setTimeout(function() { timeout = null func.apply(context, args) }, wait) } } }
以上兩種方式均可以滿足一個基本的節(jié)流函數(shù)的寫法,但是兩種寫法還是有一定的區(qū)別的:
- 第一種事件會立刻執(zhí)行,第二種事件會在 n 秒后第一次執(zhí)行
- 第一種事件停止觸發(fā)后不會再執(zhí)行事件,第二種事件停止觸發(fā)后依然會再執(zhí)行一次事件
既然執(zhí)行時的行為不同,那么有沒有辦法將兩者結(jié)合呢?
兩者結(jié)合
將兩者結(jié)合起來是要實現(xiàn)一個既能開始時執(zhí)行一次函數(shù),又能結(jié)束時再執(zhí)行一次函數(shù)!
思路是這樣的:如果觸發(fā)函數(shù)時沒有到達指定時間,則設(shè)置定時器,如果已經(jīng)到達設(shè)置的時間,則直接進行執(zhí)行。
function throttle(func, wait) { let timeout, context, args, previous = 0; const later = function() { // 定時器執(zhí)行時更新時間戳 previous = +new Date(); timeout = null; // 執(zhí)行函數(shù) func.apply(context, args) }; const throttled = function() { let now = +new Date(); //下次觸發(fā) func 剩余的時間 let remaining = wait - (now - previous); context = this; args = arguments; // 如果沒有剩余的時間了或者更改了系統(tǒng)時間 if (remaining <= 0 || remaining > wait) { // 清空定時器及timeout if (timeout) { clearTimeout(timeout); timeout = null; } // 更新時間戳變量 previous = now; func.apply(context, args); } else if (!timeout) { // 處理還沒有到達指定時間的觸發(fā)行為 // 此處設(shè)置定時器時間要設(shè)置剩余的時間,與上文中防抖函數(shù)中有區(qū)別 timeout = setTimeout(later, remaining); } }; return throttled; }
還是依舊縷一下思路:
第一次觸發(fā) throttled
時,因為 previous
為 0 ,所以remaining <= 0
這個條件成立,執(zhí)行func
函數(shù),并且重置定時器及變量,最后將previous
跟更新為當前時間。
第二次觸發(fā):
- 未到達指定時間:如果沒有到達指定時間,那么
remaining
為正數(shù),所以不會進入remaining <= 0
這個執(zhí)行語句,而是會設(shè)置定時器。不會執(zhí)行函數(shù)。 - 到達指定時間:
remaining
為負數(shù),執(zhí)行函數(shù),同第一次觸發(fā)。
同樣在定時器執(zhí)行時,也會更新previous
和timeout
的值。
其實核心在于remaining
這個變量的運算。
控制執(zhí)行時機
又又又來了一個需求,如果希望能夠控制首次和末次要不要執(zhí)行怎么辦?
可以傳遞第三個參數(shù):
leading:false
表示禁用第一次執(zhí)行trailing: false
表示禁用停止觸發(fā)的回調(diào)
function throttle(func, wait, options = {}) { //修改 let timeout, context, args, previous = 0; const later = function() { previous = options.leading === false ? 0 : +new Date(); //修改 timeout = null; func.apply(context, args); // 清空作用域及參數(shù)變量 if (!timeout) context = args = null; //修改 }; const throttled = function() { let now = +new Date(); // 如果是首次觸發(fā),并且設(shè)置首次不執(zhí)行函數(shù)。那么將previous與now進行同步 // now 與 previous 相減不小于0,則不會執(zhí)行函數(shù) if (!previous && options.leading === false) previous = now; // 新增 let remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; func.apply(context, args); // 清空作用域及參數(shù)變量 if (!timeout) context = args = null; //修改 } else if (!timeout && options.trailing !== false) { // 修改 timeout = setTimeout(later, remaining); } }; return throttled; }
我們要注意的是實現(xiàn)中有這樣一個問題:
那就是 leading:false
和 trailing: false
不能同時設(shè)置。因為如果同時設(shè)置,那么就是既不開始觸發(fā)也不結(jié)束時觸發(fā),那么函數(shù)將不會正常執(zhí)行。
其實核心還是關(guān)于時間戳的加減法,無非就是根據(jù)功能來設(shè)置時間戳而已。
取消
與防抖函數(shù)的取消功能基本相同,重置各個作用變量:
throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = null; }
完整實現(xiàn)
function throttle(func, wait, options = {}) { let timeout, context, args, previous = 0; const later = function() { previous = options.leading === false ? 0 : +new Date(); timeout = null; func.apply(context, args); if (!timeout) context = args = null; }; const throttled = function() { let now = +new Date(); if (!previous && options.leading === false) previous = now; let remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = null; } }; return throttled; }
這也是underscore.js
中節(jié)流的實現(xiàn)方式。
以上就是利用JavaScript實現(xiàn)防抖節(jié)流函數(shù)的示例代碼的詳細內(nèi)容,更多關(guān)于JavaScript防抖節(jié)流函數(shù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
利用javascript實現(xiàn)的三種圖片放大鏡效果實例(附源碼)
這篇文章主要介紹了利用javascript實現(xiàn)的幾種放大鏡效果,很實用一款漂亮的js圖片放大鏡特效,常見于電商網(wǎng)站上產(chǎn)品頁,用來放大展示圖片細節(jié),很有實用性,推薦下載學(xué)習(xí)研究。文中提供了完整的源碼供大家下載,需要的朋友可以參考借鑒,一起來看看吧。2017-01-01JavaScript斷言與類型守衛(wèi)及聯(lián)合聲明超詳細介紹
這篇文章主要介紹了JavaScript斷言與類型守衛(wèi)及聯(lián)合聲明,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-11-11JavaScript 原型鏈學(xué)習(xí)總結(jié)
在JavaScript中,一切都是對像,函數(shù)是第一型2010-10-10一文詳解preact的高性能狀態(tài)管理Signals
這篇文章主要介紹了一文詳解preact的高性能狀態(tài)管理Signals,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,感興趣的朋友可以參考一下2022-09-09JavaScript實現(xiàn)谷歌瀏覽器插件開發(fā)的方法詳解
對于瀏覽器插件相信大家都不陌生,誰的瀏覽器不裝幾個好用的插件呢,更是有油猴這個強大的神器。所以本文就來用JavaScript開發(fā)一個谷歌瀏覽器插件,感興趣的小伙伴可以了解一下2022-11-11