白话再谈防抖与节流--不蒙圈指北
再談防抖/節(jié)流
對于一名合格的前端工程師,
沒有不知道防抖與節(jié)流的吧。
我并不是一名真正的前端,
正處于成為合格前端的路上。
在前端的學(xué)習(xí)過程中,關(guān)于[防抖/節(jié)流]
也聽老師講過,也看過一些大佬寫的指南,
雖然當(dāng)時明白,過后即忘;
關(guān)鍵在于理解和思路,今天整理一下,
寫一篇不懵圈指北分享給大家。
防抖
什么是防抖
我有兩個兒子,大寶跟二仁。
這二人都是吃貨。一天,
場景1
我?:大寶,爸爸今天升級加薪
???迎娶白富 美了,給你買好吃的,
???想吃點什么水果,選一個?
大寶:我想吃,西瓜,
???不對,是哈密瓜,
???不對,是芒果,
???不對,是荔枝,
???不對,是草莓。
我?:好的,草莓是吧,這就去買。
這就是【防抖】,永遠只響應(yīng)最新的(最后一次)請求。
防抖的JS實現(xiàn)
實現(xiàn)思路
實現(xiàn)一個防抖函數(shù),作為包裝器
接收兩個參數(shù):
實際要執(zhí)行的函數(shù)(回調(diào))
要延遲的時間限制
利用延時定時器,創(chuàng)造異步執(zhí)行
如果已有定時器,則清空定時器
設(shè)置定時器,到執(zhí)行時,清空當(dāng)前定時器重新定時
返回包裝后的響應(yīng)函數(shù)
代碼
Talk is cheap,show you the code
/**
* 簡單的`debounce`函數(shù)實現(xiàn)
*
* @param {function} cb 要執(zhí)行的回調(diào)函數(shù)
* @param {number} delay 要等待的防抖延遲時間
* @returns {function}
*/
const debounce = function(cb, delay) {
// 參數(shù)檢查
// cb:function
// delay:number
if (!cb || toString.call(cb) !== '[object Function]') {
throw new Error(`${cb} is not a function.`)
}
// 沒有傳 delay 參數(shù)時(包括等于0)
if (!delay) {
delay = 500 // 設(shè)置默認延時
}
// delay 參數(shù)須為正整數(shù)
else if (!Number.isInteger(delay) || delay < 0) {
throw new Error(`${delay} is invalid as optional parameter [delay]`)
}
// 定時器
let timer = null
// 返回debounce包裝過的執(zhí)行函數(shù)
return function(...args) {
// 如果存在定時器
if (timer) {
// 清除定時器,
// 即:忽略之前的觸發(fā)
clearTimeout(timer)
}
// 設(shè)置定時器
timer = setTimeout(() => {
// 當(dāng)?shù)搅嗽O(shè)定的時間:
// 清除本次定時器,
// 并執(zhí)行函數(shù)
clearTimeout(timer)
timer = null
cb.call(null, ...args)
}, delay);
}
}
防抖典型的應(yīng)用場景:
?輸入框的提示或搜索功能
如果隨著輸入實時檢索,
將白費很多次請求;
這樣利用防抖函數(shù),可以設(shè)定500毫秒的延遲,當(dāng)用戶輸入時不進行實時檢索,超過500毫秒沒有輸入(停頓了)時,再發(fā)起檢索請求,減少無謂的請求數(shù)量,節(jié)省網(wǎng)絡(luò)和服務(wù)器資源。
放個窗口滾動的防抖示例,如下:
創(chuàng)建一個debounce_sample.html,
用 chrome 瀏覽器打開,甩起你的滾動輪并查看 console,自己感受一下。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>debounce sample</title>
</head>
<body>
<h3>
<ul>
<li>打開console</li>
<li>快速滾動鼠標滾動輪</li>
</ul>
</h3>
<div></div>
<script>
/**
* 簡單的`debounce`函數(shù)實現(xiàn)
*
* @param {function} cb 要執(zhí)行的回調(diào)函數(shù)
* @param {number} delay 要等待的防抖延遲時間
* @returns {function}
*/
const debounce = function (cb, delay) {
// ...此處省略以節(jié)省篇幅...
// ...函數(shù)內(nèi)容請參照上文...
}
// scroll in debounce
const realEventHandler = function() {
console.log('scroll in debounce')
}
const debouncedScrollHandler = debounce(realEventHandler, 500)
window.addEventListener('scroll', debouncedScrollHandler)
</script>
</body>
</html>
節(jié)流
什么是節(jié)流
場景2
[第一天]
二仁:靶鼻,我好想吃冰淇淋,
???給我買一個好嗎?
我?:好吧,那就買一個,別告訴你哥。
[第二天]
二仁:靶鼻,我今天還想吃冰淇淋,
???再給我買一個好嗎?
我?:想啥呢二仁。。。
???哪能天天吃?!!
???咱家經(jīng)濟狀況你又不是不知道,
???再加上你出生后,更揭不開鍋了,
???你也知道,
???你現(xiàn)在喝的一段奶粉是最貴的,
???你哥幼兒園一個月兩千多,
???你爸我一個月才幾千工資啊,
???還得養(yǎng)活全家,還房貸交房租少嗎,
???balabala。。。
???(總之沒買)
二仁?:好吧。。。
[第三天]
二仁:靶鼻,我有個小心愿,
???不是當(dāng)講不當(dāng)講。。。
我?:說來聽聽
二仁:就昨天那事兒,
???我還是想吃冰淇淋,
???再給我買一個好嗎?
我?:看你可憐的樣,今天我就答應(yīng)你。
???回去可得讓我嘗嘗,另外,
???悄悄地吃別讓你哥看見。
[第四天]
二仁:靶鼻,我想吃冰淇淋。。。
我?:不行。
???昨天剛吃了。
???明天再說。
這就是【節(jié)流】,
一定時間內(nèi),只響應(yīng)一次請求。
經(jīng)過該給定時間間隔之后,才能再次響應(yīng)。
節(jié)流的JS實現(xiàn)
實現(xiàn)思路
為了更方便理解,我們可以參考游戲里放大招,眼看敵人殘血了,把握好機會立馬放大準備收人頭,說正事兒,放完大招,要等一個冷卻時間過了,才能再次使用。
節(jié)流throttle看起來也是這么回事兒,請看下面的代碼實現(xiàn)(包括注釋):
代碼
/**
* `throttle`簡單的節(jié)流函數(shù)實現(xiàn)
*
* @param {function} cb 要執(zhí)行的回調(diào)函數(shù)
* @param {number} wait 要設(shè)置的節(jié)流時間間隔
* @returns {function}
*/
const throttle = function (cb, wait) {
// 參數(shù)檢查
// cb:function
// wait:number
if (!cb || toString.call(cb) !== '[object Function]') {
throw new Error(`${cb} is not a function.`)
}
// 沒有傳 wait 參數(shù)時(包括等于0)
if (!wait) {
wait = 500 // 設(shè)置默認延時
}
// wait 參數(shù)須為正整數(shù)
else if (!Number.isInteger(wait) || wait < 0) {
throw new Error(`${wait} is invalid as optional parameter [wait]`)
}
// 用來記錄上次執(zhí)行的時刻
let lasttime = Date.now()
return function (...args) {
const now = Date.now()
// 兩次執(zhí)行的時間間隔
const timespan = now - lasttime
// 當(dāng)間隔小于等待時間即處于冷卻中
const isCoolingDown = timespan < wait
console.log(timespan, isCoolingDown ? 'is cooling down' : 'execute')
// 如果還沒冷卻好,就等待
if (isCoolingDown) return
// 記錄本次執(zhí)行的時刻
lasttime = Date.now()
// 冷卻好了
cb.apply(null, args)
}
}
節(jié)流用在resize或者鼠標拖動之類的事件上是合適的,因為如果沒有節(jié)流,體驗會變得很糟糕。
下面我們創(chuàng)建一個throttle_sample.html來體驗一下效果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>throttle sample</title>
</head>
<body>
<h3>
<ul>
<li>打開console</li>
<li>快速滾動鼠標滾動輪</li>
</ul>
</h3>
<div></div>
<script src="throttle.js"></script>
<script>
const realEventHandler = function() {
console.log('scroll in throttle')
}
const scrollHandler = throttle(realEventHandler, 2000) // 兩秒鐘內(nèi)只響應(yīng)一次(打印一次log)
window.addEventListener('scroll', scrollHandler)
</script>
</body>
</html>
防抖+節(jié)流
如果你親自體驗了上面防抖的示例,可能會發(fā)現(xiàn)這樣一個問題:
當(dāng)我一直滾動鼠標滾動輪不松手時,那就一直不會觸發(fā)事件,耗一年也不會。
這,是不是問題?
在實際場景中,這,確實是個問題。
解決場景1存在的問題
場景1問題
我?:大寶,爸爸今天升級加薪
???迎娶白富 美了,給你買好吃的,
???想吃點什么水果,選一個?
大寶:我想吃,西瓜,
???不對,是哈密瓜,
???不對,是櫻桃,
???不對,是葡萄,
???不對,是橙子,
???不對,是香蕉,
???不對,是芒果,
???不對,是荔枝,
???。。。
??(一個小時過去了)
???。。。
???不對,是草莓,
我?:先停,誰家爸爸這么有耐心,
???都聽你你說一個小時了,
???就按最后一個也就是第1024個:
???【草莓】,這就去買。
解決辦法:防抖 + 節(jié)流
這就是【防抖 + 節(jié)流】,
在規(guī)定的時間內(nèi),
只響應(yīng)最后一次請求,之前的都忽略
只響應(yīng)最后一次請求,
規(guī)定的時間內(nèi),必須要響應(yīng)一次
說人話:
防抖:只響應(yīng)最后一次請求
節(jié)流:單位時間間隔內(nèi)只響應(yīng)一次
--> 只響應(yīng)單位間隔期間里最后一次請求
實現(xiàn)一個【防抖 + 節(jié)流】
這里有一點不太好理解的地方,
就是需要兩個定時器,
分別記錄 防抖/節(jié)流。
如果沒有節(jié)流函數(shù)定時器,且超出規(guī)定的時間間隔,說明用戶操作沒有中斷,此時需要強制執(zhí)行一次回調(diào)函數(shù)響應(yīng)
其他情況,則按照防抖函數(shù)處理
注意執(zhí)行回調(diào)函數(shù)響應(yīng)時,兩個定時器都要清空
/**
* 防抖+節(jié)流的組合實現(xiàn)
*
* @param {function} cb 要執(zhí)行的回調(diào)函數(shù)
* @param {number} wait 要設(shè)置的防抖節(jié)流時間
* @returns {function}
*
* 思路:
* 1. 第一次觸發(fā)或者一直觸發(fā),考慮 throttle,定時響應(yīng)一次(強制響應(yīng))
* 2. 如果沒有一直觸發(fā),則使用 debounce,響應(yīng)最后一次請求
*/
const combinedDebounceThrottle = function(cb, wait) {
// TODO:參數(shù)檢查
// cb:function
// wait:number
// 略(參照 debounce.js 或 throttle.js 部分)
let lasttime = 0
let timerDebounce = null
let timerThrottle = null
// 執(zhí)行一次回調(diào)響應(yīng)的處理部分
function executeCb(...args) {
clearTimeout(timerDebounce)
clearTimeout(timerThrottle)
timerDebounce = null
timerThrottle = null
lasttime = Date.now()
cb.apply(null, args)
}
return function(...args) {
const now = Date.now()
const timespan = now - lasttime
const isCoolingDown = timespan < wait
clearTimeout(timerDebounce)
// 如果一直 debounce 而沒有執(zhí)行響應(yīng),
// 且,超過冷卻時間,則強制執(zhí)行一次
if (!timerThrottle && !isCoolingDown) {
timerThrottle = setTimeout(executeCb, wait, ...args)
}
// 如果不是一直觸發(fā),則在延遲時間后做一次響應(yīng)(使用debounce)
else {
timerDebounce = setTimeout(executeCb, wait, ...args)
}
}
}
還是按照慣例,創(chuàng)建一個combined_sample.html文件體驗效果:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>throttle sample</title>
</head>
<body>
<h3>
<ul>
<li>打開console</li>
<li>快速滾動鼠標滾動輪</li>
</ul>
</h3>
<div></div>
<script src="combinedDebounceThrottle.js"></script>
<script>
const realEventHandler = function(e) {
console.log('scroll in debounce-throttle combined mode', e)
}
const scrollHandler = combinedDebounceThrottle(realEventHandler, 2000) // 兩秒鐘內(nèi)只響應(yīng)一次(打印一次log)
window.addEventListener('scroll', scrollHandler)
</script>
</body>
</html>
可以看到,即便一直滾動,也會在2秒后打印出控制臺信息。
這樣就實現(xiàn)了防抖+節(jié)流的組合模式。
白話了半天,讀者朋友們是不是都理解了?
其他補充
underscore.js中為我們提供了功能更為豐富的防抖與節(jié)流函數(shù)。
是不是在想:你怎么不早說?!
實現(xiàn)得越復(fù)雜就越不容易理解嘛~現(xiàn)在看了我的講解,再去看 underscore.js 庫里封裝的優(yōu)秀方法,也就能更好地理解了。
underscore 里的防抖與節(jié)流,為我們提供了更為豐富的配置選項和自定義的可能,能夠應(yīng)對更多的業(yè)務(wù)場景,下面就來探究一下 underscore 中是怎樣的吧。
防抖debounce
_.debounce(func, wait, [immediate])
| 參數(shù) | 說明 |
|---|---|
| function | 處理函數(shù) |
| wait | 指定的毫秒數(shù)間隔 |
| immediate | 立即執(zhí)行Flag 可選。 默認:false |
功能解析:
前兩個參數(shù)與前面介紹的throttle是一樣的,第三個參數(shù),
immediate指定第一次觸發(fā)或沒有等待中的時候可以立即執(zhí)行。
知道了原理,我們來簡單寫一下代碼實現(xiàn)。
// debounce 防抖:
// 用戶停止輸入&wait毫秒后,響應(yīng),
// 或 immediate = true 時,沒有等待的回調(diào)的話立即執(zhí)行
// 立即執(zhí)行并不影響去設(shè)定時器延遲執(zhí)行
_.debounce = function(func, wait, immediate){
var timer, result
var later = function(...args){
clearTimeout(timer)
timer = null
result = func.apply(null, args)
}
return function(...args){
// 因為防抖是響應(yīng)最新一次操作,所以清空之前的定時器
if(timer) clearTimeout(timer)
// 如果配置了 immediate = true
if(immediate){
// 沒有定時函數(shù)等待執(zhí)行才可以立即執(zhí)行
var callNow = !timer
// 是否立即執(zhí)行,并不影響設(shè)定時器的延遲執(zhí)行
timer = setTimeout(later, wait, ...args)
if(callNow){
result = func.apply(null, args)
}
}
else{
timer = setTimeout(later, wait, ...args)
}
return result
}
}
節(jié)流throttle
_.throttle(function, wait, [options])
| 參數(shù) | 說明 |
|---|---|
| function | 處理函數(shù) |
| wait | 指定的毫秒數(shù)間隔 |
| options | 配置 |
| 可選。 默認: { ?leading: false, ?trailing: false ? } |
針對第一次觸發(fā),
leading : true 相當(dāng)于先執(zhí)行,再等待wait毫秒之后才可再次觸發(fā)
trailing : true 相當(dāng)于先等待wait毫秒,后執(zhí)行
默認:
leading : false => 阻止第一次觸發(fā)時立即執(zhí)行,等待wait毫秒才可觸發(fā)
trailing : false => 阻止第一次觸發(fā)時的延遲執(zhí)行,經(jīng)過延遲的wait毫秒之后才可觸發(fā)
可能的配置方式:
(區(qū)別在首次執(zhí)行和先執(zhí)行還是先等待)
| 配置 | 結(jié)果 |
|---|---|
|
{ |
第一次觸發(fā)不執(zhí)行,后面同普通throttle,執(zhí)行 + 間隔wait毫秒 |
| { leading: true, trailing: false } |
第一次觸發(fā)立即執(zhí)行,后面同普通throttle,執(zhí)行 + 間隔wait毫秒 |
| { leading: false, trailing: true } |
每次觸發(fā)延遲執(zhí)行,每次執(zhí)行間隔wait毫秒 |
| { leading: true, trailing: true } |
每一次有效觸發(fā)都會執(zhí)行兩次,先立即執(zhí)行一次,后延時wait毫秒執(zhí)行一次 |
知道了原理,我們來簡單寫一下代碼實現(xiàn)。
_.now = Date.now
_.throttle = function(func, wait, options){
var lastTime = 0
var timeOut = null
var result
if(!options){
options = { leading: false, trailing: false }
}
return function(...args){ // 節(jié)流函數(shù)
var now = _.now()
// 首次執(zhí)行看是否配置了 leading = false = 默認,阻止立即執(zhí)行
if(!lastTime && options.leading === false){
lastTime = now
}
// 配置了 leading = true 時,初始值 lastTime = 0,即可以立即執(zhí)行
var remaining = lastTime + wait - now
// > 0 即間隔內(nèi)
// < 0 即超出間隔時間
// 超出間隔時間,或首次的立即執(zhí)行
if(remaining <= 0){ // trailing=false
if(timeOut){
// 如果不是首次執(zhí)行的情況,需要清空定時器
clearTimeout(timeOut)
timeOut = null
}
lastTime = now // #
result = func.apply(null, args)
}
else if(!timeOut && options.trailing !== false){ // leading
// 沒超出間隔時間,但配置了 leading=fasle 阻止了立即執(zhí)行,
// 即需要執(zhí)行一次卻還未執(zhí)行,等待中,且配置了 trailing=true
// 那就要在剩余等待毫秒時間后觸發(fā)
timeOut = setTimeout(()=>{
lastTime = options.leading === false ? 0 : _.now() // # !lastTime 的判斷中需要此處重置為0
timeOut = null
result = func.apply(null, args)
}, remaining);
}
return result
}
}
除了上文介紹的配置,還加入了可取消功能(cancel)(from 1.9.0)
小結(jié)
throttle和debounce是解決請求和響應(yīng)速度不匹配問題的兩個方案
二者的差異在于選擇不同的策略
debounce的關(guān)注點是空閑的間隔時間,
throttle的關(guān)注點是連續(xù)的執(zhí)行間隔時間。
應(yīng)用場景
游戲設(shè)計,keydown事件
文本輸入、自動完成,keyup事件
鼠標移動mousemove事件
DOM元素動態(tài)定位,window對象的resize、scroll事件
總結(jié)比較
對于我們最開始的簡單防抖/節(jié)流實現(xiàn)
相同
debounce防抖與throttle節(jié)流都實現(xiàn)了單位時間內(nèi),函數(shù)只執(zhí)行一次
不同
debounce防抖:
單位時間內(nèi),忽略前面的,響應(yīng)最新的,并在延遲wait毫秒后執(zhí)行
throttle節(jié)流:
響應(yīng)第一次的,單位時間內(nèi),不再響應(yīng),直到wait毫秒后才再響應(yīng)
以上。再說不懂防抖節(jié)流算我輸。
最后,感謝您的閱讀和支持~
總結(jié)
以上是生活随笔為你收集整理的白话再谈防抖与节流--不蒙圈指北的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vue请求简单配置
- 下一篇: 【二分答案+贪心】解决“最小值最大”问题