面试重要考点防抖节流(上)

75 阅读8分钟

防抖是什么?

防抖的核心思想是:在某个操作被频繁触发时,只有在最后一次操作之后的一段时间内没有再次触发,才真正执行该操作。 让我们来看看下面的代码来更好理解防抖的核心吧!

<!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),才会输出输入框里的内容,并且在这段时间当中不会触发输出事件。

image.png

当我们防抖之后可以看看当在输入框中输入文字时,控制台的效果:

image.png

手搓防抖代码

后面我将带大家一步步手戳手写出防抖的源代码,以及在手写过程当中遇到的问题。

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. 数据封装

  • fundelay参数被封装在闭包中
  • 外部无法直接访问这些变量
  • 提供了良好的封装性

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上下文绑定,解决频繁调用导致的性能问题。

典型场景包括搜索联想、窗口调整和按钮防重复提交。通过合理设置延迟时间,开发者能在提升用户体验的同时,降低服务器压力与页面渲染成本。掌握防抖的实现原理与应用场景,是构建高效前端交互的关键一环。