Debounce 和 throttle 是咱們在 JavaScript 中使用的兩個概念,用於加強對函數執行的控制,這在事件處理程序中特別有用。這兩種技術都回答了同一個問題「一段時間內某個函數的調用頻率是多少?」前端
文中內容多數來自如下文章,侵刪!git
本是機械開關的「去彈跳」概念,彈簧開關按下後,因爲簧片的做用,接觸點會連續接觸斷開好屢次,若是每次接觸都通電對用電器很差,因此就要控制按下到穩定的這段時間不通電github
前端開發中則是一些頻繁的事件觸發瀏覽器
mousemove
...)鍵盤(keydown
...)事件等在 debounce 函數沒有再被調用的狀況下通過 delay 毫秒後才執行回調函數,例如閉包
mousemove
事件中,確保屢次觸發只調用一次監聽函數user
,就會分紅u
,us
,use
,user
四次發出請求;而添加防抖,設置好時間,能夠實現完整輸入user
才發出校驗請求由 debounce 的功能可知防抖函數至少接收兩個參數(流行類庫中都是 3 個參數)app
fn
delay
debounce 函數返回一個閉包,閉包被頻繁的調用函數
fn
的執行,強制只有連續操做中止後執行一次使用閉包是爲了使指向定時器的變量不被gc
回收工具
delay
內的連續觸發都不執行回調函數fn
,使用的是在閉包內設置定時器setTimeOut
符合原理的簡單實現學習
function debounce(fn, delay) {
var timer;
return function() {
// 清除上一次調用時設置的定時器
// 計時器清零
clearTimeout(timer);
// 從新設置計時器
timer = setTimeout(fn, delay);
};
}
複製代碼
簡單實現的代碼,可能會形成兩個問題優化
this
指向問題。debounce 函數在定時器中調用回調函數fn
,因此fn
執行的時候this
指向全局對象(瀏覽器中window
),須要在外層用變量將this
保存下來,使用apply
進行顯式綁定
function debounce(fn, delay) {
var timer;
return function() {
// 保存調用時的this
var context = this;
clearTimeout(timer);
timer = setTimeout(function() {
// 修正 this 的指向
fn.apply(this);
}, delay);
};
}
複製代碼
event
對象。JavaScript 的事件處理函數中會提供事件對象event
,在閉包中調用時須要將這個事件對象傳入
function debounce(fn, delay) {
var timer;
return function() {
// 保存調用時的this
var context = this;
// 保存參數
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
console.log(context);
// 修正this,並傳入參數
fn.apply(context, args);
}, delay);
};
}
複製代碼
underscore
的實現)馬上執行。增長第三個參數,兩種狀況
fn
,等到中止觸發後的delay
毫秒,才能夠再次觸發(先執行)delay
毫秒後才執行回調函數(後執行)clearTimeout(timer)
後,timer
並不會變成null
,而是依然指向定時器對象function debounce(fn, delay, immediate) {
var timer;
return function() {
var context = this;
var args = arguments;
// 中止定時器
if (timer) clearTimeout(timer);
// 回調函數執行的時機
if (immediate) {
// 是否已經執行過
// 執行過,則timer指向定時器對象,callNow 爲 false
// 未執行,則timer 爲 null,callNow 爲 true
var callNow = !timer;
// 設置延時
timer = setTimeout(function() {
timer = null;
}, delay);
if (callNow) fn.apply(context, args);
} else {
// 中止調用後delay時間才執行回調函數
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
}
};
}
複製代碼
返回值與取消 debounce 函數
undefined
immediate
爲true
的時候,觸發一次後要等待delay
時間後才能再次觸發,可是想要在這個時間段內想要再次觸發,能夠先取消掉以前的 debounce 函數function debounce(fn, delay, immediate) {
var timer, result;
var debounced = function() {
var context = this;
var args = arguments;
// 中止定時器
if (timer) clearTimeout(timer);
// 回調函數執行的時機
if (immediate) {
// 是否已經執行過
// 執行過,則timer指向定時器對象,callNow 爲 false
// 未執行,則timer 爲 null,callNow 爲 true
var callNow = !timer;
// 設置延時
timer = setTimeout(function() {
timer = null;
}, delay);
if (callNow) result = fn.apply(context, args);
} else {
// 中止調用後delay時間才執行回調函數
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
}
// 返回回調函數的返回值
return result;
};
// 取消操做
debounced.cancel = function() {
clearTimeout(timer);
timer = null;
};
return debounced;
}
複製代碼
ES6 寫法
function debounce(fn, delay, immediate) {
let timer, result;
// 這裏不能使用箭頭函數,否則 this 依然會指向 Windows對象
// 使用rest參數,獲取函數的多餘參數
const debounced = function(...args) {
if (timer) clearTimeout(timer);
if (immediate) {
const callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
if (callNow) result = fn.apply(this, args);
} else {
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
return result;
};
debounced.cancel = () => {
clearTimeout(timer);
timer = null;
};
return debounced;
}
複製代碼
固定函數執行的速率
若是持續觸發事件,每隔一段時間,執行一次事件
mousemove
事件時,無論鼠標移動的速度,【節流】後的監聽函數會在 wait 秒內最多執行一次,並以此【勻速】觸發執行window
的 resize
、scroll
事件的優化等
有兩種主流實現方式
節流函數 throttle 調用後返回一個閉包
時間戳方式
定時器方式
將兩種方式結合,能夠實現兼併馬上執行和中止觸發後依然執行一次的效果
時間戳實現
function throttle(fn, wait) {
var args;
// 前一次執行的時間戳
var previous = 0;
return function() {
// 將時間轉爲時間戳
var now = +new Date();
args = arguments;
// 時間間隔大於延遲時間才執行
if (now - previous > wait) {
fn.apply(this, args);
previous = now;
}
};
}
複製代碼
previous
爲 0,除非設置的時間間隔大於當前時間的時間戳,不然差值確定大於時間間隔)定時器實現
function throttle(fn, wait) {
var timer, context, args;
return function() {
context = this;
args = arguments;
// 若是定時器存在,則不執行
if (!timer) {
timer = setTimeout(function() {
// 執行後釋放定時器變量
timer = null;
fn.apply(context, args);
}, wait);
}
};
}
複製代碼
結合時間戳和定時器實現
function throttle(fn, wait) {
var timer, context, args;
var previous = 0;
// 延時執行函數
var later = function() {
previous = +new Date();
// 執行後釋放定時器變量
timer = null;
fn.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = +new Date();
// 距離下次執行 fn 的時間
// 若是人爲修改系統時間,可能出現 now 小於 previous 狀況
// 則剩餘時間可能超過期間週期 wait
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 沒有剩餘時間 || 修改系統時間致使時間異常,則會當即執行回調函數fn
// 初次調用時,previous爲0,除非wait大於當前時間的時間戳,不然剩餘時間必定小於0
if (remaining <= 0 || remaining > wait) {
// 若是存在延時執行定時器,將其取消掉
if (timer) {
clearTimeout(timer);
timer = null;
}
previous = now;
fn.apply(context, args);
if (!timeout) context = args = null;
} else if (!timer) {
// 設置延時執行
timer = setTimeout(later, remaining);
}
};
return throttled;
}
複製代碼
增長第三個參數,讓用戶能夠本身選擇模式
{ leading: false }
{ trailing: false }
增長返回值功能
增長取消功能
function throttle(func, wait, options) {
var context, args, result;
var timeout = null;
// 上次執行時間點
var previous = 0;
if (!options) options = {};
// 延遲執行函數
var later = function() {
// 若設定了開始邊界不執行選項,上次執行時間始終爲0
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
// func 可能會修改 timeout 變量
result = func.apply(context, args);
// 定時器變量引用爲空,表示最後一次執行,則要清除閉包引用的變量
if (!timeout) context = args = null;
};
var throttled = function() {
var now = new Date().getTime();
// 首次執行時,若是設定了開始邊界不執行選項,將上次執行時間設定爲當前時間。
if (!previous && options.leading === false) previous = now;
// 延遲執行時間間隔
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 延遲時間間隔remaining小於等於0,表示上次執行至此所間隔時間已經超過一個時間窗口
// remaining 大於時間窗口 wait,表示客戶端系統時間被調整過
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
// 返回回調函數執行後的返回值
return result;
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
}
複製代碼
leading: false
和 trailing: false
不能同時設置
previous
爲 0,則remaining
值和wait
相等。因此,if (!previous && options.leading === false)
爲真,改變了previous
的值,而if (remaining <= 0 || remaining > wait)
爲假if (!previous && options.leading === false)
爲假,而if (remaining <= 0 || remaining > wait)
爲真。就變成了開始邊界執行。這樣就和leading: false
衝突了underscore
中的 debounce 函數和 throttle 函數lodash
中 debounce 函數和 throttle 函數的實現更加複雜,封裝更加完全throttle 和 debounce 是解決請求和響應速度不匹配問題的兩個方案。兩者的差別在於選擇不一樣的策略
電梯超時現象解釋二者區別。假設電梯設定爲 15 秒,不考慮容量限制
throttle
策略:保證若是電梯第 1 我的進來後,15 秒後準時送一次,不等待。若是沒有人,則待機、debounce
策略:若是電梯有人進來,等待 15 秒,若是又有人進來,從新計時 15 秒,直到 15 秒超時都沒有人再進來,則開始運送