❝防抖 ( debounce ) 节流 ( throttle ) 优化高频 JavaScript 代码的一种手段 ; 比如 鼠标的 mouseover mousemove 事件 , 浏览器的 scroll resize 事件 , 还有 输入框的 keypress keyup keydown 等事件在触发时 , 会不断的执行事件函数 , 浪费性能资源 ; 防抖 节流 就是对这类事件进行调用次数的限制 , 对性能做出一定的优化 ;
❞
防抖 debounce
❝「在事件被触发 n 秒后 , 再次执行回调 ; 如果 n 秒内触发 , 重新计时 ;」
想象一下 , 一个搜索联想功能 ; 你使用了 keyup 事件去监听用户弹起键盘后, 你请求接口获取联想数据 ; 然后渲染视图 ; 此时就会出现一个问题 , 只要你输入了内容就会立即调用接口渲染视图 ; 如果用户连续输入多个关键词 , 而你频繁的调用多次会出现什么问题呢 ?
❞
搜索联想这样搞肯定是不合适的 ;
问题分析 :
- 事件回调执行太快了, 键盘弹起立马就触发了 ; 我们需要 n 秒后再去执行回调 ;
- 如果我们输入正在连续内容的 n 秒内, 最好不要去执行回调 ;
思路分析 :
- 可以键盘弹起一定时间后再去执行回调 ;
- 给用户一个 300ms 的延时 , 如果 300ms 不输入内容, 就去调接口渲染视图 ;
- 如果第一次输入内容后, 时间快要到达 300ms 用户再次输入 , 我们重新计算时间 ;
将目标方法 ( 动作 ) 包装再 setTimeout 中 , 然后这个方法是一个事件的回调函数 , 如果这个回调一直执行 , 那么这些动作就一直不执行 ; 利用 clearTimeout 将事件内的连续动作删掉 , 用户不在触发这个事件的时候 , 再将 setTimeout 执行 ;
总结 「在事件被触发 n 秒后 , 再次执行回调 ; 如果 n 秒内触发 , 重新计时 ;」
// func 要执行的防抖函数
function debounce(func, delay) {
delay = delay || 300;
var timer = null;
// return 一个 function 利用闭包维护 timer 和 delay;
// 另外这个也是一个处理过防抖的函数;
return function() {
var _this = this;
var arg = arguments;
clearTimeout(timer); // 每次用户输入清空上一个 timer 这样能保证下面的定时器每次都重新开始
timer = setTimeout(function() {
// 解决事件对象找不到和this指向错误的问题
func.apply(_this, arg);
}, delay);
}
}
// 不要在意风格不统一, 演示而已 😂
let input = document.querySelector('input')
input.onkeyup = debounce(async function () {
let { data } = await axios.post('/v1/api/index/hits', {
words: input.value
})
render(data.data)
})
为什么要在外部维护一个 this 和 arguments , 而不是在里面直接用
可以看出 setTimeout 里面是一个匿名函数 , 通过 function 关键字声明的函数 , this 会指向 window , 这显然是不可以的 ; debounce 实际执行的是 return function , 所以外部维护这个 this 应该是没有毛病的 ;
arguments 是用来接收不固定参的 , 如果我们直接接收 arguments 其实接收的是 setTimeout 中的不固定参 , 这显然也是不合理的 ;
// 如果不想在外部维护 this 和 arguments 也很简单
function debounce(func, delay=300) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func.call(this, ...args);
}, delay);
}
}
为什么使用 call 方法 ?
- 允许为不同的对象分配和调用属于一个对象的函数/方法 . 提供新的 「this」 值给当前调用的函数/方法。 详细看 MDN
节流 throttle
❝「高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率」
嗯 , 这个相对还是比较好理解的 ; 就是一直执行的事件或者函数咱们给他限流一下 ; 像不像早高峰限流的地铁 ? 你就是限流的事件或者方法 🤣
假设, 我们现在需要做个 检测浏览器滚动条滚动的功能 ; 这个太简单, easy
❞
window.addEventListener('scroll', function () { console.log('scroll...') });
这触发频率看着有些太快了; 大部分场景下我们完全没有必要监控频率这么高 , 既然谈节流, 那肯定是不想这样快的触发;下面就看实现
function throttle(func, delay=1000) {
let timer = null;
return function (...args) {
if (timer) return; // 只要定时器执行就 return
timer = setTimeout(() => {
func.call(this, ...args);
timer = null; // 将 timer 重置为 false
}, delay);
}
}
window.addEventListener('scroll', throttle(function(e) { console.log('scroll', this, e) }))
这里可以看出, 执行频率已经降下来了, 但是下面再看下, 有没有什么其他的问题呢 ?
可以看出先先滚动 , 再执行 ; 这样有问题吗 ? 其实也没啥问题 咱们再回顾一下概念 「高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率」 n 秒内执行一次 , 那是 n 秒的开始执行 , 还是 n 秒的结束执行呢 ? 当然我们希望这个是可控的 ;
function throttle(func, delay=1000, start=true) {
let timer = null;
return function(...args) {
if (timer) return;
start && func.call(this, ...args); // 短路运算, 是否立即执行
timer = setTimeout(() => {
!start && func.call(this, ...args); // 短路运算, 是否定时器结束后执行
timer = null;
}, delay);
}
}
- start 为 true
- start 为 false
为什么立即执行, 放在这个位置 ;
- 第一次执行时 , timer 条件不成立 , start 为 true 直接执行后面的函数 ;
- 第二次执行时 ( 发生了滚动 ) , 执行第一个短路运算 , 进入定时器 , 由于定时器为异步 所以此时 timer 为 true , 就一直 return 异步销毁 timer 执行完毕 ;
既然要销毁 timer 为什么不用 clearTimeout
- 这里 timer 实际上就是一个标识而已 ; 标记 timer 是否为 true 是否可以执行 定时器
function throttle(func, delay=1000) {
let timer = 0;
return function() {
if (timer) return; // 不满足条件, timer + 1后满足, 就一直 return
timer++;
setTimeout(() => {
func();
timer = 0; // 异步执行完毕后, 重复上面的操作
});
}
}
总结
❝防抖 ( debounce ) 节流 ( throttle ) 目的都是降低 回调的执行频率 , 达到节省资源的目的 ;
❞
应用场景
防抖 ( debounce ) : 搜索联想 , 手机号检测等 ;
节流 ( throttle ) : 滚动加载 监听 , 窗口大小监听等 ;
参考文章
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/5
https://segmentfault.com/a/1190000018445196
https://zhuanlan.zhihu.com/p/38313717
https://www.jianshu.com/p/4e840b7ed35b
https://www.jianshu.com/p/53feee9f4fad
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call
本文使用 mdnice 排版