背景:
在前端開發中,我們會經常需要綁定一些持續觸發的事件,如resize, scroll, mousemove等,但是有時候不希望在事件持續觸發的過程中太頻繁地執行函數
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="content" style=" height: 150px; line-height: 150px; text-align: center; color: #fff; background-color: #ccc; font-size: 80px; " ></div> <script> let num = 1; const content = document.getElementById("content"); function count() { content.innerHTML = num++; } content.onmousemove = count; </script> </body> </html>
在上述代碼中,div 元素綁定了 mousemove 事件,當鼠標在 div(灰色)區域中移動的時候會持續地去觸發該事件導致頻繁執行函數

解決方案:防抖和節流
1. 防抖(debounce):是指觸發事件后n秒才執行函數,如果在n秒內又觸發事件了,則會重新計算函數時間
(case1:做電梯,電梯會等沒人進來了,再等一會,才關閉;case2: input輸入框,輸入內容搜索,在停下輸入過一會后,才發起請求獲取匹配的數據;case3: 玩游戲讀條,只有讀條結束才可以輸出,如果讀條被打斷,要重新讀條)
1.1 非立即執行版防抖:事件觸發后不立即執行,n秒后執行
<script> let num = 1; const content = document.getElementById("content"); function count() { content.innerHTML = num++; } // 非立即執行版:觸發事件后函數不會立即執行,而是在n秒后執行, // 如果n秒內又觸發了事件,則會重新計算函數執行時間 function debounce(func, wait) { let timeout; return function () { const context = this; const args = [...arguments]; if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { func.apply(context, args); }, wait); }; } content.onmousemove = debounce(count, 1000); </script>
1.2 立即執行版防抖:觸發事件后函數立即執行,n秒內不觸發事件才能繼續執行
// 立即執行版: 觸發事件后函數會立即執行,然后n秒內不觸發事件才能繼續執行的效果 function debounce(func, wait) { let timeout; return function () { const context = this; const args = [...arguments]; if (timeout) { clearTimeout(timeout); } const callNow = !timeout; timeout = setTimeout(() => { timeout = null; }, wait); if (callNow) { func.apply(context, args); } }; }
1.3 結合版本:實際開發過程中,需要根據不同場景來決定使用哪一個版本的防抖函數。上述版本可以合並:
function debounce(func, wait, immediate) { let timeout; return function () { const context = this; const args = [...arguments]; if (timeout) { clearTimeout(timeout); } if (immediate) { const callNow = !timeout; timeout = setTimeout(() => { timeout = null; }, wait); if (callNow) { func.apply(context, args); } } else { timeout = setTimeout(() => { func.apply(context, args); }, wait); } }; } content.onmousemove = debounce(count, 1000, false);
2. 節流(throttle):是指連續觸發事件,但是在n秒中只執行一次的函數。節流會稀釋函數的執行頻率
(case 1: 看電影,每秒有24幀,意思是每1秒的動畫,播放了24張連續的圖片;case2: 滾動條加載更多,監聽滾動條位置時,設置每1s監聽一次,而不是無限次監聽;case3: 節流相當於玩游戲某技能的cd,cd時間未到不會執行)
2.1 時間戳版節流:在持續觸發事件的過程中,函數會立即執行,並且每1s執行一次
function throttle(func, wait) { var previous = 0; return function () { let now = Date.now(); let context = this; let args = arguments; if (now - previous > wait) { func.apply(context, args); previous = now; } }; }
2.2 定時器版節流:在持續觸發事件的過程中,函數不會立即執行,每1s執行一次,在停止觸發事件后,函數會再執行一次
function throttle(func, wait) { let timeout; return function () { let context = this; let args = arguments; if (!timeout) { timeout = setTimeout(() => { timeout = null; func.apply(context, args); }, wait); } }; }
2.3 時間戳版本節流和定時器節流的區別是,時間戳版本的函數觸發是在時間段內開始的時候,而定時器版的函數觸發是在時間段內結束的時候。
兩者合並,如下版本:
/** * @desc 函數節流 * @param func 函數 * @param wait 延遲執行毫秒數 * @param type 1 表時間戳版,2 表定時器版 */ function throttle(func, wait ,type) { if(type===1){ let previous = 0; }else if(type===2){ let timeout; } return function() { let context = this; let args = arguments; if(type===1){ let now = Date.now(); if (now - previous > wait) { func.apply(context, args); previous = now; } }else if(type===2){ if (!timeout) { timeout = setTimeout(() => { timeout = null; func.apply(context, args) }, wait) } } } }
3. 防抖和節流的區別
- 用戶在搜索的時候,在不停敲字,如果每敲一個字我們就要調一次接口,接口調用太頻繁,給卡住了。-----用防抖
- 用戶在閱讀文章的時候,我們需要監聽用戶滾動到了哪個標題,但是每滾動一下就監聽,那樣會太過頻繁從而占內存,如果再加上其他的業務代碼,就卡住了。-----用節流
防抖:結合輸入框場景,只有當用戶輸入完畢一段時間后,才會調用接口,出現聯想詞
節流:指定時間間隔,只執行一次任務;應用場景:比如,懶加載監聽滾動條的位置,使用節流按一定頻率獲取
使用防抖和節流可以減少不必要的損耗(頻繁調取接口,網絡堵塞,增加服務器壓力)。
最后,匯總
參考:
https://juejin.cn/post/6844903651278848014
https://juejin.cn/post/6844904185117278215

