概念
本质上是优化高频率执行代码的一种手段
如:浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能
为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率。
防抖函数(debounce)
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
看一个🌰(栗子):
//模拟一段ajax请求
function ajax(content) {
console.log('ajax request ' + content + 'time:' + new Date().Format('HH:mm:ss'))
}
let inputa = document.getElementById('unDebounce')
inputa.addEventListener('keyup', function (e) {
ajax(e.target.value)
})
看一下运行结果:
可以看到,我们只要按下键盘,就会触发这次ajax请求。不仅从资源上来说是很浪费的行为,而且实际应用中,用户也是输出完整的字符后,才会请求。所以我们想要的是用户全部输出完后,我们才会发起请求,而不是在输入过程中发起请求。这里就需要用到防抖函数
简单版实现方式
function debounce(func, wait) {
let timeout;
return function () {
let context = this; // 保存this指向
let args = arguments; // 拿到event对象
if(timeout) clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
立即执行实现方式
防抖如果需要立即执行,可加入第三个参数用于判断,实现如下:
function debounce(func, wait, immediate) {
let timeout
return function () {
let context = this
let args = arguments
if (timeout) clearTimeout(timeout) // timeout 不为null
if (immediate && !timeout) {
timeout = setTimeout(function () {
timeout = null
}, wait)
func.apply(context, args)
} else {
timeout = setTimeout(function () {
timeout = null
func.apply(context, args)
}, wait)
}
}
}
回顾我们刚刚的🌰,看下加入防抖函数后的效果:
//模拟一段ajax请求
function ajax(content) {
console.log('ajax request ' + content + 'time:' + new Date().Format('HH:mm:ss'))
}
let inputb = document.getElementById('debounce')
let debounceAjax = debounce(ajax, 500)
inputb.addEventListener('keyup', function (e) {
debounceAjax(e.target.value)
})
看一下运行结果:
可以看到,我们加入了防抖以后,当你在频繁的输入时,并不会发送请求,只有当你在指定间隔内没有输入时,才会执行函数。如果停止输入但是在指定间隔内又输入,会重新触发计时。
个人理解 函数防抖就是法师发技能的时候要读条,技能读条没完再按技能就会重新读条。
节流函数(throttle)
规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
节流函数实现的方式有时间戳和定时器两种方式
时间戳实现方式
function throttle(func, delay) {
var lastTime = 0;
return function() {
var context = this;
var args = arguments;
var nowTime = new Date().getTime();
if (nowTime > lastTime + delay) {
func.apply(context, args)
lastTime = nowTime;
}
}
}
时间戳的方式,函数在时间段开始时立即执行(lastTime设置为0),如果lastTime设置为nowTime,则第一次执行在delay秒后。
缺点:假定函数间隔1s执行,如果最后一次停止触发,卡在4.2s,则不会再执行。
定时器实现方式
function throttle(func, delay) {
var timeout;
return function() {
var context = this;
var args = arguments;
if (!timeout) {
setTimeout(function(){
func.apply(context, args);
timeout = null;
}, delay)
}
}
}
定时器的方式,函数在时间段结束时执行。可理解为函数并不会立即执行,而是等待延迟计时完成才执行。 (由于定时器延时,最后一次触发后,会再执行一次回调函数)
时间戳 + 定时器(互补优化)
function throttle (func, delay) {
let timeout
let previous = 0
return function () {
let context = this
let args = arguments
let now = new Date().getTime()
// 距离下次函数执行的剩余时间
let remaining = delay - (now - previous)
// 如果无剩余时间或系统时间被修改
if (remaining <= 0 || remaining > delay) {
// 如果定时器还存在则清除并置为null
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 更新对比时间戳并执行函数
previous = now
func.apply(context, args)
} else if (!timeout) {
// 如果有剩余时间但定时器不存在,则设置定时器
// remaining毫秒后执行函数、更新对比时间戳
// 并将定时器置为null
timeout = setTimeout(() => {
previous = new Date().getTime()
timeout = null
func.apply(context, args)
}, remaining)
}
}
}
看一个🌰:
//模拟一段ajax请求
function ajax(content) {
console.log('ajax request ' + content + 'time:' + new Date().Format('HH:mm:ss'))
}
let throttleAjax = throttle(ajax, 1000)
let inputc = document.getElementById('throttle')
inputc.addEventListener('keyup', function(e) {
throttleAjax(e.target.value)
})
看一下运行结果:
可以看到,我们在不断输入时,ajax会按照我们设定的时间,每1s执行一次。
个人理解 函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。
总结
相同点
- 都可以通过使用
setTimeout实现 - 目的都是,降低回调执行频率。节省计算资源
不同点
- 函数防抖,在一段连续操作结束后,处理回调,利用
clearTimeout和setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能 - 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次
例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数:
节流:每隔 500ms 就执行一次。
防抖:则不管调动多少次方法,在2s后,只会执行一次
应用场景
-
debounce防抖在连续的事件,只需触发一次回调的场景有
- search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
-
throttle节流在间隔一段时间执行一次回调的场景有:
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
扩展
防抖节流函数在TypeScript下怎么写
标准的防抖函数代码
function debounce(func, wait) {
let timeout;
return function () {
let context = this; // 保存this指向
let args = arguments; // 拿到event对象
if(timeout) clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
改造成ts代码
加上对应的类型描述后,发现编译不通过:
参考:www.typescriptlang.org/docs/handbo…
TypeScript 提供了一种机制,可以在函数入参列表中第一个位置处,手动写入 this 标识其类型。但这个 this 入参只作为一个形式上的参数,供 TypeScript 做静态检查时使用,编译后是不会存在于真实代码中的。
修正ts下this参数的问题
问题解决!!
参考
7分钟理解JS的节流、防抖及使用场景
面试官:什么是防抖和节流?有什么区别?如何实现?
Debounce防抖函数在TypeScript下怎么写