文章目錄
- 前言:為什么需要防抖和節流
- 基本概念與區別
- 防抖(Debounce)
- 節流(Throttle)
- 關鍵區別
- 防抖(Debounce)詳解
- 1. 基本防抖函數實現
- 2. 防抖函數的使用
- 3. 防抖函數的工作流程
- 4. 防抖函數進階 - 立即執行選項
- 節流(Throttle)詳解
- 1. 基本節流函數實現
- 時間戳法(第一次會立即執行)
- 定時器法(第一次會延遲執行)
- 2. 節流函數的使用
- 3. 節流函數的工作流程
- 4. 節流函數進階 - 首尾調用控制
- 實際應用場景
- 1. 搜索框輸入防抖
- 2. 滾動加載節流
- 3. 按鈕點擊防抖(防止重復提交)
- 4. 窗口調整大小節流
- 防抖與節流的進階實現
- 1. 可取消的防抖函數
- 2. 可取消的節流函數
- 3. 帶返回值的防抖函數(使用Promise)
- 常見問題與解決方案
- 1. 防抖函數內無法訪問this和事件對象
- 2. React組件中使用防抖/節流
- 3. 函數依賴項變化時重置防抖/節流
- 第三方庫的實現
- 1. Lodash
- 2. Underscore
- 3. RxJS
- 總結與最佳實踐
- 1. 選擇合適的技術
- 2. 性能考慮
- 3. 最佳實踐
- 4. 回顧關鍵概念
- 2. 性能考慮
- 3. 最佳實踐
- 4. 回顧關鍵概念
前言:為什么需要防抖和節流
在前端開發中,我們經常會遇到一些高頻觸發的事件,例如:
- 瀏覽器窗口調整大小(resize)
- 頁面滾動(scroll)
- 鼠標移動(mousemove)
- 鍵盤輸入(keyup、keydown)
- 頻繁點擊按鈕等
如果不加控制,這些事件處理函數可能會在短時間內被頻繁調用,導致以下問題:
- 性能問題:函數頻繁執行,特別是復雜計算或DOM操作,會導致頁面卡頓
- 資源浪費:例如輸入搜索時,頻繁發送不必要的API請求
- 不良用戶體驗:例如按鈕重復點擊導致的重復提交表單
看一個具體例子:
// 不做任何處理的搜索輸入框
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', function() {// 每次輸入都會執行,即使用戶正在快速輸入console.log('執行搜索:', this.value);fetchSearchResults(this.value); // 發送請求獲取搜索結果
});
在上面的例子中,當用戶快速輸入"JavaScript"這個詞時,可能會依次觸發以下請求:
- 搜索"J"
- 搜索"Ja"
- 搜索"Jav"
- 搜索"Java"
- 搜索"JavaS"
- …
- 搜索"JavaScript"
顯然,除了最后一個請求,前面的所有請求都是不必要的,這不僅浪費了網絡資源,還可能導致服務器壓力過大。
這就是為什么我們需要防抖和節流技術:它們幫助我們控制函數的執行頻率,優化性能和用戶體驗。
基本概念與區別
防抖(Debounce)
概念:函數防抖是指在事件被觸發n秒后再執行回調,如果在這n秒內事件又被觸發,則重新計時。
形象比喻:電梯關門 - 當有人進入電梯后,電梯會等待一段時間再關門,如果在這段時間內又有人進入,電梯會重新計時等待,直到一段時間內沒有人進入才會關門。
典型場景:
- 搜索框輸入,等用戶輸入完畢后再發送請求
- 窗口調整大小完成后執行重排重繪
- 按鈕提交事件防止重復提交
節流(Throttle)
概念:函數節流是指規定一個單位時間,在這個單位時間內,只能有一次觸發事件的回調函數執行,如果在同一個單位時間內某事件被觸發多次,只有一次能生效。
形象比喻:水龍頭控制水流 - 無論你如何快速地多次擰開水龍頭,水流速度都不會超過水管的限制。
典型場景:
- 滾動事件處理
- 射擊游戲中的武器發射頻率限制
- 鼠標移動事件處理
關鍵區別
特性 | 防抖(Debounce) | 節流(Throttle) |
---|---|---|
執行時機 | 在一段時間內沒有再次觸發事件后執行 | 在一段時間內只執行一次 |
適用場景 | 需要等待操作完全結束后執行 | 需要保持一定的執行頻率 |
執行頻率 | 不穩定,取決于事件觸發頻率和間隔 | 穩定,保證一定時間內執行一次 |
最后一次是否執行 | 延遲執行,一定會執行 | 可能不會執行最后一次(取決于實現) |
下面通過可視化圖表來理解兩者的區別:
連續事件觸發:
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
│ │ │ │ │ │ │ │ │ │
0 1 2 3 4 5 6 7 8 9 (時間軸)防抖(延遲3秒):
─────────────────────→ (只在最后一次事件后等待3秒執行)↓9+3=12節流(間隔3秒):
↓ ↓ ↓ (每隔3秒執行一次)
│ │ │
0 3 6 (時間軸)
防抖(Debounce)詳解
1. 基本防抖函數實現
/*** 基礎版防抖函數* @param {Function} func - 需要防抖的函數* @param {number} wait - 等待時間,單位毫秒* @returns {Function} - 防抖處理后的函數*/
function debounce(func, wait) {let timeout;return function() {const context = this; // 保存this指向const args = arguments; // 保存傳入的參數// 清除之前的定時器clearTimeout(timeout);// 設置新的定時器timeout = setTimeout(function() {func.apply(context, args);}, wait);};
}
2. 防抖函數的使用
// 定義一個可能頻繁調用的函數
function handleSearch(searchTerm) {console.log('Searching for:', searchTerm);// 發送API請求等操作...
}// 獲取輸入框元素
const searchInput = document.getElementById('search-input');// 未使用防抖的版本 - 每次輸入都會執行
/*
searchInput.addEventListener('input', function() {handleSearch(this.value);
});
*/// 使用防抖后的版本 - 停止輸入300毫秒后才執行
const debouncedSearch = debounce(function() {handleSearch(this.value);
}, 300);searchInput.addEventListener('input', debouncedSearch);
3. 防抖函數的工作流程
以搜索框為例,當用戶連續輸入"hello"這個詞:
時間軸: 0ms 100ms 200ms 300ms 400ms 700ms
操作: h e l l o (停止輸入)
函數調用: 無 無 無 無 無 執行搜索"hello"
每次按鍵都會重置定時器,只有當用戶停止輸入300ms后,才會執行一次搜索。
4. 防抖函數進階 - 立即執行選項
在某些場景下,我們可能希望第一次觸發事件時立即執行函數,然后等待一段時間再允許執行下一次。例如點擊提交按鈕時,我們希望立即響應第一次點擊,然后暫時禁用后續點擊。
/*** 帶立即執行選項的防抖函數* @param {Function} func - 需要防抖的函數* @param {number} wait - 等待時間,單位毫秒* @param {boolean} immediate - 是否立即執行* @returns {Function} - 防抖處理后的函數*/
function debounce(func, wait, immediate) {let timeout;return function() {const context = this;const args = arguments;const later = function() {timeout = null;if (!immediate) func.apply(context, args);};const callNow = immediate && !timeout;clearTimeout(timeout);timeout = setTimeout(later, wait);if (callNow) func.apply(context, args);};
}// 使用立即執行的防抖 - 第一次點擊立即響應,后續點擊被防抖
const button = document.getElementById('submit-button');
const immediateDebounceClick = debounce(function() {console.log('Button clicked!');// 提交表單等操作...
}, 1000, true);button.addEventListener('click', immediateDebounceClick);
節流(Throttle)詳解
1. 基本節流函數實現
有兩種常見的方式實現節流函數:時間戳法和定時器法。
時間戳法(第一次會立即執行)
/*** 時間戳實現的節流函數* @param {Function} func - 需要節流的函數* @param {number} wait - 等待時間,單位毫秒* @returns {Function} - 節流處理后的函數*/
function throttle(func, wait) {let previous = 0; // 上一次執行的時間戳return function() {const now = Date.now(); // 當前時間戳const context = this;const args = arguments;// 如果當前時間與上一次執行時間差大于等待時間if (now - previous > wait) {func.apply(context, args);previous = now;}};
}
定時器法(第一次會延遲執行)