技术背景:
相信以下场景你都不陌生
- 多次点击按钮导致页面失去响应或者出现意外情况。
- 如何实现搜索联想功能以及各企业邮箱提示功能。
- 页面滚动、输入框输入以及窗口尺寸变化频繁触发事件。
- 手机号、邮箱格式的实时校验
- …
为了解决或者实现这类场景,优化性能和改善用户体验。衍生出了一种技术,防抖(debouce)和节流(throttle)
一、概念
- 防抖(debounce)
延迟执行函数,直到一段连续的时间内没有新的事件被触发。
解释:
- 非立即执行,即当一个事件在一段时间内连续触发时,只有最后一次触发且超过延迟时间后才会执行相应的处理函数。
- 立即执行,当一个事件在一段时间内连续触发时,第一次触发时会立即执行,但对后续不超过预设延迟时间的触发不做处理。
类比:王者荣耀的回城效果,多次点击回城会一直重置回城时间,只有停止点击回城并等待一段时间后才会实现回城功能。
具体来说,当触发一个事件时,防抖函数会启动一个定时器,延迟一段时间执行事件处理函数。如果在这段时间内又触发了同样的事件,就会清除之前的定时器,并重新设置一个新的定时器。只有在最后一个事件触发后,经过一段时间内没有新的事件再次触发时,事件处理函数才会被执行。
- 节流(throttle)
确保函数在一定时间间隔内最多执行一次,无论事件触发频率如何。
解释:在一段时间内,无论事件触发频率如何,处理函数能且仅能触发一次。
类比:王者荣耀中,在技能的CD周期内,技能仅能触发一次,而且是从第一次触发时开始执行。
具体来说,当触发一个事件时,节流函数会立即执行该事件处理函数,然后在设定的时间间隔内,如果再次触发相同的事件,节流函数会忽略该事件,直到这段时间间隔过去,才会重新触发执行,与立即执行的防抖相比,节流是控制处理函数的触发频率,只要一直在触发,处理函数会按照一定的时间间隔执行,而防抖函数如果一直触发,只有第一次生效。
二、手写实现
- 防抖(debounce)
<html>
<button>打印</button>
<script>
/*
******封装防抖工具函数 myDebouce *******
*/
// fn 需要添加防抖效果的函数(回调函数),delay 延迟时间
const myDebouce = (func, delay, immediate = true) => {
// 定时器变量,利用闭包的性质,用来存储当前正在执行的定时器
let timer = null;
return function (...args) {
const context = this; // 保留当前上下文
timer && clearTimeout(timer); // 清除之前的定时器
if (immediate) {
const callNow = !timer; // 判断是否应该立即执行
callNow && func.apply(context, args); // 立即执行函数
timer = setTimeout(() => {
timer = null; // 定时器结束后,重置 timer,避免超过延迟时间后无法响应处理函数
}, delay);
} else {
timer = setTimeout(() => {
func.apply(context, args); // 延迟执行
timer = null; // 执行后重置 timer
}, delay);
}
};
};
// 定义处理函数,并添加防抖效果
const click = myDebouce(function(message){
console.log(message);
},1000)
// 绑定点击事件
document.querySelector('button').addEventListener('click',function () {
click('防抖成功')
})
</script>
</html>
设置了立即触发的属性后,如果一直点击按钮,事件只会触发一次;如果关闭了立即触发,会在连续点击按钮停止后delay毫秒后执行
- 节流(throttle)
方法一: 使用时间戳的写法
<html>
<button>打印</button>
<script>
/*
******封装节流工具函数 throttle*******
*/
// fn 需要添加节流效果的函数(回调函数), T 时间周期,T的默认值是500ms
const myThrottle = (fn,T = 500) => {
// 储存上一次函数执行的时间戳
let old = 0
return function (...args){
// 获取当前时间戳
const now = new Date().getTime()
// 判断当前时间戳与上一次触发的时间戳差值是否大于时间周期
if( now - old >= T){
fn(...args);
// 更新上一次执行的时间戳
old = now;
}
}
}
// 定义处理函数,并添加节流效果
const click = myDebouce(function(message){
console.log(message);
},1000)
// 绑定点击事件
document.querySelector('button').addEventListener('click',function () {
click('节流成功!按周期,每秒打印一次,无论频率多高')
})
</script>
</html>
方法二: 使用定时器 setTimeout 的写法
// fn 需要添加节流效果的函数(回调函数), T 时间周期
const myThrottle = (fn,T)=>{
// 通过闭包保存一个 “节流阀”,默认为false
let flag = false
return function (...args){
// 节流阀存在,表示已经执行
if(flag){
return
}
else {
flag = true; //立刻将节流阀设置为true,表示处理函数正在执行
setTimeout(() => {
fn(...args)
//关键!!!! 执行完毕,重置节流阀为false
flag = false
}, T);
}
}
}
// 绑定事件
const click = myThrottle(function (message) {
console.log(message)
},1000)
// 绑定点击事件
document.querySelector('button').addEventListener('click',()=>{
click('节流成功')
})
如果一直点击按钮,控制台会按T的时间周期间断执行处理函数,在时间周期T内,函数仅能执行一次。
三、使用场景
- 防抖
- 避免用户重复点击按钮等需要网络请求的触发事件。
- 搜索框搜索输入,只需用户最后一次输入完,再发送请求。
- 调整浏览器窗口resize时,避免重复执行绑定的回调函数,浪费大量资源,降低性能。
- 手机号、邮箱号的格式验证。
- 节流
- 滚动加载,加载更多或滚到底部监听。
- 搜索框,搜索联想功能
- 浏览器播放事件,每隔一秒计算一次进度信息等。
- 鼠标移动或者滚轮滑动等频繁触发的事件。
四、区别与联系
联系
- 都可以通过定时器setTimeout实现
- 都是用来限制函数执行频率的技术
区别
- 实现方式不同。
函数防抖是在一定时间内,如果事件重复触发,则只执行最后一次操作。函数节流则是在一定时间内,无论事件触发多少次,只会执行一次操作。
- 执行时机不同。
函数防抖执行时机在事件停止触发后一段时间后,而函数节流则是在指定时间周期内只会执行一次。
- 适用场景不同。
函数防抖适用于一些需要等待用户停止操作之后再执行的场景,如搜索输入框、窗口调整。函数节流适用于一些高频率触发的事件,如鼠标移动事件,滚动事件等。
总而言之,防抖和节流都是用来限制函数执行频率的技术