JavaScript 防抖与节流实现原理及代码详解

作者:闭包旅人时间:2026-10-24 16:22:26浏览:308

本文详细解析 JavaScript 中防抖(Debounce)与节流(Throttle)的核心原理及实现方式。通过自动门类比解释防抖逻辑,结合滚动统计等实际场景说明节流应用。文章深入剖析闭包在共享定时器中的作用,并完整展示处理 this 指向与参数传递的高级实现方案,帮助开发者有效优化高频事件触发的性能问题。

防抖

以自动门为例,当感应到有人靠近时,门会打开并启动 5 秒倒计时。若在 5 秒内有另一人靠近,门再次感应到人体,倒计时将重置为新的 5 秒。

在编程中,当事件触发时设置一个延迟执行时间。如果在延迟期间事件再次被触发,则重置延迟计时器,直到最后一次触发后的延迟时间结束,才执行最终动作。这种机制旨在防止事件被多次频繁触发。

在 Web 应用开发中,防抖技术常用于以下场景:

  • 监听页面大小改变事件以进行数据统计。
  • 监听页面滚动位置变化以进行统计或加载更多内容。
  • 控制输入框连续输入时的请求频率,避免频繁发送请求。

初始阶段,点击按钮会执行 console.log('pay money') 操作。


<body>
  <button id="btn">click</button>
</body>
<script>
  const btn = document.getElementById('btn')
  function payMoney() {
    console.log('pay money');
  }
  btn.addEventListener('click', payMoney)
</script>

定义 debounce 函数,该函数接收目标函数作为参数,并返回一个新的函数。


const btn = document.getElementById('btn')
function payMoney() {
  console.log('pay money');
}
function debounce(func) {
  // 在函数里面返回函数 , 只有当点击的时候才返回该函数
  return function () {
    func()
  }
}
btn.addEventListener('click', debounce(payMoney))

在返回的函数内部设置延时器,指定延迟执行的时间。


const btn = document.getElementById('btn')
function payMoney() {
  console.log('pay money');
}
function debounce(func, delay) {
  return function () {
    setTimeout(_ => func(), delay)
  }
}
btn.addEventListener('click', debounce(payMoney, 1000))

清除之前设置的延时器,确保未执行的旧任务被取消。

上述实现存在原因:

每次点击按钮时,都会执行返回函数内部的内容。

每次点击生成的执行函数都是独立的,彼此之间没有关联。

由于这些函数相互独立,清除延时器的操作无法作用于其他实例,因此在此阶段完全无效。

为了使这些独立的执行函数之间产生联系,需要利用作用域链(闭包)机制。


const btn = document.getElementById('btn')
function payMoney() {
  console.log('pay money');
}
function debounce(func, delay) {
  return function () {
    let timer
    clearInterval(timer)
    timer = setTimeout(_ => func(), delay)
  }
}
btn.addEventListener('click', debounce(payMoney, 1000))

将 timer 变量定义在返回函数的外部,这样在定义监听事件的同时,就定义了 timer 变量。

得益于作用域链,所有独立的执行函数都能访问到这个外部的 timer 变量。

此时,timer 变量只被创建了一次,是唯一的。我们只是不断给 timer 赋值以重置延时,而不是创建新的变量。

每次清除延时实际上就是清除上一个定义的延时,相当于多个函数共用同一个外部变量。


const btn = document.getElementById('btn')
function payMoney() {
  console.log('pay money');
}
function debounce(func, delay) {
  let timer
  return function () {
    clearInterval(timer)
    timer = setTimeout(_ => func(), delay)
  }
}
btn.addEventListener('click', debounce(payMoney, 1000))

此时的代码中,this 的指向是否为 window?

由于回调函数的执行上下文原因,运行时 this 已经指向了 Window 对象。


const btn = document.getElementById('btn')
function payMoney() {
  console.log('pay money');
  console.log(this);
}
function debounce(func, delay) {
  let timer
  return function () {
    clearInterval(timer)
    timer = setTimeout(_ => func(), delay)
  }
}
btn.addEventListener('click', debounce(payMoney, 1000))

针对 this 指向问题的解决办法:

在 setTimeout 之前将当前的 this 保存下来,此时 this 指向的是触发事件的按钮。


