一都许、防抖debounce
你是否在日常開發(fā)中遇到一個問題命辖,在滾動事件中需要做個復雜計算或者實現(xiàn)一個按鈕的防二次點擊操作
- 這些需求都可以通過函數(shù)防抖動來實現(xiàn)。如果在頻繁的事件回調(diào)中做復雜計算恤批,很有可能導致頁面卡頓异吻,不如將多次計算合并為一次計算,只在一個精確點做操作
- 防抖和節(jié)流的作用都是防止函數(shù)多次調(diào)用。區(qū)別在于诀浪,假設一個用戶一直觸發(fā)這個函數(shù)棋返,且每次觸發(fā)函數(shù)的間隔小于
wait
,防抖的情況下只會調(diào)用一次雷猪,而節(jié)流的 情況會每隔一定時間(參數(shù)wait
)調(diào)用函數(shù)
持續(xù)觸發(fā)
scroll
事件時睛竣,并不執(zhí)行handle
函數(shù),當1000
毫秒內(nèi)沒有觸發(fā)scroll
事件時求摇,才會延時觸發(fā)scroll
事件
// 防抖
function debounce(fn, wait) {
var timeout = null;
return function() {
if(timeout !== null) clearTimeout(timeout);
timeout = setTimeout(fn, wait);
}
}
// 處理函數(shù)
function handle() {
console.log(Math.random());
}
// 滾動事件
// 當持續(xù)觸發(fā)scroll事件時射沟,事件處理函數(shù)handle只在停止?jié)L動1000毫秒之后才會調(diào)用一次,也就是說在持續(xù)觸發(fā)scroll事件的過程中与境,事件處理函數(shù)handle一直沒有執(zhí)行
window.addEventListener('scroll', debounce(handle, 1000));
我們先來看一個袖珍版的防抖理解一下防抖的實現(xiàn)
// func是用戶傳入需要防抖的函數(shù)
// wait是等待時間
const debounce = (func, wait = 50) => {
// 緩存一個定時器id
let timer = 0
// 這里返回的函數(shù)是每次用戶實際調(diào)用的防抖函數(shù)
// 如果已經(jīng)設定過定時器了就清空上一次的定時器
// 開始一個新的定時器验夯,延遲執(zhí)行用戶傳入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不難看出如果用戶調(diào)用該函數(shù)的間隔小于wait的情況下,上一次的時間還未到就被清除了摔刁,并不會執(zhí)行函數(shù)
這是一個簡單版的防抖挥转,但是有缺陷,這個防抖只能在最后調(diào)用共屈。一般的防抖會有
immediate
選項绑谣,表示是否立即調(diào)用。這兩者的區(qū)別趁俊,舉個栗子來說
- 例如在搜索引擎搜索問題的時候域仇,我們當然是希望用戶輸入完最后一個字才調(diào)用查詢接口刑然,這個時候適用延遲執(zhí)行的防抖函數(shù)寺擂,它總是在一連串(間隔小于
wait
的)函數(shù)觸發(fā)之后調(diào)用。 - 例如用戶給
interviewMap
點star
的時候泼掠,我們希望用戶點第一下的時候就去調(diào)用接口怔软,并且成功之后改變star
按鈕的樣子,用戶就可以立馬得到反饋是否star
成功了择镇,這個情況適用立即執(zhí)行的防抖函數(shù)挡逼,它總是在第一次調(diào)用,并且下一次調(diào)用必須與前一次調(diào)用的時間間隔大于wait
才會觸發(fā)
完整代碼
下面我們來實現(xiàn)一個帶有立即執(zhí)行選項的防抖函數(shù)
// 這個是用來獲取當前時間戳的
function now() {
return +new Date()
}
/**
* 防抖函數(shù)腻豌,返回函數(shù)連續(xù)調(diào)用時家坎,空閑時間必須大于或等于 wait,func 才會執(zhí)行
*
* @param {function} func 回調(diào)函數(shù)
* @param {number} wait 表示時間窗口的間隔
* @param {boolean} immediate 設置為ture時吝梅,是否立即調(diào)用函數(shù)
* @return {function} 返回客戶調(diào)用函數(shù)
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延遲執(zhí)行函數(shù)
const later = () => setTimeout(() => {
// 延遲函數(shù)執(zhí)行完畢虱疏,清空緩存的定時器序號
timer = null
// 延遲執(zhí)行的情況下,函數(shù)會在延遲函數(shù)中執(zhí)行
// 使用到之前緩存的參數(shù)和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 這里返回的函數(shù)是每次實際調(diào)用的函數(shù)
return function(...params) {
// 如果沒有創(chuàng)建延遲執(zhí)行函數(shù)(later)苏携,就創(chuàng)建一個
if (!timer) {
timer = later()
// 如果是立即執(zhí)行做瞪,調(diào)用函數(shù)
// 否則緩存參數(shù)和調(diào)用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延遲執(zhí)行函數(shù)(later),調(diào)用的時候清除原來的并重新設定一個
// 這樣做延遲函數(shù)會重新計時
} else {
clearTimeout(timer)
timer = later()
}
}
}
- 對于按鈕防點擊來說的實現(xiàn):如果函數(shù)是立即執(zhí)行的,就立即調(diào)用装蓬,如果函數(shù)是延遲執(zhí)行的著拭,就緩存上下文和參數(shù),放到延遲函數(shù)中去執(zhí)行牍帚。一旦我開始一個定時器儡遮,只要我定時器還在,你每次點擊我都重新計時暗赶。一旦你點累了峦萎,定時器時間到,定時器重置為
null
忆首,就可以再次點擊了爱榔。 - 對于延時執(zhí)行函數(shù)來說的實現(xiàn):清除定時器ID,如果是延遲調(diào)用就調(diào)用函數(shù)
二糙及、節(jié)流throttle
防抖動和節(jié)流本質(zhì)是不一樣的详幽。防抖動是將多次執(zhí)行變?yōu)樽詈笠淮螆?zhí)行,節(jié)流是將多次執(zhí)行變成每隔一段時間執(zhí)行
如下圖浸锨,持續(xù)觸發(fā)scroll
事件時唇聘,并不立即執(zhí)行handle
函數(shù),每隔1000
毫秒才會執(zhí)行一次handle
函數(shù)
節(jié)流版本
/**
* underscore 節(jié)流函數(shù)柱搜,返回函數(shù)連續(xù)調(diào)用時迟郎,func 執(zhí)行頻率限定為 次 / wait
*
* @param {function} func 回調(diào)函數(shù)
* @param {number} wait 表示時間窗口的間隔
* @param {object} options 如果想忽略開始函數(shù)的的調(diào)用,傳入{leading: false}聪蘸。
* 如果想忽略結尾函數(shù)的調(diào)用宪肖,傳入{trailing: false}
* 兩者不能共存,否則函數(shù)不能執(zhí)行
* @return {function} 返回客戶調(diào)用函數(shù)
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的時間戳
var previous = 0;
// 如果 options 沒傳則設為空對象
if (!options) options = {};
// 定時器回調(diào)函數(shù)
var later = function() {
// 如果設置了 leading健爬,就將 previous 設為 0
// 用于下面函數(shù)的第一個 if 判斷
previous = options.leading === false ? 0 : _.now();
// 置空一是為了防止內(nèi)存泄漏控乾,二是為了下面的定時器判斷
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 獲得當前時間戳
var now = _.now();
// 首次進入前者肯定為 true
// 如果需要第一次不執(zhí)行函數(shù)
// 就將上次時間戳設為當前的
// 這樣在接下來計算 remaining 的值時會大于0
if (!previous && options.leading === false) previous = now;
// 計算剩余時間
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果當前調(diào)用已經(jīng)大于上次調(diào)用時間 + wait
// 或者用戶手動調(diào)了時間
// 如果設置了 trailing,只會進入這個條件
// 如果沒有設置 leading娜遵,那么第一次會進入這個條件
// 還有一點蜕衡,你可能會覺得開啟了定時器那么應該不會進入這個 if 條件了
// 其實還是會進入的,因為定時器的延時
// 并不是準確的時間设拟,很可能你設置了2秒
// 但是他需要2.2秒才觸發(fā)慨仿,這時候就會進入這個條件
if (remaining <= 0 || remaining > wait) {
// 如果存在定時器就清理掉否則會調(diào)用二次回調(diào)
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判斷是否設置了定時器和 trailing
// 沒有的話就開啟一個定時器
// 并且不能不能同時設置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};