节流防抖的应用

126 阅读6分钟

一、为什么要用到防抖和节流

在日常的前端开发中,我们经常需要处理用户的各种交互行为,比如滚动、点击、输入等。这些行为在特定场景下可能会触发非常频繁的事件,例如:用户在滚动条上快速拖动、用户在输入框中快速连续输入、窗口大小被不断调整。

频繁触发回调导致的大量计算会引发页面的抖动甚至卡顿。为了规避这种情况,我们需要一些手段来控制事件被触发的频率。就是在这样的背景下,throttle(事件节流)和 debounce(事件防抖)出现了。

二、防抖

1.什么是防抖

防抖的中心思想是:在事件被触发后,等待一段时间(这个时间由我们设定)。如果在等待期间事件再次被触发,则重新计时。只有当等待期间没有再次触发事件时,函数才会执行一次。

在某些 RPG 游戏中,有些强力技能设计为:按下技能键后,角色开始吟唱(2 秒),如果在吟唱过程中再次按键或被打断,吟唱重新开始,只有完整吟唱 2 秒后,技能才会释放。这也是防抖的应用:只有在最后一次按键后的等待时间内没有新的干扰,才会执行最终操作。

image.png

2.防抖的实现

理解了大致的思路,我们来实现一个防抖函数:

/**
 * 防抖函数,用于限制函数的执行频率
 *
 * @param {Function} func - 需要防抖的函数
 * @param {number} wait - 等待时间(毫秒),即停止触发事件后多少毫秒执行函数
 * @return {Function} - 返回一个新的防抖函数
 */
function debounce(func, wait) {
  let timeout;

  return function executedFunction(...args) {
    // 延迟执行函数
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };

    // 清除之前的定时器,重新设置新的定时器
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

3.防抖的应用

防抖在前端开发中有多种应用场景:

  1. 搜索框输入优化:当用户在搜索框中输入内容时,使用防抖可以等待用户输入完成后再发送请求,避免频繁的 API 调用。
  2. 表单验证:在用户填写表单时,可以使用防抖来延迟验证,直到用户停止输入一段时间后再进行验证。
  3. 窗口调整事件处理:当用户调整浏览器窗口大小时,使用防抖可以等待调整完成后再重新计算布局。

下面是一个简单的防抖应用示例:

const inputField = document.getElementById("inputField");
function handleInput(event) {
  console.log("Input value:", event.target.value);
}

const debouncedHandleInput = debounce(handleInput, 1000);
inputField.addEventListener("input", debouncedHandleInput);

image.png

4.支持 immediate 模式

防抖函数的 immediate 模式是一种特殊的工作模式,与常规防抖不同,immediate 模式会在第一次触发事件时立即执行函数,而不是等待延迟结束后执行。在这种模式下,函数会在事件首次触发时立即执行,随后的连续触发会被忽略,直到延迟时间结束后才能再次触发执行。

/**
 * 手动实现防抖函数
 *
 * @param {Function} fn 要被防抖处理的函数
 * @param {number} delay 延迟执行的时间,单位为毫秒
 * @param {boolean} immediate 是否在第一次触发时立即执行函数,默认为false
 * @returns {Function} 返回一个防抖处理后的函数
 */
function debounce(fn, delay, immediate = false) {
  let timer = null;
  // 使用闭包保存定时器状态
  return function (...args) {
    const context = this;

    // 每次调用都要清除之前的定时器,重新计时
    if (timer) clearTimeout(timer);

    if (immediate) {
      const callNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, delay);

      // 如果当前是第一次触发或者上一次触发后已经超过了延时时间,则立即执行函数
      if (callNow) {
        fn.apply(context, args);
      }
    } else {
      timer = setTimeout(() => {
        fn.apply(context, args);
      }, delay);
    }
  };
}

三、节流

1.什么是节流

节流的中心思想是:在某个时间间隔内,无论事件触发多少次,函数都只执行一次。

在英雄联盟、Dota 等 MOBA 游戏中:每个技能都有冷却时间(如 10 秒),在冷却期间,无论玩家按下技能键多少次,技能都不会释放,只有等冷却结束后,才能再次释放技能,系统会在固定间隔后才接受新的技能释放请求。 这也是节流的体现:在规定的时间间隔内,无论触发多少次,都只执行一次操作。

image.png

2.节流的实现

理解了大致的思路,我们来实现一个节流函数:

/**
 * 节流函数
 * @param {Function} fn - 需要节流的回调函数
 * @param {number} interval - 节流时间间隔(毫秒)
 * @returns {Function} - 经过节流处理的新函数
 */
function throttle(fn, interval) {
  // 记录上一次触发回调的时间
  let last = 0;

  return function () {
    // 保存函数执行时的上下文和参数
    let context = this;
    let args = arguments;
    // 获取当前时间戳
    let now = +new Date();

    // 检查是否达到节流时间间隔
    if (now - last >= interval) {
      last = now;
      fn.apply(context, args);
    }
  };
}

3.节流的应用

节流在前端开发中有多种应用场景:

  1. 滚动事件优化:在处理页面滚动事件时,使用节流可以限制处理函数的执行频率,提高页面性能。
  2. 拖拽元素实现:在实现拖拽功能时,使用节流可以限制位置更新的频率,使拖拽更加流畅。
  3. 游戏中的按键处理:在游戏开发中,使用节流可以控制玩家操作的频率,如限制射击频率。

下面是一个简单的节流应用示例:

function handleScroll() {
  console.log("Scroll event handled");
}
const throttledScroll = throttle(handleScroll, 2000);
window.addEventListener("scroll", throttledScroll);

image.png

4.支持 leading 和 trailing 两种执行模式

节流函数(Throttle)中的 leading 和 trailing 是两种控制执行时机的模式:leading 模式(首次执行模式)使函数在事件序列开始时立即执行,适合需要立即响应的场景,如按钮点击反馈;trailing 模式(延迟执行模式)则使函数在事件序列结束后才执行,使用最后一次触发的参数,适合等待用户操作完成后再响应的场景,如调整窗口大小后重新布局。

/**
* 节流函数
*
* @param {Function} fn 要节流的函数。
* @param {number} delay 延迟的毫秒数。
* @param {boolean} [leading=true] 是否在第一次调用时立即执行函数。
* @param {boolean} [trailing=false] 是否在最后一次调用后执行函数。
* @returns {Function} 返回一个新的节流函数。

*/
function throttle(fn, delay, leading = true, trailing = false) {
  // 上次执行的时间戳
  let lastExecTime = 0;
  // 定时器变量
  let timer = null;

  return function (...args) {
    const now = Date.now();
    const context = this;
    // 如果是第一次调用且不需要立即执行
    if (!leading && lastExecTime === 0) {
      lastExecTime = now;
    }
    const elapsed = now - lastExecTime;
    if (elapsed >= delay) {
      if (timer) {
        clearTimeout(timer);
      }
      fn.apply(context, args);
      lastExecTime = now;
    } else if (trailing && !timer) {
      timer = setTimeout(() => {
        fn.apply(context, args);
        // 如果leading为false,重置为0,这样下次调用会被视为首次调用
        lastExecTime = leading ? Date.now() : 0;
        timer = null;
      }, delay - elapsed);
    }
  };
}

四、总结

节流和防抖都以闭包的形式存在,它们通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。

通过合理使用防抖和节流技术,可以有效提高 Web 应用的性能和用户体验,减少不必要的计算和网络请求,使应用响应更加流畅。