防抖 debounce
场景
- 搜索提示
- 如果默认情况下,输入一个字符就触发送一次请求,这样太浪费性能。
- 加入防抖后,指定一个最大触发时间,用户在这个时间内输入内容后,就会重新开始计时,只有超过该时间间隔才会发送请求
- 优势:避免频繁触发事件,导致的性能问题
- 其他场景: window 的 resize / scroll 事件等
生活中的例子:乘坐电梯,假设你正在电梯里面 电梯门开始关闭,突然,另一个人进入电梯。这个时候,电梯门打开了,它没有升降。 现在又有一个人要进入电梯,又执行了上述相同的功能。 电梯延迟了它的功能(升降楼层),但是,优化了电梯的利用率。王者荣耀的回城也是一样的,只有在最后一次点击回城时才开始回城。
实现过程1
- 基本实现:
const debounce = (func, wait) => {
let timerId
return () => {
clearTimeout(timerId)
timerId = setTimeout(() => {
func()
}, wait)
}
}
// 测试:
let count = 0
const getCity = () => console.log('鼠标移动了', ++count)
// 鼠标移动事件,在鼠标停止移动后,再经过 500ms 才会执行
window.onmousemove = debounce(getCity, 500)
实现过程2
- 处理 this 指向:
const debounce = (func, wait) => {
let timerId
// 注意:此处不要使用箭头函数,箭头函数 this 指向的问题!!!
return function() {
const context = this
clearTimeout(timerId)
timerId = setTimeout(() => {
func.apply(context)
}, wait)
}
}
// 测试
<div id="container" style="height: 100px; background-color: skyblue"></div>
// 注意:箭头函数中 this 指向问题
function getCity() {
console.log('鼠标移动了 this:', this)
}
const container = document.getElementById('container')
// 鼠标移动事件,在鼠标停止移动后,再经过 500ms 才会执行
container.addEventListener('mousemove', debounce(getCity, 500))
实现过程3
- 事件对象(处理参数):
const debounce = (func, wait) => {
let timerId
return function() {
const context = this
const args = arguments
clearTimeout(timerId)
timerId = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
// const fn = debounce((arg) => {}, 500)
// fn(123)
// 测试:
function getCity(e) {
console.log('鼠标移动了,事件对象:', e)
}
const container = document.getElementById('container')
container.addEventListener('mousemove', debounce(getCity, 500))
实现过程4
- 立即执行:
const debounce = (func, wait, leading) => {
let timerId
return function() {
const context = this
const args = arguments
if (timerId) clearTimeout(timerId)
if (leading === true) {
if (!timerId) func.apply(context, args)
timerId = setTimeout(() => {
// 重置 timerId 的值。
timerId = null
}, wait)
} else {
timerId = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
}
// 测试:
container.addEventListener('mousemove', debounce(getCity, 500, true))
- 分解步骤1:
// 1 添加立即执行判断
const debounce = (func, wait, leading) => {
let timerId
return function() {
const context = this
const args = arguments
if (leading === true) {
clearTimeout(timerId)
timerId = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
}
- 分解步骤2:
// 2 立即调用
const debounce = (func, wait, leading) => {
let timerId
return function() {
const context = this
const args = arguments
if (leading === true) {
// 问题:因为直接调用了 func,没有了 debounce 的特性
func.apply(context, args)
clearTimeout(timerId)
timerId = setTimeout(() => { }, wait)
}
}
}
- 分解步骤3:
// 3 调用一次
const debounce = (func, wait, leading) => {
let timerId
let callNow = true
return function() {
const context = this
const args = arguments
if (leading === true) {
// 问题:只会触发一次
if (callNow) func.apply(context, args)
callNow = false
clearTimeout(timerId)
timerId = setTimeout(() => { }, wait)
}
}
}
- 分解步骤4:
// 4 实现debounce效果
const debounce = (func, wait, leading) => {
let timerId
let flag = true
return function() {
const context = this
const args = arguments
if (leading === true) {
if (flag) func.apply(context, args)
flag = false
clearTimeout(timerId)
timerId = setTimeout(() => {
// 在间隔时间达到后,重置标志。这样,再触发该事件时,上面的 if(flag) 判断就成了
flag = true
}, wait)
}
}
}
- 分解步骤5:
// 5 使用 timerId 代替 flag
const debounce = (func, wait, leading) => {
let timerId
return function() {
const context = this
const args = arguments
if (leading === true) {
if (!timerId) func.apply(context, args)
clearTimeout(timerId)
timerId = setTimeout(() => {
// 重置 timerId 的值。
timerId = null
}, wait)
}
}
}
实现过程5
- 返回值:只能处理 leading=true 的情况
const debounce = (func, wait, leading) => {
let timerId, result
return function() {
const context = this
const args = arguments
if (timerId) clearTimeout(timerId)
if (leading === true) {
if (!timerId) result = func.apply(context, args)
timerId = setTimeout(() => {
// 重置 timerId 的值。
timerId = null
}, wait)
} else {
timerId = setTimeout(() => {
func.apply(context, args)
}, wait)
}
return result
}
}
// 测试
function getCity(e) {
console.log('鼠标移动了,事件对象:', e)
return 666
}
const container = document.getElementById('container')
const btnCancel = document.getElementById('container')
const handleMouseMove = debounce(getCity, 2000, true)
container.addEventListener('mousemove', handleMouseMove)
// 结果: 666
console.log('测试返回值:', handleMouseMove())
实现过程6
- 取消
const debounce = (func, wait, leading) => {
let timerId, result
function debounced() {
const context = this
const args = arguments
if (timerId) clearTimeout(timerId)
if (leading === true) {
if (!timerId) result = func.apply(context, args)
timerId = setTimeout(() => {
// 重置 timerId 的值。
timerId = null
}, wait)
} else {
timerId = setTimeout(() => {
func.apply(context, args)
}, wait)
}
return result
}
// 取消
debounced.cancel = function() {
clearTimeout(timerId)
timerId = null
}
return debounced
}
// 测试:
function getCity(e) {
console.log('鼠标移动了,事件对象:', e)
}
const container = document.getElementById('container')
const btnCancel = document.getElementById('container')
const handleMouseMove = debounce(getCity, 2000, true)
container.addEventListener('mousemove', handleMouseMove)
btnCancel.addEventListener('click', () => {
handleMouseMove.cancel()
})