JavaScript防抖与节流

264 阅读7分钟

实际开发中并不建议自己写,工具库更好用,但其基本概念得懂。防抖和节流作为性能优化的重要手段,通过限制函数执行的频率或时机,确保在不影响功能的前提下,提高应用的响应速度和整体性能,从而提升用户体验。

为什么会有防抖和节流?

防抖(Debounce)和节流(Throttle)技术的出现,主要是为了解决在前端开发中由于用户交互或特定事件触发过快而导致的性能瓶颈和用户体验问题。 具体原因包括:

  • 性能考量: 在Web应用中,一些操作如快速连续点击按钮、频繁调整窗口大小或在输入框中快速键入内容,可能会导致相关事件处理函数被非常高频地调用。如果没有适当的控制,这会迅速增加浏览器的计算负担,消耗CPU资源,引起页面响应迟缓甚至卡顿。
  • 网络请求优化: 对于需要发起网络请求的事件处理,如搜索建议、表单验证等,如果不加限制地频繁触发,会导致大量不必要的HTTP请求,增加服务器压力,消耗用户流量,同时也降低了应用的响应速度。
  • 提升用户体验: 频繁的页面重绘或状态变更会使得UI显得不稳定或“颤抖”,影响用户的操作流畅感和满意度。特别是在动画、滚动等动态效果中,控制执行频率是保证视觉效果平滑的关键。
  • 资源节约: 在移动设备或网络环境不佳的情况下,频繁的计算和网络活动更加耗电且影响加载速度,合理地使用防抖和节流可以有效节省资源,提升应用在各种条件下的表现。

防抖与节流是什么?

防抖和节流是两种常见的函数调用优化策略,用于控制函数执行频率,以提升前端性能,尤其是在处理高频触发的事件时(如窗口滚动、键盘输入等)。

它们的主要区别在于处理连续触发的方式:

防抖(Debounce)

  • 目的:确保在最后一次调用后的固定延迟时间后再执行函数,如果在这个延迟期间又被调用,则重新开始计时。这适用于如搜索建议、表单验证等场景,确保只在用户停止操作一段时间后才执行相应操作。
  • 特点:适合减少连续快速操作导致的大量不必要的函数执行,比如网络请求。用户连续输入时,仅在网络请求最后一次输入后的一段时间发送请求。

节流(Throttle)

  • 目的:限制函数在一定时间间隔内只能执行一次,无论期间被触发多少次。这意味着无论事件多么频繁,函数都会按照固定的频率执行。
  • 特点:保证函数在给定时间区间内至少执行一次,且只执行一次,适合处理连续动画、滚动事件处理等,确保操作既不会无限制执行,也不会完全丢失交互响应,比如限制滚动事件处理函数的执行频率来保持平滑滚动体验。

总结

  • 防抖更多用于减少不必要的计算或网络请求,确保在操作停止后执行。
  • 节流则是为了控制函数执行的频率,确保在高频率操作下仍能维持一定的响应性,同时不致于过度消耗资源。
  • 两者都是提高Web应用性能和用户体验的有效手段,选择使用哪种策略取决于具体需求和场景。
  • 需要注意的是防抖函数会延迟执行,而节流函数会立即执行。而这里的延迟和立即并不准确,取决于事件循环的队列中的任务执行情况。
  • 理论上防抖可以无限执行,节流指定时间内至少执行一次,如果相同的执行次数,防抖总时间总是优于节流时间

怎么实现防抖和节流?

防抖实现思路(基本):

  1. 清除现有计时器:如果之前已经设置了要执行该函数的计时器(即函数已被请求执行但还在等待中),则取消这个即将执行的操作。
  2. 重新设置计时器:然后设置一个新的计时器,延迟执行目标函数。如果在这段时间内又被触发,计时器将不断被重新设置,因此只有当最后一次触发后的延迟时间结束后,目标函数才会真正执行。
/**
 * 函数防抖
 * @param {Function} func 需要被延迟执行的函数
 * @param {number} wait 延迟时间(毫秒)
 * @returns {Function} 返回一个新函数,该函数具有防抖功能
 */
