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;