JS `Debounce` 与 `Throttle`:优化高频事件处理,减少不必要的执行

好的,各位老铁,咱们今天来聊聊前端性能优化里两个“老生常谈”但又“举足轻重”的家伙:Debounce(防抖)和 Throttle(节流)。 别看名字挺唬人,其实原理简单得就像你早上吃的那根油条,只不过要稍微加工一下,让它更符合现代社会“既要快,又要省”的要求。

开场白:为啥需要 Debounce 和 Throttle?

想象一下,你正在开发一个搜索框,用户每输入一个字,就要向服务器发送一次请求,获取搜索结果。如果用户输入速度很快,比如 1 秒钟敲了 5 个字,那就要发送 5 次请求。这不仅浪费服务器资源,还会让用户觉得卡顿。

再比如,你正在监听 windowresize 事件,每次窗口大小改变,都要重新计算页面布局。如果用户拖动窗口的速度很快,resize 事件就会被频繁触发,导致页面卡顿。

这些都是典型的高频事件,如果不加以控制,就会严重影响用户体验。Debounce 和 Throttle 就是用来解决这些问题的两把利器。

第一回合:Debounce(防抖)—— “你尽管来,我只认最后一次”

Debounce 的核心思想是:当事件被触发后,延迟一段时间执行回调函数。如果在延迟时间内再次触发事件,则重新计时。只有当延迟时间内没有再次触发事件,才会执行回调函数。

简单来说,Debounce 就像一个“守门员”,只有在一段时间内没有新的请求进来,才会放行最后一个请求。

1. 简单实现:

function debounce(func, delay) {
  let timerId;

  return function(...args) {
    // 清除之前的定时器
    clearTimeout(timerId);

    // 创建一个新的定时器
    timerId = setTimeout(() => {
      func.apply(this, args); // 执行回调函数
    }, delay);
  };
}

代码解释:

  • debounce(func, delay):接受两个参数,func 是要执行的回调函数,delay 是延迟的时间(毫秒)。
  • timerId:用来保存定时器的 ID。
  • return function(...args):返回一个新的函数,这个函数才是真正被调用的函数。 ...args 是ES6语法,将所有参数收集到一个数组中。
  • clearTimeout(timerId):每次调用返回的函数时,都会先清除之前的定时器。
  • setTimeout(() => { ... }, delay):创建一个新的定时器,延迟 delay 毫秒后执行回调函数。
  • func.apply(this, args):执行回调函数,this 指向调用返回的函数的上下文,args 是传递给回调函数的参数。

2. 使用场景:

  • 搜索框输入: 在用户停止输入一段时间后,才发送搜索请求。
  • 窗口 resize: 在窗口大小调整完毕后,才重新计算页面布局。
  • 按钮点击: 防止用户快速点击按钮,导致重复提交。

3. 示例代码:

<input type="text" id="search-input">
<div id="search-result"></div>

<script>
  const searchInput = document.getElementById('search-input');
  const searchResult = document.getElementById('search-result');

  function search(query) {
    // 模拟搜索请求
    console.log(`Sending search request for: ${query}`);
    searchResult.textContent = `Searching for: ${query}...`;

    setTimeout(() => {
      searchResult.textContent = `Results for: ${query}: [Result 1, Result 2, Result 3]`;
    }, 500); // 模拟延迟
  }

  const debouncedSearch = debounce(search, 300); // 延迟 300 毫秒

  searchInput.addEventListener('input', (event) => {
    debouncedSearch(event.target.value);
  });
</script>

代码解释:

  • 当用户在 search-input 中输入时,会触发 input 事件。
  • debouncedSearch 函数会在用户停止输入 300 毫秒后执行。
  • 如果用户在 300 毫秒内再次输入,debouncedSearch 函数会重新计时。

4. 带立即执行的 Debounce:

有时候,我们希望在第一次触发事件时立即执行回调函数,然后在延迟时间内忽略后续的触发。

function debounce(func, delay, immediate) {
  let timerId;

  return function(...args) {
    const context = this; // 保存 this 上下文
    const callNow = immediate && !timerId; // 是否立即执行

    clearTimeout(timerId);

    timerId = setTimeout(() => {
      timerId = null; // 重置 timerId
      if (!immediate) {
        func.apply(context, args); // 延迟执行
      }
    }, delay);

    if (callNow) {
      func.apply(context, args); // 立即执行
    }
  };
}

代码解释:

  • immediate:一个布尔值,表示是否立即执行回调函数。
  • callNow:一个布尔值,表示是否应该立即执行回调函数。只有当 immediatetruetimerIdnull 时,才应该立即执行。
  • timerId = null;:在定时器执行完毕后,需要将 timerId 重置为 null,以便下次可以立即执行。
  • context = this; 保存 this 上下文,避免在setTimeout中this指向window