function debounce(func, wait) {
  let timeout; // 用于存储setTimeout的ID。

  // 返回一个封装函数,用于实现防抖逻辑。
  return function () {
    const context = this; // 函数执行的上下文。
    const args = arguments; // 函数调用时的参数。

    // 清除之前的定时器,以防止旧的延迟执行函数被执行。
    if(timeout){
      clearTimeout(timeout);
    }

    // 设置新的定时器,当定时器到期时,执行原函数。
    timeout = setTimeout(() => func.apply(context, args), wait);
  };
}

节流实现思路时间戳版(基本):

  1. 记录上次执行时间:保存函数上一次被执行的时间戳。
  2. 计算剩余时间:在函数被调用时,比较当前时间和上次执行时间,判断是否已达到预设的时间间隔。如果未达到,则直接返回,不做处理;如果已超过设定间隔,则执行函数,并更新上次执行时间。
/**
 * 函数节流
 * @param {Function} func 要节流的函数
 * @param {number} limit 节流限制的时间间隔(毫秒)
 * @returns {Function} 返回一个新函数,新函数将控制调用原函数的频率
 */
function throttle(func, limit) {
  let lastExecutionTime = 0; // 上次函数执行的时间戳

  // 返回一个节流函数
  return function() {
    const context = this; // 函数执行的上下文
    const args = arguments; // 函数调用时的参数
    const now = Date.now(); // 当前时间戳

    // 如果自上次执行以来的时间超过了限制
    if (now - lastExecutionTime >= limit) {
      func.apply(context, args); // 执行原函数
      lastExecutionTime = now; // 更新上次执行的时间戳
    }
  };
}

节流实现定时器版(基本):

function throttle(func, wait) {
  let timeout = null;
  return function () {
    const context = this;
    const args = arguments;

    if (!timeout) {
      timeout = setTimeout(function () {
        timeout = null;
        func.apply(context, args);
      }, wait);
    }
  };
}

举一反三

好了,介绍了一些基本的防抖节流概念,网上还有一些它们的其他变种或实现,比如浏览器的requestAnimationFrame方法,有兴趣的可以去看看,以下是一些变种的概念:

防抖函数

  • 带超时的防抖函数(timed debounce):与基本防抖函数类似,但在函数触发后,会设置一个超时时间。如果在这个时间内再次触发,函数将不会执行。
  • 带优先级的防抖函数(priority debounce):这种实现方式允许在不同的优先级之间进行防抖。例如,可以有一个高优先级的操作,当它触发时,无论低优先级操作如何,都会执行高优先级操作。
  • 带去重功能的防抖函数(deduplicate debounce):这种实现方式会检查函数是否已经执行过,如果已经执行过,则不会再次执行。这可以帮助避免重复执行不必要的操作。

节流函数

  • 带超时的节流函数(timed throttle):与基本节流函数类似,但在函数触发后,会设置一个超时时间。如果在这个时间内再次触发,函数将不会执行。
  • 带优先级的节流函数(priority throttle):这种实现方式允许在不同的优先级之间进行节流。例如,可以有一个高优先级的操作,当它触发时,无论低优先级操作如何,都会执行高优先级操作。
  • 带去重功能的节流函数(deduplicate throttle):这种实现方式会检查函数是否已经执行过,如果已经执行过,则不会再次执行。这可以帮助避免重复执行不必要的操作。
    // 去重节流函数
    function deduplicateThrottle(func, wait) {
      let timeout, latestValue;
      return function (...args) {
        const context = this;
    
        // 如果没有计时器或者新的调用的值与上次不同,就更新latestValue
        if (!timeout || args[0] !== latestValue) {
          latestValue = args[0];
          clearTimeout(timeout);
          timeout = setTimeout(function () {
            timeout = null;
            func.apply(context, latestValue ? [latestValue] : args);
          }, wait);
        }
      };
    }
    
    // 测试函数
    const throttledLog = deduplicateThrottle(console.log, 500);
    
    // 连续调用
    throttledLog("A");
    setTimeout(() => throttledLog("B1"), 600);
    setTimeout(() => throttledLog("B2"), 1000);
    setTimeout(() => throttledLog("D"), 2000);
    throttledLog("C");
    // 输出 C B2 D