防抖和節流嚴格算起來應該屬于性能優化的知識,但實際上遇到的頻率相當高,處理不當或者放任不管就容易引起瀏覽器卡死。所以還是很有必要早點掌握的。(信我,你看完肯定就懂了)
## 從滾動條監聽的例子說起
先說一個常見的功能,很多網站會提供這么一個按鈕:用于返回頂部。

這個按鈕只會在滾動到距離頂部一定位置之后才出現,那么我們現在抽象出這個功能需求-- **監聽瀏覽器滾動事件,返回當前滾條與頂部的距離**
這個需求很簡單,直接寫:
```
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滾動條位置:' + scrollTop);
}
window.onscroll = showTop
```
但是!

在運行的時候會發現存在一個問題:**這個函數的默認執行頻率,太!高!了!**。 高到什么程度呢?以chrome為例,我們可以點擊選中一個頁面的滾動條,然后點擊一次鍵盤的【向下方向鍵】,會發現函數執行了**8-9**次!

然而實際上我們并不需要如此高頻的反饋,畢竟瀏覽器的性能是有限的,不應該浪費在這里,所以接著討論如何優化這種場景。
## 防抖(debounce)
基于上述場景,首先提出第一種思路:**在第一次觸發事件時,不立即執行函數,而是給出一個期限值比如200ms**,然后:
- 如果在200ms內沒有再次觸發滾動事件,那么就執行函數
- 如果在200ms內再次觸發滾動事件,那么當前的計時取消,重新開始計時
**效果**:如果短時間內大量觸發同一事件,只會執行一次函數。
**實現**:既然前面都提到了計時,那實現的關鍵就在于`setTimeOut`這個函數,由于還需要一個變量來保存計時,考慮維護全局純凈,可以借助閉包來實現:
```
/*
* fn [function] 需要防抖的函數
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
let timer = null //借助閉包
return function() {
if(timer){
clearTimeout(timer) //進入該分支語句,說明當前正在一個計時過程中,并且又觸發了相同事件。所以要取消當前的計時,重新開始計時
timer = setTimeOut(fn,delay)
}else{
timer = setTimeOut(fn,delay) // 進入該分支說明當前并沒有在計時,那么就開始一個計時
}
}
}
```
當然 上述代碼是為了貼合思路,方便理解(這么貼心不給個贊咩?),寫完會發現其實 time = setTimeOut(fn,delay)是一定會執行的,所以可以稍微簡化下:
```
/*****************************簡化后的分割線 ******************************/
function debounce(fn,delay){
let timer = null //借助閉包
return function() {
if(timer){
clearTimeout(timer)
}
timer = setTimeout(fn,delay) // 簡化寫法
}
}
// 然后是舊代碼
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滾動條位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000) // 為了方便觀察效果我們取個大點的間斷值,實際使用根據需要來配置
```
此時會發現,必須在停止滾動1秒以后,才會打印出滾動條位置。
到這里,已經把防抖實現了,現在給出定義:
對于**短時間內連續觸發**的事件(上面的滾動事件),**防抖的含義就是讓某個時間期限(如上面的1000毫秒)內,事件處理函數只執行一次**。
## 節流(throttle)
繼續思考,使用上面的防抖方案來處理問題的結果是:
- 如果在限定時間段內,不斷觸發滾動事件(比如某個用戶閑著無聊,按住滾動不斷的拖來拖去),只要不停止觸發,理論上就永遠不會輸出當前距離頂部的距離。
**但是如果產品同學的期望處理方案是:即使用戶不斷拖動滾動條,也能在某個時間間隔之后給出反饋呢?**(此處暫且不論哪種方案更合適,既然產品爸爸說話了我們就先考慮怎么實現)

其實很簡單:我們可以設計一種**類似控制閥門一樣定期開放的函數,也就是讓函數執行一次后,在某個時間段內暫時失效,過了這段時間后再重新激活**(類似于技能冷卻時間)。
**效果**:如果短時間內大量觸發同一事件,那么**在函數執行一次之后,該函數在指定的時間期限內不再工作**,直至過了這段時間才重新生效。
**實現**: 這里借助`setTimeout`來做一個簡單的實現,加上一個狀態位`valid`來表示當前函數是否處于工作狀態
```
function throttle(fn,delay){
let valid = true
return function() {
if(!valid){
//休息時間 暫不接客
return false
}
// 工作時間,執行函數并且在間隔期內把狀態位設為無效
valid = false
setTimeout(() => {
fn()
valid = true;
}, delay)
}
}
/* 請注意,節流函數并不止上面這種實現方案,
例如可以完全不借助setTimeout,可以把狀態位換成時間戳,然后利用時間戳差值是否大于指定間隔時間來做判定。
也可以直接將setTimeout的返回的標記當做判斷條件-判斷當前定時器是否存在,如果存在表示還在冷卻,并且在執行fn之后消除定時器表示激活,原理都一樣
*/
// 以下照舊
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滾動條位置:' + scrollTop);
}
window.onscroll = throttle(showTop,1000)
```
運行以上代碼的結果是:
- 如果一直拖著滾動條進行滾動,那么會以1s的時間間隔,持續輸出當前位置和頂部的距離
## 其他應用場景舉例
講完了這兩個技巧,下面介紹一下平時開發中常遇到的場景:
1. 搜索框input事件,例如要支持輸入實時搜索可以使用節流方案(間隔一段時間就必須查詢相關內容),或者實現輸入間隔大于某個值(如500ms),就當做用戶輸入完成,然后開始搜索,具體使用哪種方案要看業務需求。
2. 頁面resize事件,常見于需要做頁面適配的時候。需要根據最終呈現的頁面情況進行dom渲染(這種情形一般是使用防抖,因為只需要判斷最后一次的變化情況
## 思考總結
上述內容基于防抖和節流的核心思路設計了簡單的實現算法,但是不代表實際的庫(例如undercore js)的源碼就直接是這樣的,最起碼的可以看出,在上述代碼實現中,因為`showTop`本身的很簡單,無需考慮作用域和參數傳遞,所以連`apply`都沒有用到,實際上肯定還要考慮傳遞`argument`以及上下文環境(畢竟apply需要用到this對象)。這里的相關知識在**本專欄**`《柯里化》`**和**`《this對象》`的文章里也有提到。本文依然堅持突出核心代碼,盡可能剝離無關功能點的思路行文因此不做贅述。
- 前言
- 寫在前言
- 一些開發遇到的問題
- H5標簽中的屬性控制
- el-table的每個對象的屬性值
- el-form多個表單同時驗證必填項
- el-table 修改表頭
- el-input的多種驗證
- vue鍵盤回車事件
- blob導出
- table中selectable( 是否勾選)
- 手動更新視圖
- 日期選擇器,自定義可選范圍
- select 自定義搜索
- 監聽回車事件
- 表格初始化不可勾選
- el-input輸入限制
- table時間格式轉換
- table自適應高度
- JS問題記錄
- js字符數組轉換為數字數組
- js防抖和節流
- JS電腦是否有網判斷
- JS屬性記錄
- 遍歷方法(12個)
- 改變原數組(9個)
- 不改變原數組(8個)
- JS數組、字符串常用方法
- 遍歷對象
- Vue
- vue-router
- vue-router 如何在新窗口打開頁面
- vue-router 之 keep-alive緩存篇
- keep-alive項目案例
- 路由知識點歸納總結
- params、query傳參
- vue問題記錄
- vuejs npm chromedriver 報錯
- vuex
- vuex個人理解
- Vuex的簡單實例應用