节流/防抖
什么时候需要防抖/节流
在正常开发过程中,特别是在处理用户输入或窗口调整大小等频繁触发的事件时,如果事件的回调函数过于复杂或者是 ajax 请求,在高频调用下难免会出现卡顿。为了限制函数执行频率,一般有两种解决方案:
- debounce 防抖
- throttle 节流
防抖的原理:在一定时间内函数只调用一次,当防抖函数被触发时,它不会立即执行目标函数,而是设置一个定时器。若在定时器时间到期前再次触发,则取消之前的定时器并重新设置定时器。只有当一段时间内没有新的触发,目标函数才会执行。
从简单的例子入手:
<!DOCTYPE html>
<html>
<head>
<title>Debounce Example</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
<input id="search" type="text" name="search" placeholder="Search">
<script src="debounce.js"></script>
</body>
</html>
debounce.js
var inputElement = document.getElementById('search');
function searchValue(ev) {
// ... search code
console.log(ev.target.value)
};
inputElement.oninput = searchValue;
在用户停止输入后再进行搜索处理
Version 1
通过防抖的定义简单实现防抖函数:
...
function debounce(func, wait) {
let timeout;
return function () {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
};
}
inputElement.oninput = debounce(searchValue, 1000);
Version 2
虽然通过防抖处理使输入停止 1s 后才执行事件,但是执行代码时会发现, event
对象变成 undefined 了。所以使用 apply 方法显式指定 this 的值和参数 debounce v2:
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(function () {
func.apply(this, args)
}, wait);
};
}
Version 3
拓展:在之前的版本中都是 在延迟之后执行函数,如果我们希望立即执行函数,并且直到停止触发后,才可以重新执行函数。也就是在延迟之前执行函数。
debounce v3:
function debounce(func, wait, leading) {
let timeout;
let result;
return function (...args) {
if (timeout) clearTimeout(timeout);
if (leading) {
var invoked = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (invoked) result = func.apply(this, args)
} else {
timeout = setTimeout(function () {
func.apply(this, args)
}, wait);
}
return result;
};
}
Version 4
拓展:在执行防抖的过程中,有时候我们又会希望在一些特殊情况下可以取消防抖。所以给 debounce 补充 cancel 函数。
debounce v4:
function debounce(func, wait, leading) {
let timeout;
let result;
return function (...args) {
if (timeout) clearTimeout(timeout);
if (leading) {
var invoked = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (invoked) result = func.apply(this, args)
} else {
timeout = setTimeout(function () {
func.apply(this, args)
}, wait);
}
return result;
};
debounce.cancel = function () {
clearTimeout(timeout);
timeout = null;
};
return debounce;
}
...
const debounceAction = debounce(() => {}, 10000, true);
// 取消防抖
debounceAction.cancel();
Version 5
拓展:在执行防抖的过程中,需要取消防抖并且立即执行。
debounce v5:
function debounce(func, wait, leading) {
let timeout;
let result;
return function (...args) {
if (timeout) clearTimeout(timeout);
if (leading) {
var invoked = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (invoked) result = func.apply(this, args)
} else {
timeout = setTimeout(function () {
func.apply(this, args)
}, wait);
}
return result;
};
debounce.cancel = function () {
clearTimeout(timeout);
timeout = null;
};
debounced.flush = function() {
if (timeout) {
clearTimeout(timeout);
func.apply(this, arguments);
timeout = null;
}
};
return debounce;
}
...
const debounceAction = debounce(() => {}, 10000, true);
debounceAction.flush();
节流
与防抖(debounce)不同,防抖是在最后一次调用结束后的延迟时间后才执行,而节流是确保函数在规定的时间间隔内至多执行一次。
节流的实现通常有两种方式:使用时间戳和使用定时器。
使用时间戳实现节流
使用时间戳的节流函数会在每次事件触发时,检查当前时间与上次执行时间的间隔,如果超过了设定的时间间隔,就执行函数。
function throttle(func, wait) {
let previous = 0;
const context = this;
const args = arguments;
return function (...args) {
const now = Date.now();
if (now - previous > wait) {
previous = now;
func.apply(context, args);
}
};
}
使用定时器实现节流
在第一次事件触发时执行函数,并设置一个定时器,在设定的时间间隔之后允许下一次执行。
function throttle(func, wait) {
let timeout = null;
return function(...args) {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(this, args);
}, wait);
}
};
}
version 2
对于两种方法实现节流都有一些缺点,使用时间戳在停止触发之后没办法再次执行函数,而使用定时器没办法再一开始就执行函数。综合两种方法的特点,可以写出 throttle v2:
function throttle(func, wait) {
let timeout,
previous = 0;
const throttled = function (...args) {
const now = Date.now();
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(this, args);
} else if (!timeout) {
timeout = setTimeout(() => {
previous = Date.now();
timeout = null;
func.apply(this, args);
}, remaining);
}
};
// 添加 `cancel` 方法,用于取消节流操作
throttled.cancel = function () {
clearTimeout(timeout);
timeout = null;
previous = 0;
};
return throttled;
}
version 3
拓展:添加 Boolean 控制是否需要首次或者是否需要停止触发后再次执行函数。
throttle v3:
function throttle(func, wait, options = {}) {
let timeout, previous = 0;
const { leading = true, trailing = true } = options;
const throttled = function(...args) {
const now = Date.now();
if (!previous && !leading) previous = now;
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(this, args);
} else if (!timeout && trailing) {
timeout = setTimeout(() => {
previous = leading ? Date.now() : 0;
timeout = null;
if (trailing) func.apply(this, args);
}, remaining);
}
};
throttled.cancel = function() {
clearTimeout(timeout);
timeout = null;
previous = 0;
};
return throttled;
}
如果传入 {leading: false, trailing: false} 节流函数不会执行任何操作。因为不会再第一次事件触发时执行 且 不会再最后一次事件触发后等待间隔再执行,这意味着没有条件可以触发他。