JS 防抖与节流

发布于:2025-03-28 ⋅ 阅读:(33) ⋅ 点赞:(0)

防抖

        核心思想:延迟执行,只有在事件触发的频率降低到一定程度后才会执行,而且如果事件持续触发,之前地所有执行都会被取消。

        有的操作是高频触发的,但是其实只需要一次触发。比如短时间内多次缩放页面resize,我们不应该每次缩放都去执行操作,应该只做一次。再比如监听输入框的输入,不应每次输入框内容变化都触发监听,应该是用户完成一段输入后再进行触发。

        防抖就是为了避免事件重复触发。

        具体步骤: 每当事件触发 就 开一个定时器 。如果再次触发,清除之前该事件的定时器,重设一个。直到定时器到时,触发操作。

示例:

function debounce(func, delay = 300) {
  let timer;
  
  // 返回一个新的函数,该函数在被调用时会延迟执行 func
  return function(...args) {
    // 保存当前 this 上下文(确保 func 的 this 正确)
    const context = this;
    
    // 清除之前的定时器,重置为最新触发
    if (timer) clearTimeout(timer);
    
    // 设置新的定时器,延迟执行目标函数
    timer = setTimeout(() => {
      func.apply(context, args); // 使用 apply 传递 this 和参数
    }, delay);
  };
}

// 使用示例:输入框搜索防抖
const input = document.querySelector('input');
input.addEventListener('input', debounce((e) => {
  console.log('搜索内容:', e.target.value);
}, 500));

防抖的变种:立即执行

在某些情况下,我们希望在事件第一次触发时立即执行函数,之后再按照防抖的规则进行处理。可以通过增加一个参数来实现这个功能:

function debounce(func, delay = 300, immediate = false) {
    let timer;
    return function (...args) {
        const context = this;
        const isFirstCall = !timer;
        // 清除之前的定时器
        clearTimeout(timer);
        if (immediate && isFirstCall) {
            // 第一次触发且设置了立即执行,则立即执行函数
            func.apply(context, args);
        }
        // 设置新的定时器
        timer = setTimeout(() => {
            timer = null;
            if (!immediate) {
                // 如果没有设置立即执行,则在延迟时间后执行函数
                func.apply(context, args);
            }
        }, delay);
    };
}

// 使用示例
function clickHandler() {
    console.log('按钮被点击');
}

const button = document.getElementById('myButton');
// 对 clickHandler 函数进行防抖处理,延迟时间为 1000 毫秒,立即执行
button.addEventListener('click', debounce(clickHandler, 1000, true));

节流

        防抖存在的问题:事件会一直等到用户完成操作后一段时间在操作,如果一直操作,会一直不触发。比如说是一个按钮,点击就发送请求,如果一直点,那么请求就会一直发布出去。这里正确的思路应该是第一次点击就发送,然后上一个请求回来后,才能再发。

        核心思想:间隔执行,保证在特定时间内至少执行一次,无论事件触发频率如何。某个操作希望上一次的完成后再进行下一次,或者希望隔一段时间触发一次。节流就是减少流量,将频繁触发的事件减少,并每隔一段时间执行。即,控制事件触发的频率

        执行步骤:事件触发之后,执行操作,然后设置一个定时器,在设定时间内相同的事件触发无效,过了定时器的时间后,操作才可以再次触发。

节流方式一:时间戳方式

function throttle(func, interval) {
    let lastTime = 0;
    return function (...args) {
        const context = this;
        const now = Date.now();
        if (now - lastTime >= interval) {
            func.apply(context, args);
            lastTime = now;
        }
    };
}

// 使用示例
function handleScroll() {
    console.log('滚动事件处理');
}

const scrollArea = document.getElementById('scrollArea');
// 对 handleScroll 函数进行节流处理,时间间隔为 500 毫秒
scrollArea.addEventListener('scroll', throttle(handleScroll, 500));
  • throttle 函数接收两个参数,func 是需要进行节流处理的函数,interval 是时间间隔。
  • 利用 lastTime 变量记录上一次函数执行的时间。
  • 每次事件触发时,获取当前时间 now,通过计算 now - lastTime 判断是否超过了设定的时间间隔。如果超过了,就执行函数并更新 lastTime

