节流 debounce 和 防抖 throttle 的简单实现
2011 年,Twitter 曝出一个 bug:当用户在滚动页面时,网站会变慢甚至无响应。John Resig 发表了一篇关于该问题的博客,并指出把高消耗的函数执行绑定在 onscroll
事件上是多么得不靠谱。下面以 lodash
中的 debounce
和 throttle
为例,来讲解 函数节流
在解决类似问题中的作用。
debounce
搜索引擎的 自动补全
功能已司空见惯,每当用户输入一个字符就去发一次请求,显然有点浪费。我们可以考虑在用户停止输入 500 ms
后再去请求,这样用户体验基本不会受到影响,也减少了不必要的请求,减轻了服务器的压力。
简单实现
function debounce(func, wait) {
let timer;
return function(...args) {
const context = this;
clearTimeout(timer);
timer = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}
调用示例
/** 原来的做法 */
input.onkeypress = doSomeThing;
/** 使用函数节流 */
input.onkeypress = debounce(doSomeThing, 500);
/** 错误示例 */
input.onkeypress = function() {
// 原因:返回一个函数,但没有执行
// debounce(doSomeThing, 500);
// 原因:每次事件触发单独创建一个闭包,会产生多个定时器
// debounce(doSomeThing, 500)();
}
leading edge
如果用户打字速度很快,我们希望能在他输入第一个字符的时候就给出相关提示,可以使用 leading
参数来控制。
扩展 leading
参数
function debounce(func, wait, { leading = false } = {} ) {
let context, xargs, timer;
let firstInvoke = true;
function invokeFunc() {
func.apply(context, xargs);
}
function debounced(...args) {
context = this;
xargs = args;
clearTimeout(timer);
if (leading && firstInvoke) {
invokeFunc();
firstInvoke = false;
}
timer = setTimeout(function() {
invokeFunc();
}, wait);
};
return debounced;
}
maxWait
无限滚动
在移动端场景中必不可少,我们希望能在页面滚动即将到达底部时去请求更多的数据。通过上面的实现,我们只有等用户停止滚动 wait ms
后才能开始检测 到页面底部距离
,未免有些慢了。不过我们通过 maxWait
参数,可以每隔 maxWait ms
就去执行检测代码来解决类似问题。
扩展 maxWait
参数
function debounce(func, wait, { leading = false, maxWait = 0 } = {}) {
let context, xargs, timer, timeLast;
let firstInvoke = true;
function invokeFunc() {
func.apply(context, xargs);
}
function debounced(...args) {
context = this;
xargs = args;
const timeNow = +new Date();
clearTimeout(timer);
if (leading && firstInvoke) {
invokeFunc();
firstInvoke = false;
}
if (!timeLast) {
timeLast = timeNow;
}
if (!!maxWait && timeNow - timeLast >= maxWait) {
invokeFunc();
timeLast = timeNow;
} else {
timer = setTimeout(function() {
invokeFunc();
}, wait);
}
};
return debounced;
}
trailing edge
除了以上参数, debounce
还提供了 trailing
参数。在调整浏览器窗口大小时会触发多次 onresize
事件,如果我们只对操作停止时的窗口尺寸感兴趣,那么就使用 trailing = true
来保证这一点( debounce
中 trailing
默认为 true
)。
扩展 trailing
参数
function debounce(func, wait, { leading = false, maxWait = 0, trailing = true } = {}) {
let context, xargs, timer, timeLast;
let firstInvoke = true;
function invokeFunc() {
func.apply(context, xargs);
}
function debounced(...args) {
context = this;
xargs = args;
const timeNow = +new Date();
clearTimeout(timer);
if (leading && firstInvoke) {
firstInvoke = false;
invokeFunc();
}
if (!timeLast) {
timeLast = timeNow;
}
if (!!maxWait && timeNow - timeLast >= maxWait) {
invokeFunc();
timeLast = timeNow;
} else if (trailing) {
timer = setTimeout(function() {
invokeFunc();
}, wait);
}
};
return debounced;
}
throttle
通过以上示例代码不难看出,使用 debounce
就可以实现 throttle
的功能,或者说 throttle
就是封装后的 debounce
。其实 lodash
的源码也是这么做得, underscore
则将两个函数的实现分开了,有兴趣可以 看一下 。
实现 throttle
function throttle(func, wait, { leading = true, trailing = true } = {}) {
return debounce(func, wait, { leading, maxWait: wait, trailing });
}
私有函数
除了以上参数, lodash
中的 debounce
和 throttle
还包含以下两个私有函数可供调用,
cancel
:取消延时函数(定时器)的执行flush
:立即执行用户回调
调用示例
const debounceFunc = _.debounce(doSomething, 500);
debounceFunc.cancel();
debounceFunc.flush();
应用场景
- debounce
// 避免过分频繁得计算布局
window.onresize = debounce(calculateLayout, 150);
// 防止用户连续点击,发送重复请求
button.onclick = debounce(sendMail, 300, { leading: true, trailing: false });
// 恰当地处理批量登录
const debounceFunc = debounce(batchLog, 250, { maxWait: 1000 });
const source = new EventSource('/stream');
source.onmessage = debounceFunc;
// 取消节流调用
window.onpopstate = debounceFunc.cancel;
- throttle
// 避免过分频繁得更新定位
window.onscroll = throttle(updatePosition, 100);
// 恰当地处理身份更新
const throttleFunc = throttle(renewToken, 300000, { 'trailing': false });
button.onclick = throttleFunc;
// 取消防抖调用
window.onpopstate = throttled.cancel;
参考
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 去除移动端 300ms 点击延迟
下一篇: 谈谈自己对于 AOP 的了解
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论