防抖是什么?
防抖的核心思想是:在某个操作被频繁触发时,只有在最后一次操作之后的一段时间内没有再次触发,才真正执行该操作。 让我们来看看下面的代码来更好理解防抖的核心吧!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖节流</title>
</head>
<body>
<input type="text" id="inputA">
<script>
let inputA = document.getElementById('inputA')
function ajax(content) {
console.log('ajax request' + content);
}
inputA.addEventListener('keyup', function (event) {
ajax(event.target.value);
})
</script>
</body>
</html>
当每次在输入框当中按下键盘之后,都会触发事件。对此在这里需要用到防抖,防抖的目的就是当用户停止输入了指定时间间隔delay后(停止输入了0.3s),才会输出输入框里的内容,并且在这段时间当中不会触发输出事件。
当我们防抖之后可以看看当在输入框中输入文字时,控制台的效果:
手搓防抖代码
后面我将带大家一步步手戳手写出防抖的源代码,以及在手写过程当中遇到的问题。
1. 首先模拟一段ajax请求的函数
function ajax(content){
console.log('ajax request' + content)
}
接收内容参数并打印请求信息
2. 获取输入框元素
let inputB = document.getElementById('inputB')
DOM操作和事件绑定,获取ID为'inputB'的输入框元素
3. 定义防抖函数
function debounce(fun, delay) {
return function (args) {
let that = this
let _args = args
clearTimeout(fun.id)
fun.id = setTimeout(function () {
fun.call(that, _args)
}, delay)
}
}
第1行:定义防抖函数,接收原函数和延迟时间
第2行:返回一个新函数,用于包装原函数
第3行:保存当前执行上下文(this)
第4行:保存传入的参数
第5行:清除之前的定时器(通过fun.id存储定时器ID)
第6行:设置新的定时器,延迟执行
第7行:使用call方法调用原函数,保持正确的this指向和参数
4.创建防抖版本的ajax函数
let debounceAjax = debounce(ajax, 500)
将ajax函数包装成防抖函数,延迟时间为500毫秒
5.为输入框添加键盘抬起事件监听器:
inputB.addEventListener('keyup', function (e) {
debounceAjax(e.target.value)
})
当用户输入时,调用防抖函数,传入输入框的值。这样就实现了防抖效果:只有在用户停止输入500ms后才会真正发起请求,避免频繁的API调用。
闭包的作用
在这个防抖函数中,闭包发挥了关键作用,让我详细解释:
闭包的具体体现
1. 外层函数 debounce
function debounce(fun, delay) {
// 这里形成了闭包环境
return function (args) {
// 内层函数可以访问外层函数的参数
}
}
2. 闭包捕获的变量
fun:原始函数(ajax函数)delay:延迟时间(500ms)
闭包的具体作用
1. 保持函数引用
let debounceAjax = debounce(ajax, 500)
- 闭包让返回的函数能够记住传入的
ajax函数 - 每次调用
debounceAjax时,都能访问到原始的ajax函数
2. 维护定时器状态
clearTimeout(fun.id)
fun.id = setTimeout(function () {
fun.call(that, _args)
}, delay)
- 通过
fun.id属性存储定时器ID - 闭包让每次调用都能访问到同一个
fun对象 - 实现定时器的清除和重置
3. 参数传递
return function (args) {
let _args = args // 捕获当前调用的参数
// ...
fun.call(that, _args) // 将参数传递给原函数
}
- 闭包让内层函数能够接收并保存每次调用的参数
- 确保最终执行时使用最新的参数
闭包的优势
1. 数据封装
fun和delay参数被封装在闭包中- 外部无法直接访问这些变量
- 提供了良好的封装性
2. 状态保持
- 每次调用都能访问到相同的函数引用
- 定时器状态在多次调用间保持一致
- 实现防抖的核心机制
3. 内存效率
- 避免了全局变量的使用
- 每个防抖函数都有自己独立的作用域
- 防止变量污染
实际执行流程
// 第一次调用
debounceAjax("a") → 设置500ms定时器
// 200ms后第二次调用
debounceAjax("ab") → 清除之前的定时器,设置新的500ms定时器
// 300ms后第三次调用
debounceAjax("abc") → 清除之前的定时器,设置新的500ms定时器
// 500ms后,执行ajax("abc")
闭包让这个防抖函数能够:
- 记住原始函数和延迟时间
- 维护定时器状态
- 在多次调用间保持数据一致性
- 实现真正的防抖效果
this丢失解决
在这个防抖函数中,this丢失问题产生的原因如下:
this丢失的具体原因
1. 函数调用方式改变
// 原始调用方式
ajax.call(this, content) // this指向调用者
// 经过防抖包装后
debounceAjax(e.target.value) // this指向全局对象或undefined
2. 闭包中的this指向
function debounce(fun, delay) {
return function (args) {
// 这里的this指向调用debounceAjax时的上下文
let that = this // 保存当前的this
// ...
}
}
this丢失的具体场景
1. 事件处理中的this
inputB.addEventListener('keyup', function (e) {
debounceAjax(e.target.value) // this指向inputB元素
})
2. 对象方法中的this
const obj = {
name: 'test',
handleInput: function(value) {
debounceAjax(value) // this指向obj对象
}
}
3. 箭头函数中的this
inputB.addEventListener('keyup', (e) => {
debounceAjax(e.target.value) // this指向外层作用域
})
解决方案分析
1. 保存this引用
let that = this // 在闭包中保存当前的this
2. 使用call方法恢复this
fun.call(that, _args) // 使用保存的this调用原函数
this丢失的完整流程
// 步骤1:原始调用
obj.handleInput("test")
↓
// 步骤2:防抖函数内部
function (args) {
let that = this // this指向obj
// ...
}
↓
// 步骤3:定时器回调
setTimeout(function () {
fun.call(that, _args) // 使用保存的obj作为this
}, delay)
为什么需要保存this
1. 保持函数上下文
- 确保原函数在正确的上下文中执行
- 避免this指向错误导致的功能异常
2. 支持对象方法
const api = {
baseURL: 'https://api.example.com',
request: function(data) {
console.log(this.baseURL + '/api', data)
}
}
const debouncedRequest = debounce(api.request, 500)
// 如果不保存this,api.request中的this.baseURL将无法访问
3. 兼容不同调用方式
- 事件处理函数
- 对象方法
- 普通函数调用
通过保存this引用并使用call方法,防抖函数能够正确处理各种调用场景下的this指向问题。
让我们来看下手搓防抖的完整代码
<script>
function ajax(content) {
console.log('ajax request ' + content)
}
function debounce(fun, delay) {
return function (args) {
let that = this
let _args = args
clearTimeout(fun.id)
fun.id = setTimeout(function () {
fun.call(that, _args)
}, delay)
}
}
let inputB= document.getElementById('inputB')
let debounceAjax = debounce(ajax, 500)
inputB.addEventListener('keyup', function (e) {
debounceAjax(e.target.value)
})
</script>
防抖的应用场景
当然可以!以下是三个具体的防抖应用场景,并附有详细说明:
✅ 1. 搜索框输入联想(如百度搜索框)
防抖应用:
使用防抖技术后,设置一个等待时间(例如 300 毫秒),当用户停止输入 300 毫秒后才发送请求。如果在这段时间内用户又输入了新的字符,则重新计时。
📌 效果:减少了请求次数,减轻服务器压力,同时提升用户体验。
✅ 2. 窗口大小调整(resize 事件)
场景描述:
网页布局需要根据浏览器窗口的大小进行动态调整,比如响应式设计中的元素重排、图片切换等。
问题:
window.resize 事件会在窗口大小变化时频繁触发,可能导致页面不断重绘、重排,影响性能。
防抖应用:
对 resize 事件使用防抖处理,设置一个延迟时间(例如 200 毫秒),只有在窗口停止调整 200 毫秒后才执行布局更新逻辑。
📌 效果:避免频繁执行昂贵的布局计算,提高页面响应速度。
✅ 3. 高频按钮点击(如提交表单、点赞按钮)
场景描述:
用户可能连续多次点击某个按钮,比如提交表单、点赞、加购等操作。
问题:
如果没有限制,可能会导致重复提交、数据异常、接口被频繁调用等问题。
防抖应用:
对按钮的点击事件进行防抖处理,设置一定的时间间隔(如 500 毫秒),在这个时间内即使多次点击也只执行一次操作。
📌 效果:防止重复提交,保护后端接口,增强交互体验。
| 应用场景 | 使用防抖的原因 |
|---|---|
| 输入框搜索 | 减少请求频率,优化网络资源 |
| 窗口大小调整 | 避免频繁重绘重排,提高页面性能 |
| 高频按钮点击 | 防止重复操作,保证数据一致性 |
总结
防抖是一种优化高频触发事件的策略,通过延迟执行确保只有在用户操作停止后才触发函数,有效减少不必要的资源消耗。其核心在于利用定时器和闭包机制,结合
this上下文绑定,解决频繁调用导致的性能问题。典型场景包括搜索联想、窗口调整和按钮防重复提交。通过合理设置延迟时间,开发者能在提升用户体验的同时,降低服务器压力与页面渲染成本。掌握防抖的实现原理与应用场景,是构建高效前端交互的关键一环。