JavaScript?防抖debounce與節(jié)流thorttle
前言:
防抖(Debounce) 和 節(jié)流(Throttle) 技術(shù)用于限制函數(shù)執(zhí)行的次數(shù)。通常,一個(gè)函數(shù)將被執(zhí)行多少次或何時(shí)執(zhí)行由開(kāi)發(fā)人員決定。但在某些情況下,開(kāi)發(fā)人員會(huì)將這種能力賦予用戶,由用戶決定執(zhí)行該功能的時(shí)間和次數(shù)。
例如,添加到click
、scroll
、resize
等事件上的函數(shù),允許用戶決定何時(shí)執(zhí)行它們以及執(zhí)行多少次。有時(shí),用戶可能會(huì)比所需更頻繁地執(zhí)行這些操作。這可能不利于網(wǎng)站的性能,特別是如果附加到這些事件的函數(shù)執(zhí)行一些繁重的計(jì)算。在這種情況下,用戶可以控制函數(shù)的執(zhí)行,開(kāi)發(fā)人員必須設(shè)計(jì)一些技術(shù)來(lái)限制用戶可以執(zhí)行函數(shù)的次數(shù)。
舉個(gè)例子,假設(shè)我們?yōu)闈L動(dòng)事件scroll
添加了一個(gè)函數(shù),該函數(shù)中會(huì)執(zhí)行修改DOM元素的操作。我們知道,修改DOM元素大小開(kāi)銷很大,會(huì)引起瀏覽器的回流(Reflow)和重排(Repaint),以及重新渲染整個(gè)或部分頁(yè)面。如果用戶頻繁滾動(dòng),導(dǎo)致該函數(shù)頻繁被調(diào)用,可能會(huì)影響網(wǎng)頁(yè)性能或?qū)е马?yè)面卡頓等。
此外,有些事件回調(diào)函數(shù)中包含ajax等異步操作的時(shí)候,多次觸發(fā)會(huì)導(dǎo)致返回的內(nèi)容結(jié)果順序不一致,而導(dǎo)致得到的結(jié)果非最后一次觸發(fā)事件對(duì)應(yīng)的結(jié)果
所以,為了優(yōu)化網(wǎng)頁(yè)的性能,控制函數(shù)被調(diào)用的頻率是很有必要的,防抖(Debounce) 和 節(jié)流(Throttle) 是通過(guò)控制函數(shù)被調(diào)用的頻率來(lái)優(yōu)化腳本性能的兩種方法
一、防抖(Debounce)
防抖:無(wú)論用戶觸發(fā)多少次事件,對(duì)應(yīng)的回調(diào)函數(shù)只會(huì)在事件停止觸發(fā)指定事件后執(zhí)行。(即:回調(diào)函數(shù)在事件停止觸發(fā)指定時(shí)間后被調(diào)用)
例如,假設(shè)用戶在 100 毫秒內(nèi)點(diǎn)擊了 5 次按鈕。防抖技術(shù)不會(huì)讓這些點(diǎn)擊中的任何一個(gè)執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。一旦用戶停止點(diǎn)擊,如果去抖時(shí)間為 100 毫秒,則回調(diào)函數(shù)將在 100 毫秒后執(zhí)行。因此,肉眼看來(lái),防抖就像將多個(gè)事件組合成一個(gè)事件一樣。
1.1 防抖函數(shù)的實(shí)現(xiàn)
(1)版本1 —— 停止觸發(fā)事件n毫秒后執(zhí)行回調(diào)函數(shù)
觸發(fā)事件后函數(shù)不會(huì)立即執(zhí)行,而是在停止事件觸發(fā)后 n 毫秒后執(zhí)行,如果在 n 毫秒內(nèi)又觸發(fā)了事件,則會(huì)重新計(jì)時(shí)
/** * @desc 函數(shù)防抖 * @param func 回調(diào)函數(shù) * @param delay 延遲執(zhí)行毫秒數(shù) */ function debounce(func, delay) { let timer; // 定時(shí)器 return function () { let context = this; // 記錄 this 值,防止在回調(diào)函數(shù)中丟失 let args = arguments; // 函數(shù)參數(shù) //如果定時(shí)器存在,則清除定時(shí)器(如果沒(méi)有,也沒(méi)必要進(jìn)行處理) timer ? clearTimeout(timer) : null; timer = setTimeout(() => { // 防止 this 值變?yōu)?window func.apply(context, args) }, delay); } }
(2)版本2
觸發(fā)事件后立即執(zhí)行回調(diào)函數(shù),但是觸發(fā)后n毫秒內(nèi)不會(huì)再執(zhí)行回調(diào)函數(shù),如果 n 毫秒內(nèi)觸發(fā)了事件,也會(huì)重新計(jì)時(shí)。
/** * @desc 函數(shù)防抖 * @param func 回調(diào)函數(shù) * @param delay 延遲執(zhí)行毫秒數(shù) */ function _debounce(func, delay) { let timer; // 定時(shí)器 return function () { let context = this; // 記錄 this 值,防止在回調(diào)函數(shù)中丟失 let args = arguments; // 函數(shù)參數(shù) // 標(biāo)識(shí)是否立即執(zhí)行 let isImmediately = !timer; //如果定時(shí)器存在,則清除定時(shí)器(如果沒(méi)有,也沒(méi)必要進(jìn)行處理) timer ? clearTimeout(timer) : null; timer = setTimeout(() => { timer = null; }, delay); // isImmediately 為 true 則 執(zhí)行函數(shù)(即首次觸發(fā)事件) isImmediately ? func.apply(context, args) : null; } }
舉個(gè)例子來(lái)對(duì)比一下兩個(gè)版本的區(qū)別:
document.body.onclick= debounce(function () { console.log('hello') },1000)
如上代碼中,我們給body添加了一個(gè)點(diǎn)擊事件監(jiān)聽(tīng)器。
- 如果是版本1的防抖函數(shù),當(dāng)我點(diǎn)擊body時(shí),控制臺(tái)不會(huì)立即打印
hello
,要等 1000ms 后才會(huì)打印。在這 1000s 內(nèi)如果還點(diǎn)擊了 body,那么就會(huì)重新計(jì)時(shí)。即最后一次點(diǎn)擊 body 過(guò)1000ms后控制臺(tái)才會(huì)打印hello
- 如果是版本2的防抖函數(shù),當(dāng)我首次點(diǎn)擊body時(shí),控制臺(tái)會(huì)立馬打印
hello
,但是在此之后的 1000ms 內(nèi)點(diǎn)擊 body ,控制臺(tái)不會(huì)有任何反應(yīng)。在這 1000s 內(nèi)如果還點(diǎn)擊了 body,那么就會(huì)重新計(jì)時(shí)。必須等計(jì)時(shí)結(jié)束后再點(diǎn)擊body,控制臺(tái)才會(huì)再次打印hello
。
1.2 防抖的實(shí)際應(yīng)用
(1)搜索框建議項(xiàng)
通常,搜索框會(huì)提供下拉菜單,為用戶當(dāng)前的輸入提供自動(dòng)完成選項(xiàng)。但有時(shí)建議項(xiàng)是通過(guò)請(qǐng)求后端得到的??梢栽趯?shí)現(xiàn)提示文本時(shí)應(yīng)用防抖,在等待用戶停止輸入一段時(shí)間后再顯示建議文本。因此,在每次擊鍵時(shí),都會(huì)等待幾秒鐘,然后再給出建議。
(2)消除resize事件處理程序的抖動(dòng)。
window 觸發(fā) resize 的時(shí)候,不斷的調(diào)整瀏覽器窗口大小會(huì)不斷觸發(fā)這個(gè)事件,用防抖讓其只觸發(fā)一次
(3)自動(dòng)保存
例如掘金一類的網(wǎng)站,都會(huì)內(nèi)嵌文本編輯器,在編輯過(guò)程中會(huì)自動(dòng)保存文本,防止數(shù)據(jù)丟失。每次保存都會(huì)與后端進(jìn)行數(shù)據(jù)交互,所以可以應(yīng)用防抖,在用戶停止輸入后一段時(shí)間內(nèi)再自動(dòng)保存。
(4)手機(jī)號(hào)、郵箱等輸入驗(yàn)證檢測(cè)
通常對(duì)于一些特殊格式的輸入項(xiàng),我們通常會(huì)檢查格式。我們可以應(yīng)用防抖在用戶停止輸入后一段時(shí)間再進(jìn)行格式檢測(cè),而不是輸入框中內(nèi)容發(fā)生改變就檢測(cè)。
(5)在用戶停止輸入之前不要發(fā)出任何 Ajax 請(qǐng)求
二、節(jié)流(Throttle)
節(jié)流:無(wú)論用戶觸發(fā)事件多少次,附加的函數(shù)在給定的時(shí)間間隔內(nèi)只會(huì)執(zhí)行一次。(即:回調(diào)函數(shù)在規(guī)定時(shí)間內(nèi)最多執(zhí)行一次)
例如,當(dāng)用戶單擊一個(gè)按鈕時(shí),會(huì)執(zhí)行一個(gè)在控制臺(tái)上打印Hello, world
的函數(shù)?,F(xiàn)在,假設(shè)對(duì)這個(gè)函數(shù)應(yīng)用 1000 毫秒的限制時(shí),無(wú)論用戶點(diǎn)擊按鈕多少次,Hello, world
在 1000 毫秒內(nèi)都只會(huì)打印一次。節(jié)流可確保函數(shù)定期執(zhí)行。
2.1 節(jié)流函數(shù)的實(shí)現(xiàn)
(1)版本1 —— 使用定時(shí)器
/** * @desc 函數(shù)節(jié)流 * @param func 回調(diào)函數(shù) * @param limit 時(shí)間限制 */ const throttle = (func, limit) => { let inThrottle; // 是否處于節(jié)流限制時(shí)間內(nèi) return function() { const context = this; const args = arguments; // 跳出時(shí)間限制 if (!inThrottle) { func.apply(context, args); // 執(zhí)行回調(diào) inThrottle = true; // 開(kāi)啟定時(shí)器計(jì)時(shí) setTimeout(() => inThrottle = false, limit); } } }
(2)版本2 —— 計(jì)算當(dāng)前時(shí)間與上次執(zhí)行函數(shù)時(shí)間的間隔
/** * @desc 函數(shù)節(jié)流 * @param func 回調(diào)函數(shù) * @param limit 時(shí)間限制 */ function throttle(func, limit) { //上次執(zhí)行時(shí)間 let previous = 0; return function() { //當(dāng)前時(shí)間 let now = Date.now(); let context = this; let args = arguments; // 若當(dāng)前時(shí)間-上次執(zhí)行時(shí)間大于時(shí)間限制 if (now - previous > limit) { func.apply(context, args); previous = now; } } }
很多現(xiàn)有的庫(kù)中已經(jīng)實(shí)現(xiàn)了防抖函數(shù)和節(jié)流函數(shù)
2.2 節(jié)流的實(shí)際應(yīng)用
(1)游戲中通過(guò)按下按鈕執(zhí)行的關(guān)鍵動(dòng)作(例如:射擊、平A)
拿王者榮耀為例,通常都有攻速一說(shuō)。如果攻速低,即使 n 毫秒內(nèi)點(diǎn)擊平A按鈕多次,也只會(huì)執(zhí)行一次平A。其實(shí)這里就類似于節(jié)流的思想,可以通過(guò)設(shè)置節(jié)流的時(shí)間間隔限制,來(lái)改變攻速。
(2)滾動(dòng)事件處理
如果滾動(dòng)事件被觸發(fā)得太頻繁,可能會(huì)影響性能,因?yàn)樗罅恳曨l和圖像。因此滾動(dòng)事件必須使用節(jié)流
(3)限制mousemove/touchmove事件處理程序
小結(jié)
如何選擇防抖和節(jié)流:
關(guān)于防抖函數(shù)和節(jié)流函數(shù)的選擇,一篇博客中是這樣建議的:
A debounce is utilized when you only care about the final state. A throttle is best used when you want to handle all intermediate states but at a controlled rate.
即:如果只關(guān)心最終狀態(tài),建議使用防抖。如果是想要函數(shù)以可控的速率執(zhí)行,那么建議使用節(jié)流。
延時(shí)多久合理:
- 大多數(shù)屏幕的刷新頻率是每秒60Hz,瀏覽器的渲染頁(yè)面的標(biāo)準(zhǔn)幀率也為60FPS,瀏覽器每秒會(huì)重繪60次,而每幀之間的時(shí)間間隔是DOM視圖更新的最小間隔。
- 一個(gè)平滑而流暢的動(dòng)畫(huà),最佳的循環(huán)間隔即幀與幀的切換時(shí)間希望是 16.6ms(1s/60)內(nèi),也意味著17ms內(nèi)的多次DOM改動(dòng)會(huì)被合并為一次渲染。
- 當(dāng)執(zhí)行回調(diào)函數(shù)時(shí)間大于16.6ms(系統(tǒng)屏幕限制的刷新頻率),UI將會(huì)出現(xiàn)丟幀(即UI這一刻不會(huì)被渲染),且丟幀越多,引起卡頓情況更嚴(yán)重。
到此這篇關(guān)于JavaScript 防抖debounce與節(jié)流thorttle的文章就介紹到這了,更多相關(guān)JavaScript 防抖與節(jié)流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript Array Flatten 與遞歸使用介紹
用 JavaScript 將 [1,2,3,[4,5, [6,7]], [[[8]]]] 這樣一個(gè) Array 變成 [1,2,3,4,5, 6,7,8] 呢?傳說(shuō)中的 Array Flatten2011-10-10js獲得當(dāng)前時(shí)區(qū)夏令時(shí)發(fā)生和終止的時(shí)間代碼
這篇文章主要介紹了js獲得當(dāng)前時(shí)區(qū)夏令時(shí)發(fā)生和終止的時(shí)間代碼,需要的朋友可以參考下2014-02-02微信小程序?qū)崿F(xiàn)購(gòu)物車(chē)功能
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)購(gòu)物車(chē)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11js將json格式內(nèi)容轉(zhuǎn)換成對(duì)象的方法
這篇文章介紹了js將json格式內(nèi)容轉(zhuǎn)換成對(duì)象的方法,有需要的朋友可以參考一下2013-11-11解析arp病毒背后利用的Javascript技術(shù)附解密方法
解析arp病毒背后利用的Javascript技術(shù)附解密方法...2007-08-08Javascript類型轉(zhuǎn)換的規(guī)則實(shí)例解析
這篇文章主要介紹了Javascript類型轉(zhuǎn)換的規(guī)則實(shí)例解析,涉及到j(luò)avascript類型轉(zhuǎn)換相關(guān)知識(shí),對(duì)本文感興趣的朋友一起學(xué)習(xí)吧2016-02-02JavaScript 數(shù)組去重并統(tǒng)計(jì)重復(fù)元素出現(xiàn)的次數(shù)實(shí)例
下面小編就為大家分享一篇JavaScript 數(shù)組去重并統(tǒng)計(jì)重復(fù)元素出現(xiàn)的次數(shù)實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-12-12基于javascript實(shí)現(xiàn)的搜索時(shí)自動(dòng)提示功能
這篇文章主要介紹了基于javascript實(shí)現(xiàn)的搜索時(shí)自動(dòng)提示功能,非常實(shí)用,推薦給需要的小伙伴參考下。2014-12-12