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