防抖與節(jié)流
前言
我們 JavaScript 中有一些事件,比如瀏覽器的 resize缀辩、scroll 事件肛搬,鼠標(biāo)的 mousemove蛇捌、mouseover 事件以及輸入框的 keypress、keyup 事件赫编,它們在觸發(fā)的時候會不斷調(diào)用事件綁定的回調(diào)函數(shù)巡蘸,極大的浪費(fèi)資源,降低前端性能擂送。為了優(yōu)化用戶體驗(yàn)悦荒,我們需要對這類事件進(jìn)行調(diào)用次數(shù)的限制。我們可以使用防抖與節(jié)流來降低事件的觸發(fā)頻率嘹吨。
防抖
1搬味、定義
當(dāng)連續(xù)觸發(fā)事件時,事件被觸發(fā) N 秒后才執(zhí)行回調(diào)躺苦,否則如果在這 n 秒內(nèi)又被觸發(fā)身腻,則重新計時
例:鼠標(biāo)移動事件
正常情況:從開始監(jiān)聽到鼠標(biāo)不再移動打印出無數(shù)次
防抖處理:只在鼠標(biāo)不再移動后 N 秒打印出鼠標(biāo)最后停止的位置(N 秒為人為設(shè)置)
2、為什么要防抖匹厘?
在某一函數(shù)在短時間內(nèi)被連續(xù)觸發(fā)導(dǎo)致占用大量性能使得系統(tǒng)卡頓嘀趟,而我們并不真正需要連續(xù)觸發(fā)這個函數(shù)。我們需要的是連續(xù)觸發(fā)中最后一次的結(jié)果愈诚,這時我們使用防抖來優(yōu)化函數(shù)的觸發(fā)她按,達(dá)到減少占用性能,防止連續(xù)觸發(fā)導(dǎo)致的卡頓炕柔。
3酌泰、防抖的實(shí)現(xiàn)
// 防抖
function debounce(fun, delay) {
return function (args) {
let that = this;
let _args = args;
clearTimeout(fun.id);
fun.id = setTimeout(function () {
fun.call(that, _args);
}, delay);
};
}
4、示例
移動鼠標(biāo)匕累,更新數(shù)字陵刹,數(shù)字記錄調(diào)用事件回調(diào)次數(shù)
4.1 正常調(diào)用
每次移動都會調(diào)用回調(diào)。
// 容器
const container = document.getElementById("container");
// 數(shù)字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
num += 1;
numid.innerHTML = num;
console.timeEnd("start");
}
const wrapFun = callback;
container.addEventListener("mousemove", function (e) {
console.time("start");
wrapFun();
});
4.2 防抖調(diào)用
每次移動間隔超過 1000ms 才會調(diào)用回調(diào)欢嘿,反之重新計算時間衰琐。
// 容器
const container = document.getElementById("container");
// 數(shù)字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
num += 1;
numid.innerHTML = num;
console.timeEnd("start");
}
const wrapFun = dobounce(callback, 1000);
container.addEventListener("mousemove", function (e) {
console.time("start");
wrapFun();
});
4.3 防抖優(yōu)化
首次調(diào)用立即執(zhí)行
// 防抖
function debounce(fun, delay, immediate) {
return function (args) {
let that = this;
let _args = args;
clearTimeout(fun.id);
if (immediate) {
// 如果已經(jīng)執(zhí)行過也糊,不再執(zhí)行
const callNow = !fun.id;
fun.id = setTimeout(function () {
fun.id = null;
}, delay);
if (callNow) fun.call(that, _args);
} else {
fun.id = setTimeout(function () {
fun.call(that, _args);
}, delay);
}
};
}
節(jié)流
1、定義
當(dāng)連續(xù)觸發(fā)事件時羡宙,每隔 N 時間狸剃,觸發(fā)一次函數(shù)
例:監(jiān)聽鼠標(biāo)在屏幕中的位置并打印,連續(xù)不斷的移動鼠標(biāo)一段時間狗热,獲取鼠標(biāo)的移動軌跡
正常情況:從開始監(jiān)聽到鼠標(biāo)不再移動打印出無數(shù)次钞馁,(軌跡最為精細(xì))
節(jié)流處理:當(dāng)我們不需要這樣精確的軌跡時,每隔 N 秒觸發(fā)一次函數(shù)打印鼠標(biāo)位置匿刮,得到較為粗糙的鼠標(biāo)軌跡(N 秒為人為設(shè)置)
2僧凰、為什么要節(jié)流?
同樣的僻焚,也是為了防止在某一函數(shù)在短時間內(nèi)被連續(xù)觸發(fā)導(dǎo)致占用大量性能使得系統(tǒng)卡頓允悦,而我們并不真正需要連續(xù)觸發(fā)這個函數(shù)。我們需要的是穩(wěn)定的每隔一段時間觸發(fā)一次虑啤,這時我們使用節(jié)流來優(yōu)化函數(shù)的觸發(fā)隙弛,達(dá)到減少占用性能,防止連續(xù)觸發(fā)導(dǎo)致的卡頓狞山。
3全闷、節(jié)流的實(shí)現(xiàn)
// 節(jié)流
function throttle(fun, delay) {
let last, deferTimer;
return function (args) {
let that = this;
let _args = arguments;
let now = +new Date();
// 保證第一次執(zhí)行
if (last && now < last + delay) {
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
fun.apply(that, _args);
}, delay);
} else {
last = now;
fun.apply(that, _args);
}
};
}
4、示例
移動鼠標(biāo)萍启,更新數(shù)字婚被,數(shù)字記錄調(diào)用事件回調(diào)次數(shù)
4.1 節(jié)流調(diào)用
首次調(diào)用立即執(zhí)行永罚,每隔 1000ms 調(diào)用一次吊说。
// 容器
const container = document.getElementById("container");
// 數(shù)字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
num += 1;
numid.innerHTML = num;
console.timeEnd("start");
}
const wrapFun = throttle(callback, 1000);
container.addEventListener("mousemove", function (e) {
console.time("start");
wrapFun();
});
附加
lodash 的防抖與節(jié)流實(shí)現(xiàn)
// 防抖
function debounce(func, wait, options) {
var lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
lastInvokeTime = 0,
leading = false,
maxing = false,
trailing = true;
if (typeof func != "function") {
throw new TypeError(FUNC_ERROR_TEXT);
}
wait = toNumber(wait) || 0;
if (isObject(options)) {
leading = !!options.leading;
maxing = "maxWait" in options;
maxWait = maxing
? nativeMax(toNumber(options.maxWait) || 0, wait)
: maxWait;
trailing = "trailing" in options ? !!options.trailing : trailing;
}
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
function leadingEdge(time) {
lastInvokeTime = time;
timerId = setTimeout(timerExpired, wait);
return leading ? invokeFunc(time) : result;
}
function remainingWait(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime,
timeWaiting = wait - timeSinceLastCall;
return maxing
? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;
return (
lastCallTime === undefined ||
timeSinceLastCall >= wait ||
timeSinceLastCall < 0 ||
(maxing && timeSinceLastInvoke >= maxWait)
);
}
function timerExpired() {
var time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
timerId = setTimeout(timerExpired, remainingWait(time));
}
function trailingEdge(time) {
timerId = undefined;
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}
function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
function flush() {
return timerId === undefined ? result : trailingEdge(now());
}
function debounced() {
var time = now(),
isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
clearTimeout(timerId);
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
// 節(jié)流
function throttle(func, wait, options) {
var leading = true,
trailing = true;
if (typeof func != "function") {
throw new TypeError(FUNC_ERROR_TEXT);
}
if (isObject(options)) {
leading = "leading" in options ? !!options.leading : leading;
trailing = "trailing" in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
leading: leading,
maxWait: wait,
trailing: trailing,
});
}