第二回合:Throttle(节流)—— “你来一次,我就让你歇一会”

Throttle 的核心思想是:在一段时间内,只允许回调函数执行一次。

简单来说,Throttle 就像一个“水龙头”,即使你一直拧着,也只能在一段时间内流出一定量的水。

1. 简单实现:

function throttle(func, delay) {
  let lastCalled = 0; // 上次执行回调函数的时间

  return function(...args) {
    const now = Date.now(); // 当前时间

    if (now - lastCalled >= delay) {
      func.apply(this, args); // 执行回调函数
      lastCalled = now; // 更新上次执行回调函数的时间
    }
  };
}

代码解释:

  • throttle(func, delay):接受两个参数,func 是要执行的回调函数,delay 是时间间隔(毫秒)。
  • lastCalled:用来保存上次执行回调函数的时间。
  • return function(...args):返回一个新的函数,这个函数才是真正被调用的函数。
  • now - lastCalled >= delay:判断当前时间与上次执行回调函数的时间间隔是否大于等于 delay
  • func.apply(this, args):执行回调函数。
  • lastCalled = now:更新上次执行回调函数的时间。

2. 使用场景:

  • 滚动事件: 在滚动过程中,每隔一段时间执行一次回调函数。
  • 鼠标移动事件: 在鼠标移动过程中,每隔一段时间执行一次回调函数。
  • 游戏中的射击频率: 限制玩家的射击频率。

3. 示例代码:

<div id="scrollable-area" style="height: 200px; overflow: auto;">
  <div style="height: 1000px;">Scroll me!</div>
</div>

<script>
  const scrollableArea = document.getElementById('scrollable-area');

  function handleScroll() {
    console.log('Scrolling...');
  }

  const throttledScroll = throttle(handleScroll, 200); // 每 200 毫秒执行一次

  scrollableArea.addEventListener('scroll', throttledScroll);
</script>

代码解释:

  • 当用户滚动 scrollable-area 时,会触发 scroll 事件。
  • throttledScroll 函数会每 200 毫秒执行一次。
  • 即使用户滚动速度很快,handleScroll 函数也不会被频繁执行。

4. 使用定时器的 Throttle:

除了使用时间戳,还可以使用定时器来实现 Throttle。

function throttle(func, delay) {
  let timerId = null;

  return function(...args) {
    const context = this;

    if (!timerId) {
      timerId = setTimeout(() => {
        func.apply(context, args);
        timerId = null; // 重置 timerId
      }, delay);
    }
  };
}

代码解释:

  • timerId:用来保存定时器的 ID。
  • if (!timerId):判断是否已经存在定时器。如果不存在,则创建一个新的定时器。
  • timerId = null;:在定时器执行完毕后,需要将 timerId 重置为 null,以便下次可以创建新的定时器。

Debounce vs Throttle:傻傻分不清楚?

特性 Debounce (防抖) Throttle (节流)
执行时机 在一段时间内没有再次触发事件后执行 在一段时间内,只允许执行一次
适用场景 搜索框输入、窗口 resize、按钮点击 滚动事件、鼠标移动事件、游戏中的射击频率
目标 减少不必要的执行,避免资源浪费 限制执行频率,防止页面卡顿
形象比喻 “你尽管来,我只认最后一次” “你来一次,我就让你歇一会”
触发事件后 延迟执行,如果在延迟时间内再次触发,则重新计时 立即执行(或在delay时间之后),后续触发会被忽略直到下一个delay时间段
最终结果 只执行一次(最后一次) 在一段时间内最多执行一次

总结:

  • Debounce: 适用于只需要最后一次结果的场景。
  • Throttle: 适用于需要限制执行频率的场景。

进阶:Lodash 的 Debounce 和 Throttle

Lodash 是一个非常流行的 JavaScript 工具库,提供了很多实用的函数,包括 Debounce 和 Throttle。Lodash 的实现更加完善,考虑了更多的边界情况。

// Lodash 的 Debounce
_.debounce(func, [wait=0], [options={}])

// Lodash 的 Throttle
_.throttle(func, [wait=0], [options={}])

使用 Lodash 的 Debounce 和 Throttle 可以省去自己编写代码的麻烦,而且可以保证代码的质量。

结尾:

Debounce 和 Throttle 是前端性能优化中非常重要的两个概念。掌握了它们,可以有效地提高 Web 应用的性能和用户体验。希望通过今天的讲解,大家能够对 Debounce 和 Throttle 有更深入的理解,并在实际开发中灵活运用。

记住,优化永无止境,让我们一起努力,打造更流畅、更高效的 Web 应用!

下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注