函数防抖--Debounce
事件被触发后
n
秒再执行回调函数,如果在n秒内再次被触发,则重新计时
先来感受一下防抖的作用
没有防抖的代码示例:
function search (content) {
console.log('查询' + content);
}
let input = document.querySelector('#input')
input.addEventListener('keyup', function (e) {
search(this.value)
})
运行结果:
可以发现每次我们键盘输入的后一瞬间,就会发送查询请求。这不仅非常浪费资源而且在实际应用中,我们查询时是需要全部输入结束后才想要请求返回结果。
优化一下:
// 防抖函数
function debounce(fn, delay) {
let timer = null;
return function(...args) { // 获取事件函数的所有参数
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
}
let input1 = document.querySelector('#input1')
input1.addEventListener('keyup', debounce(function() {
search(this.value)
}, 500))
运行结果:
加了防抖后可以看到在频繁触发事件时不会发送请求查询。
在指定间隔时间内没有触发事件,才会执行函数,在间隔期间再次触发事件则会重新计时。
防抖的原理
当事件首次触发时,会设置一个定时器,如果在设定的时间内事件被再次触发,则会清除之前的计时器并重新设置新的计时器。只有时间在设定的延迟时间内没有被再次触发时,函数才会执行
接下来我们来模拟一下这个过程:
let sig = function () {
console.log('间隔时间内未重复触发事件', new Date().toLocaleString())
}
let mul = function () {
console.log('间隔时间内重复触发事件', new Date().toLocaleString())
}
setInterval(debounce(sig, 500), 1000) // 规定防抖间隔 0.5秒,1秒触发一次事件
setInterval(debounce(mul, 2000), 1000) // 规定防抖间隔2秒,1秒触发一次事件
运行结果:
可以发现,只有sig
函数被正常执行。其实是因为在规定的防抖间隔时间内sig
函数没有被反复触发,正常执行。而mul
函数在间隔时间内被反复触发重置计时所以没法执行输出。
-
时间线分析:
0ms
: 启动setInterval
,安排1000ms
后调用闭包函数1000ms
: 调用闭包函数,设置500ms
的定时器1500ms
: 定时器触发,执行sig
函数,输出 "间隔时间内未重复触发事件"2000ms
: 再次调用闭包函数,设置新的500ms
定时器2500ms
: 定时器触发,再次输出 "间隔时间内未重复触发事件"- ... 以此类推
-
mul
函数为什么不执行
对于setInterval(debounce(mul, 2000), 1000)
,因为2000ms > 1000ms
,所以每次定时器尚未触发就被下一次调用清除了,导致mul
函数不会执行。
可以把函数的防抖理解为
moba
游戏的回城操作,如果你在回城期间(防抖间隔)反复点击按钮,则进度条会反复重置,直到最后一次点击
函数节流--Throttle
节流的原理
规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
来个代码感受一下:
// 节流
function throttle (fn, wait) {
let preTime = 0
return function (...arg) {
let nowTime = Date.now() // 第一次点击的时间戳
if (nowTime - preTime >= wait) {
// fn.apply(this, args); // 不需要数组解构,args数组,盛放参数列表 apply方法显式绑定this
fn.call(this, ...arg) // call 数组解构,直接传入参数列表 显式绑定this指向
preTime = nowTime
}
}
}
运行效果:
可以看到,持续输出的情况下,节流会在一定时间间隔后执行一次目标函数
可以把节流理解为攻速阈值,在规定的数值内,不论你敲键盘按平A有多频繁,都只会规定时间后发出普攻,这就是节流。
防抖、节流的实现方法
一、lodash实现方法
1. 防抖:_.debounce()
<body> ...... </body>
<script src="./lodash.min.js"></script> <!-- 引入lodash库-->
<script>
// DOM操作内容
box.addEventListener('mousemove', _.debounce(mouseMove, 500))
</script>
2. 节流:_.throttle()
<body> ...... </body>
<script src="./lodash.min.js"></script> <!-- 引入lodash库-->
<script>
// DOM操作内容
box.addEventListener('mousemove', _.throttle(mouseMove, 1000))
</script>
二、万能的手搓
1. 防抖
- 触发事件时,清除之前设置的定时器(如果有)
- 启动新的定时器,在指定的时间间隔内等待
- 在等待期间再次触发事件,重复步骤1、2
- 如果等待期间没有触发事件,执行函数
防抖代码思路
- 定义防抖函数,传入参数为需要执行的函数引用和延迟时间
- 由于可能需要定位且清除定时器,考虑到函数执行后被销毁,使用闭包实现
- 防抖函数返回的需要是函数体供事件触发调用
// 防抖函数
function debounce (fn, t) {
let timeId
// 返回一个匿名函数
return function () {
// 如果有定时器就清除
if (timeId) clearTimeout(timeId) // 使用闭包,定时器就不会被销毁
// 开启定时器 500
timeId = setTimeout(function () {
fn()
}, t)
}
}
发现问题
- 大部分情况,函数传递不止参数且有一个事件参数
e
中存放着可能需要的方法 this
指向被覆盖丢失,会导致后续对DOM
元素使用this
操作时出错
解决办法
- 使用
ES6
函数拓展方法和数组解构...arg
将参数全部传递给arg
数组 - 使用
call
、apply
方法显式绑定this
bind
返回值为一个函数,在此处不适用
防抖最终代码
function debounce (fn, wait) {
var timer = null
return function (...arg) { // 函数this指向btn,扩展运算符,将参数转为数组,用于传递参数
let that = this // 在定时器为非箭头函数时需要
clearTimeout(timer)
timer = setTimeout(function () {
fn.call(that, ...arg) // 数组解构,args盛放参数列表 call方法显式绑定this
}, wait)
}
}
function debounce (fn, delay) {
let timer = null;
return function (...args) { // 扩展运算符,将参数转为数组,用于传递参数
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args); // 不需要数组解构,args数组,盛放参数列表 apply方法显式绑定this
}, delay);
}
}
2. 节流
- 初始化检查当前有没有正在等待的定时器
- 首次触发时,创建定时器,设置间隔时间后执行函数
- 等待期间再次触发,检查定时器是否还在运行
- 定时器到期执行目标函数,将 定时器重置,准备接受下一次触发
- 回到步骤1,准备处理下一个时间窗口的触发
节流遇到的问题与防抖同理,这里不过多赘述
节流最终代码
function throttle (fn, wait) {
let preTime = 0
return function (...arg) {
let nowTime = Date.now() // 第一次点击的时间戳
if (nowTime - preTime >= wait) {
// fn.apply(this, args); // 不需要数组解构,args数组,盛放参数列表 apply方法显式绑定this
fn.call(this, ...arg) // call 数组解构,直接传入参数列表 显式绑定this指向
preTime = nowTime
}
}
}
防抖、节流应用场景
防抖应用场景
search
搜索联想,用户在不断输入值时,用防抖来节约请求资源。window
触发resize
的时候,不断的调整浏览器窗口会不断的触发这个事件,用防抖来让其只触发一次- 网络状态不佳时,用户多次快速点击按钮,用防抖只触发一次,降低服务器端的并发压力
节流应用场景
- 鼠标不断点击触发,
mousedown
(单位时间内只触发一次) - 监听滚动事件,比如是否滑到底部自动加载更多,用
throttle
来判断