const btn = document.getElementById('btn')
function payMoney() {
  console.log('pay money');
  console.log(this);
}
function debounce(func, delay) {
  let timer
  // 只有当点击的时候,才返回此函数,所以 this 是指向按钮的
  return function () {
    let context = this
    clearInterval(timer)
    timer = setTimeout(_ => {
      func.apply(context)
    }, delay)
  }
}
btn.addEventListener('click', debounce(payMoney, 1000))

同时考虑参数传递的问题,添加 args 以保留原始参数。


const btn = document.getElementById('btn')
function payMoney() {
  console.log('pay money');
  console.log(this);
}
function debounce(func, delay) {
  let timer
  return function () {
    let context = this
    let args = arguments
    clearInterval(timer)
    timer = setTimeout(_ => {
      func.apply(context)
      console.log(context, args);
    }, delay)
  }
}
btn.addEventListener('click', debounce(payMoney, 1000))

节流

在事件第一次触发后立即执行,随后防止接下来多次触发,通过时间间隔限制来控制执行频率。

滚动屏幕场景:统计用户滚动屏幕的行为以作出相应的网页反应。

当用户不断进行滚动操作时,会不断产生请求,导致网络请求数量激增,容易引发 ⛔️ 网络阻塞。

我们可以在触发事件时立即执行任务,然后设定一个时间间隔。在这段时间内,无论用户如何滚动都忽略后续操作。

当时间间隔结束后,如果监测到用户仍有滚动行为,则再次执行任务,并重新设置时间间隔。

首先,编写一个在改变页面尺寸时同时改变页面背景颜色的代码示例。


function coloring() {
  let r = Math.floor(Math.random() * 255)
  let g = Math.floor(Math.random() * 255)
  let b = Math.floor(Math.random() * 255)
  document.body.style.background = `rgb(${r}, ${g}, ${b})`
}
window.addEventListener('resize', coloring)

function throttle(func, delay) {
  let timer
  return function () {
    timer = setTimeout(_ => {
      func()
    }, delay)
  }
}
window.addEventListener('resize', throttle(coloring, 2000))

判断当前触发的事件是否处于设定的时间间隔内。

  • 如果不在间隔内:触发事件并执行任务。
  • 如果在间隔内:不触发事件,忽略操作。

function throttle(func, delay) {
  let timer
  return function () {
    // timer 被赋值了,直接返回,即不执行任务
    if (timer) {
      return
    }
    // 此时 timer 没被赋值,或 timer 已经执行完了  
    // 为 timer 赋值进行延时执行
    timer = setTimeout(_ => {
      func()
      // 延迟执行以后我们要清空timer的值
      timer = null
    }, delay)
  }
}
window.addEventListener('resize', throttle(coloring, 2000))

解决 this 指向问题(虽然当前示例中 this 指向 Window,但需确保通用性)。


function throttle(func, delay) {
  let timer
  return function () {
    let context = this
    let args = arguments
    // timer 被赋值了,直接返回,即不执行任务
    if (timer) {
      return
    }
    // 此时 timer 没被赋值,或 timer 已经执行完了  
    // 为 timer 赋值进行延时执行
    timer = setTimeout(_ => {
      func.apply(context, args)
      // 延迟执行以后我们要清空timer的值
      timer = null
    }, delay)
  }
}
window.addEventListener('resize', throttle(coloring, 1000))

节流的核心在于控制事件间隔。另一种常见的时间间隔判断方法是使用 Date 对象。


function throttle(func, delay) {
  // 我们要和前一个时间点进行比较才能确定是否已经过了时间间隔
  // 在返回函数外围,避免每次执行都被自动修改
  let pre = 0
  return function () {
    // 保存执行函数当时的时间
    let now = new Date()
    // 刚开始,一定会执行
    if (now - pre > delay) {
      // 已经过了时间间隔,就可以执行函数了
      func()
      // 执行后,重新设置间隔点
      pre = now
    }
  }
}
window.addEventListener('resize', throttle(coloring, 1000))

解决参数传递问题,确保原始函数的参数能被正确传递。


function throttle(func, delay) {
  let pre = 0
  return function () {
    let context = this
    let args = arguments
    let now = new Date()
    if (now - pre > delay) {
      func.apply(context, args)
      pre = now
    }
  }
}
window.addEventListener('resize', throttle(coloring, 1000))

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注本站的更多内容!

作者最新文章