在頁面上的某些事件觸發頻率很是高,好比滾動條滾動、窗口尺寸變化、鼠標移動等,若是咱們須要註冊這類事件,不得不考慮效率問題。而防抖函數就是爲了解決這一類問題而出現的。javascript
在頁面上的某些事件觸發頻率很是高,好比滾動條滾動、窗口尺寸變化、鼠標移動等,若是咱們須要註冊這類事件,不得不考慮效率問題。html
當窗口尺寸發生變化時,哪怕只變化了一點點,都有可能形成成百上千次對處理函數的調用,這對網頁性能的影響是極其巨大的。java
因而,咱們能夠考慮,每次窗口尺寸變化、滾動條滾動、鼠標移動時,不要當即執行相關操做,而是等一段時間,以窗口尺寸中止變化、滾動條再也不滾動、鼠標再也不移動爲計時起點,一段時間後再去執行操做。git
咱們來列舉一個關於鼠標移動的例子:github
<div id="container"></div>
複製代碼
div{
height: 200px;
line-height: 200px;
text-align: center; color: #fff;
background-color: #444;
font-size: 25px;
border-radius: 3px;
}
複製代碼
let count = 1;
let container = document.getElementsByTagName('div')[0];
function updateCount() {
container.innerHTML = count ++ ;
}
container.addEventListener('mousemove',updateCount);
複製代碼
咱們來看一下效果:瀏覽器
咱們能夠看到,鼠標從左側滑到右側,咱們綁定的事件執行了119屢次緩存
這個例子很簡單,瀏覽器徹底反應的過來,但若是在頻繁的事件回調中作複雜計算,頗有可能致使頁面卡頓,不如將屢次計算合併爲一次計算,只在一個精確點作操做。app
爲了處理這個問題,通常有兩種解決方案:函數
PS:防抖函數和節流節流函數的做用都是防止函數屢次調用。區別在於,假設一個用戶一直觸發這個函數,咱們設定一個最小觸發時間,當每次觸發函數的間隔小於最小觸發時間,防抖的狀況下只會調用一次,而節流的 狀況會每隔一個最小觸發時間調用函數。性能
關於節流函數部分,請看下一篇文章。
防抖的原理就是:你儘管觸發事件,可是我必定在事件觸發 n 秒後才執行,若是你在一個事件觸發的 n 秒內又觸發了這個事件,那我就以新的事件的時間爲準,n 秒後才執行,總之,就是要等你觸發完事件 n 秒內再也不觸發事件,我才執行,真是任性吶!
/**
* 防抖函數
* @param func 用戶傳入的防抖函數
* @param wait 等待的時間
*/
const debounce = function (func,wait = 50) {
// 緩存一個定時器id
let timer = null;
// 這裏返回的函數時每次用戶實際調用的防抖函數
// 若是已經設定過定時器了就清空上一次的定時器
// 開始一個定時器,延遲執行用戶傳入的方法
return function(...args){
if(timer) clearTimeout(timer);
timer = setTimeout(()=>{
//將實際的this和參數傳入用戶實際調用的函數
func.apply(this,args);
},wait);
}
};
複製代碼
使用這個防抖函數應用在最開始的例子上:
container.addEventListener('mousemove',debounce(updateCount,100));
複製代碼
咱們能夠看到,無論咱們怎麼移動,咱們綁定的回調事件都是在鼠標中止後100ms後纔會觸發。
這是一個簡單版的防抖,可是有缺陷,這個防抖只能在最後調用。通常的防抖會有immediate選項,表示是否當即調用。這二者的區別,舉個栗子來講:
/**
* 防抖函數
* @param func 用戶傳入的防抖函數
* @param wait 等待的時間
* @param immediate 是否當即執行
*/
const debounce = function (func,wait = 50,immediate = false) {
// 緩存一個定時器id
let timer = null;
// 這裏返回的函數時每次用戶實際調用的防抖函數
return function(...args){
// 若是已經設定過定時器了就清空上一次的定時器
if(timer) clearTimeout(timer);
if(immediate){
let callNow = !timer;
//等待wait的時間間隔後,timer爲null的時候,函數才能夠繼續執行
timer = setTimeout(()=>{
timer = null;
},wait);
//未執行過,執行
if(callNow) func.apply(this,args);
}else{
// 開始一個定時器,延遲執行用戶傳入的方法
timer = setTimeout(()=>{
//將實際的this和參數傳入用戶實際調用的函數
func.apply(this,args);
},wait);
}
}
};
複製代碼
此時要注意,用戶傳入的函數多是有返回值的,可是當immediate爲false的時候,由於使用了setTimeout,函數的返回值永遠爲undefined,因此咱們只在immediate爲true的時候返回函數的返回值
/**
* 防抖函數
* @param func 用戶傳入的防抖函數
* @param wait 等待的時間
* @param immediate 是否當即執行
*/
const debounce = function (func,wait = 50,immediate = false) {
// 緩存一個定時器id
let timer = null;
let result;
// 這裏返回的函數時每次用戶實際調用的防抖函數
return function(...args){
// 若是已經設定過定時器了就清空上一次的定時器
if(timer) clearTimeout(timer);
if(immediate){
let callNow = !timer;
//等待wait的時間間隔後,timer爲null的時候,函數才能夠繼續執行
timer = setTimeout(()=>{
timer = null;
},wait);
//未執行過,執行
if(callNow) result = func.apply(this,args);
}else{
// 開始一個定時器,延遲執行用戶傳入的方法
timer = setTimeout(()=>{
//將實際的this和參數傳入用戶實際調用的函數
func.apply(this,args);
},wait);
}
return result;
}
};
複製代碼
最後咱們再思考一個小需求,我但願能取消 debounce 函數,好比說我 debounce 的時間間隔是 10 秒鐘,immediate 爲 true,這樣的話,我只有等 10 秒後才能從新觸發事件,如今我但願有一個按鈕,點擊後,取消防抖,這樣我再去觸發,就能夠又馬上執行啦
/**
* 防抖函數
* @param func 用戶傳入的防抖函數
* @param wait 等待的時間
* @param immediate 是否當即執行
*/
const debounce = function (func,wait = 50,immediate = false) {
// 緩存一個定時器id
let timer = null;
let result;
let debounced = function (...args) {
// 若是已經設定過定時器了就清空上一次的定時器
if(timer) clearTimeout(timer);
if(immediate){
let callNow = !timer;
//等待wait的時間間隔後,timer爲null的時候,函數才能夠繼續執行
timer = setTimeout(()=>{
timer = null;
},wait);
//未執行過,執行
if(callNow) result = func.apply(this,args);
}else{
// 開始一個定時器,延遲執行用戶傳入的方法
timer = setTimeout(()=>{
//將實際的this和參數傳入用戶實際調用的函數
func.apply(this,args);
},wait);
}
return result;
};
debounced.cancel = function(){
clearTimeout(timer);
timer = null;
};
// 這裏返回的函數時每次用戶實際調用的防抖函數
return debounced;
};
複製代碼
在原頁面的基礎上,修改以下
div{
height: 200px;
line-height: 200px;
text-align: center; color: #fff;
background-color: #444;
font-size: 25px;
border-radius: 3px;
}
複製代碼
<div id="container"></div>
<button id="cancel">點擊取消防抖</button>
複製代碼
let count = 1;
let container = document.getElementsByTagName('div')[0];
let button = document.getElementById('cancel');
function updateCount() {
container.innerHTML = count ++ ;
}
let action = debounce(updateCount,10000,true);
container.addEventListener('mousemove',action);
button.addEventListener('click',action.cancel);
複製代碼
至此咱們已經完成了一個 debounce 函數