节流方式二:定时器实现方式

function throttle(func, interval) {
    let timer = null;
    return function (...args) {
        const context = this;
        if (!timer) {
            func.apply(context, args);
            timer = setTimeout(() => {
                timer = null;
            }, interval);
        }
    };
}

// 使用示例
function handleClick() {
    console.log('按钮点击处理');
}

const button = document.getElementById('button');
// 对 handleClick 函数进行节流处理,时间间隔为 1000 毫秒
button.addEventListener('click', throttle(handleClick, 1000));

节流方式三:时间戳和定时器结合

function throttle(func, interval) {
    let lastTime = 0;
    let timer = null;
    return function (...args) {
        const context = this;
        const now = Date.now();
        const remaining = interval - (now - lastTime);

        clearTimeout(timer);

        if (remaining <= 0) {
            func.apply(context, args);
            lastTime = now;
        } else {
            timer = setTimeout(() => {
                func.apply(context, args);
                lastTime = Date.now();
            }, remaining);
        }
    };
}

        这种结合方式综合了时间戳和定时器的优点,既能保证在开始时立即执行函数,又能在后续按照固定时间间隔执行。

综合示例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Debounce vs Throttle 可视化演示</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
    .demo-box { border: 2px solid #ddd; padding: 20px; margin: 20px 0; border-radius: 8px; }
    .stats { background: #f5f5f5; padding: 10px; margin: 10px 0; }
    .interactive-area { height: 150px; border: 2px dashed #999; margin: 10px 0; display: flex; align-items: center; justify-content: center; }
  </style>
</head>
<body>
  <h1>防抖(Debounce) vs 节流(Throttle)</h1>

  <!-- 输入框防抖演示 -->
  <div class="demo-box">
    <h3>1. 输入框搜索 - 防抖演示</h3>
    <input type="text" id="searchInput" placeholder="输入内容观察防抖效果">
    <div class="stats">
      输入事件触发: <span id="inputCount">0</span> 次,
      实际处理: <span id="inputProcessed">0</span> 次
    </div>
  </div>

  <!-- 按钮点击防抖/节流演示 -->
  <div class="demo-box">
    <h3>2. 按钮点击 - 防抖 vs 节流</h3>
    <button id="debounceBtn">防抖按钮(1秒内只响应首次)</button>
    <button id="throttleBtn">节流按钮(1秒内只响应一次)</button>
    <div class="stats">
      防抖点击: <span id="debounceClick">0</span> 次,
      节流点击: <span id="throttleClick">0</span> 次
    </div>
  </div>

  <!-- 滚动节流演示 -->
  <div class="demo-box">
    <h3>3. 滚动事件 - 节流演示</h3>
    <div class="interactive-area" id="scrollArea">滚动此区域</div>
    <div class="stats">
      滚动事件触发: <span id="scrollCount">0</span> 次,
      实际处理: <span id="scrollProcessed">0</span> 次
    </div>
  </div>

  <!-- 鼠标移动防抖演示 -->
  <div class="demo-box">
    <h3>4. 鼠标移动 - 防抖 vs 节流</h3>
    <div class="interactive-area" id="mouseArea">在此移动鼠标</div>
    <div class="stats">
      防抖处理: <span id="debounceMouse">0</span> 次,
      节流处理: <span id="throttleMouse">0</span> 次
    </div>
  </div>

  <script>
    // ================= 工具函数 =================
    // 防抖函数(支持立即执行)
    function debounce(func, delay = 300, immediate = false) {
      let timer;
      return function(...args) {
        const context = this;
        const isFirstCall = !timer;
        
        clearTimeout(timer);
        if (immediate && isFirstCall) func.apply(context, args);
        
        timer = setTimeout(() => {
          timer = null;
          if (!immediate) func.apply(context, args);
        }, delay);
      };
    }

    // 节流函数(结合时间戳和定时器)
    function throttle(func, interval = 300) {
      let lastTime = 0, timer;
      return function(...args) {
        const context = this;
        const now = Date.now();
        const remaining = interval - (now - lastTime);

        clearTimeout(timer);
        
        if (remaining <= 0) {
          func.apply(context, args);
          lastTime = now;
        } else if (!timer) {
          timer = setTimeout(() => {
            func.apply(context, args);
            lastTime = Date.now();
            timer = null;
          }, remaining);
        }
      };
    }

    // ================= 示例逻辑 =================
    // 1. 输入框防抖
    const searchInput = document.getElementById('searchInput');
    let inputCount = 0, inputProcessed = 0;

    searchInput.addEventListener('input', () => {
      document.getElementById('inputCount').textContent = ++inputCount;
    });

    const processSearch = debounce(() => {
      document.getElementById('inputProcessed').textContent = ++inputProcessed;
    }, 500);
    searchInput.addEventListener('input', processSearch);

    // 2. 按钮点击
    const debounceBtn = document.getElementById('debounceBtn');
    const throttleBtn = document.getElementById('throttleBtn');
    let debounceClick = 0, throttleClick = 0;

    debounceBtn.addEventListener('click', debounce(() => {
      document.getElementById('debounceClick').textContent = ++debounceClick;
    }, 1000, true));

    throttleBtn.addEventListener('click', throttle(() => {
      document.getElementById('throttleClick').textContent = ++throttleClick;
    }, 1000));

    // 3. 滚动节流
    const scrollArea = document.getElementById('scrollArea');
    let scrollCount = 0, scrollProcessed = 0;

    scrollArea.addEventListener('scroll', () => {
      document.getElementById('scrollCount').textContent = ++scrollCount;
    });

    scrollArea.addEventListener('scroll', throttle(() => {
      document.getElementById('scrollProcessed').textContent = ++scrollProcessed;
    }, 500));

    // 4. 鼠标移动对比
    const mouseArea = document.getElementById('mouseArea');
    let debounceMouse = 0, throttleMouse = 0;

    // 防抖处理(延迟200ms)
    mouseArea.addEventListener('mousemove', debounce(() => {
      document.getElementById('debounceMouse').textContent = ++debounceMouse;
    }, 200));

    // 节流处理(每200ms执行一次)
    mouseArea.addEventListener('mousemove', throttle(() => {
      document.getElementById('throttleMouse').textContent = ++throttleMouse;
    }, 200));
  </script>
</body>
</html>

下面对上述代码中的四个例子分别进行分析:

1.输入框搜索-防抖演示
// 获取输入框元素
const searchInput = document.getElementById('searchInput');
// 记录输入事件触发的次数
let inputCount = 0;
// 记录实际处理的次数
let inputProcessed = 0;

// 监听输入框的 input 事件,每次触发时更新输入事件触发的次数
searchInput.addEventListener('input', () => {
  document.getElementById('inputCount').textContent = ++inputCount;
});

// 创建一个经过防抖处理的函数
const processSearch = debounce(() => {
  // 每次执行时更新实际处理的次数
  document.getElementById('inputProcessed').textContent = ++inputProcessed;
}, 500);
// 监听输入框的 input 事件,使用经过防抖处理的函数
searchInput.addEventListener('input', processSearch);

  • 功能:演示输入框输入时的防抖效果。当用户在输入框中输入内容时,input 事件会频繁触发,inputCount 会实时记录触发次数。而 processSearch 是经过防抖处理的函数,它会在用户停止输入 500 毫秒后才执行,inputProcessed 记录实际处理的次数。
  • 原理:使用 debounce 函数对输入事件处理函数进行包装,确保在用户停止输入一段时间后才执行实际处理逻辑,避免不必要的频繁处理。
2. 按钮点击防抖/节流演示
// 获取防抖按钮元素
const debounceBtn = document.getElementById('debounceBtn');
// 获取节流按钮元素
const throttleBtn = document.getElementById('throttleBtn');
// 记录防抖按钮点击的次数
let debounceClick = 0;
// 记录节流按钮点击的次数
let throttleClick = 0;

// 监听防抖按钮的 click 事件,使用经过防抖处理的函数
debounceBtn.addEventListener('click', debounce(() => {
  // 每次执行时更新防抖按钮点击的次数
  document.getElementById('debounceClick').textContent = ++debounceClick;
}, 1000, true));

// 监听节流按钮的 click 事件,使用经过节流处理的函数
throttleBtn.addEventListener('click', throttle(() => {
  // 每次执行时更新节流按钮点击的次数
  document.getElementById('throttleClick').textContent = ++throttleClick;
}, 1000));

  • 功能:演示按钮点击时的防抖和节流效果。debounceBtn 是防抖按钮,在 1 秒内多次点击,只有首次点击会立即响应;throttleBtn 是节流按钮,在 1 秒内无论点击多少次,只会响应一次。
  • 原理
    • 防抖:使用 debounce 函数对按钮点击事件处理函数进行包装,设置 immediate 为 true 表示首次点击立即执行,后续点击会在 1 秒内无新点击时才执行。
    • 节流:使用 throttle 函数对按钮点击事件处理函数进行包装,确保在 1 秒内最多执行一次处理函数。

3. 滚动节流演示 

// 获取滚动区域元素
const scrollArea = document.getElementById('scrollArea');
// 记录滚动事件触发的次数
let scrollCount = 0;
// 记录实际处理的次数
let scrollProcessed = 0;

// 监听滚动区域的 scroll 事件,每次触发时更新滚动事件触发的次数
scrollArea.addEventListener('scroll', () => {
  document.getElementById('scrollCount').textContent = ++scrollCount;
});

// 监听滚动区域的 scroll 事件,使用经过节流处理的函数
scrollArea.addEventListener('scroll', throttle(() => {
  // 每次执行时更新实际处理的次数
  document.getElementById('scrollProcessed').textContent = ++scrollProcessed;
}, 500));

  • 功能:演示滚动事件的节流效果。当用户滚动 scrollArea 区域时,scroll 事件会频繁触发,scrollCount 会实时记录触发次数。而经过节流处理的函数会确保每 500 毫秒最多执行一次,scrollProcessed 记录实际处理的次数。
  • 原理:使用 throttle 函数对滚动事件处理函数进行包装,通过时间戳和定时器结合的方式,控制处理函数的执行频率,避免频繁处理滚动事件。

4. 鼠标移动防抖演示

// 获取鼠标移动区域元素
const mouseArea = document.getElementById('mouseArea');
// 记录鼠标移动防抖处理的次数
let debounceMouse = 0;
// 记录鼠标移动节流处理的次数
let throttleMouse = 0;

// 监听鼠标移动区域的 mousemove 事件,使用经过防抖处理的函数
mouseArea.addEventListener('mousemove', debounce(() => {
  // 每次执行时更新鼠标移动防抖处理的次数
  document.getElementById('debounceMouse').textContent = ++debounceMouse;
}, 200));

// 监听鼠标移动区域的 mousemove 事件,使用经过节流处理的函数
mouseArea.addEventListener('mousemove', throttle(() => {
  // 每次执行时更新鼠标移动节流处理的次数
  document.getElementById('throttleMouse').textContent = ++throttleMouse;
}, 200));

  • 功能:演示鼠标移动时的防抖和节流效果。当用户在 mouseArea 区域移动鼠标时,mousemove 事件会频繁触发。经过防抖处理的函数会在用户停止移动鼠标 200 毫秒后执行,而经过节流处理的函数会确保每 200 毫秒最多执行一次。
  • 原理
    • 防抖:使用 debounce 函数对鼠标移动事件处理函数进行包装,确保在用户停止移动鼠标一段时间后才执行实际处理逻辑。
    • 节流:使用 throttle 函数对鼠标移动事件处理函数进行包装,控制处理函数的执行频率,避免频繁处理鼠标移动事件。