如何理解 防抖和节流?

509 阅读9分钟

一、什么是防抖(Debounce)

防抖是一种控制函数执行频率的函数,它的主要目的是延迟函数的执行,直到事件停止触发一段时间后才执行。防抖通常应用于那些用户快速频繁操作的场景中,例如搜索框的输入,防止用户每输入一个字符就触发一次请求。

工作原理

当事件发生时,防抖会重新设置计时器,如果在预定的延迟时间内没有再次触发事件,则执行目标函数;否则,重新启动计时器。

举个例子,假设我们为输入框绑定了keyup事件,当用户每输入一个字符时,我们会调用某个函数进行搜索操作。如果不进行防抖处理,用户每输入一个字符都会触发一次搜索请求,这样的行为对于性能会产生很大的压力。防抖通过延迟执行搜索请求,确保只有在用户停止输入一段时间后才真正发起请求。

防抖的应用场景

  • 输入框实时搜索:当用户在输入框中输入内容时,触发请求进行搜索。使用防抖可以避免每次输入都发送请求,从而减轻服务器压力。
  • 窗口调整大小:浏览器窗口大小变化时触发resize事件,频繁触发可能导致不必要的重绘。使用防抖可以减少重绘的次数。
  • 按钮点击:防止在用户短时间内连续点击按钮,避免多次执行同一操作。

防抖的代码

function debounce(func, delay) {
  let timer;
  return function (...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => func.apply(this, args), delay);
  };
}

const searchInput = document.querySelector("#search");
searchInput.addEventListener("keyup", debounce(function () {
  console.log("Searching for:", searchInput.value);
}, 300));

我们通过debounce函数来处理输入框的keyup事件,确保只有在用户停止输入300毫秒后才会执行搜索操作。


二、什么是节流(Throttle)

节流是控制函数执行频率的另一种方式,它的目的是限定函数的执行频率,保证在一定时间内函数最多只执行一次。节流技术适用于那些频繁触发,但又不要求每次都执行的场景。

工作原理

节流会以固定的时间间隔执行目标函数,每次触发事件时,只有在上次函数执行的时间超过设定的间隔,才会执行目标函数。如果触发事件的频率高于设定的时间间隔,函数会被跳过。

举个例子,假设我们要监听浏览器的滚动事件,滚动事件会非常频繁地触发。如果每次触发都执行回调函数,页面性能可能会受到很大的影响。通过节流,我们可以控制滚动回调的执行频率,减少不必要的计算。

节流的应用场景

  • 滚动监听:滚动条位置变化时可能会频繁触发滚动事件,使用节流可以减少事件的触发频率。
  • 窗口调整大小:类似于防抖,节流可以限制窗口调整大小时执行的频率,避免频繁计算布局。
  • 动画帧:在动画过程中,控制动画回调的执行频率,避免在每一帧都做大量计算。

节流的代码

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

window.addEventListener("scroll", throttle(function () {
  console.log("Scroll event triggered");
}, 1000));

在这个例子中,我们为scroll事件添加了节流处理,确保每1000毫秒内只执行一次回调函数,从而减少浏览器的性能消耗。


三、防抖与节流的区别

特性防抖(Debounce)节流(Throttle)
执行时机事件停止触发一段时间后执行一次固定时间间隔内最多执行一次
适用场景用户输入、表单提交、搜索建议等滚动、窗口调整大小、resize、动画帧等
执行频率触发间隔越短,执行次数越少控制执行频率,确保每隔一定时间执行一次
适用问题减少频繁操作引发的重复请求限制频繁触发事件的频率

简单的应用对比

  1. 防抖适用于需要等待事件结束后执行的操作,比如用户输入结束后的搜索。
  2. 节流适用于需要固定时间间隔内执行的操作,比如滚动监听或定时任务。

四、如何选择防抖和节流?

在实际开发中,选择使用防抖还是节流,要根据具体的需求来决定:

  • 如果事件触发频率非常高,而你只关心最终的执行结果(例如用户输入或按钮点击),那么选择防抖
  • 如果事件触发频率很高且需要定期执行某些操作(例如滚动监听、游戏中的帧渲染等),那么选择节流

此外,也可以将两者结合使用,来应对复杂的应用场景。例如,滚动监听可以结合节流处理,而按钮点击事件可以使用防抖来避免多次提交。


五、防抖节流实际应用场景

1. 输入框实时搜索(防抖)

应用场景:在输入框中实时搜索是现代Web应用中常见的功能,尤其是在电子商务、社交媒体或搜索引擎中。用户在输入关键词时,页面会根据输入的内容动态显示搜索结果。如果每次用户输入一个字符都向服务器发送请求,系统会面临大量的请求压力,甚至可能导致页面响应变慢。

解决方案:使用防抖技术,可以避免在每次键盘输入时都发送请求。只有当用户停止输入一段时间后,才会触发请求。

// 防抖函数
function debounce(func, delay) {
  let timer;
  return function (...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => func.apply(this, args), delay);
  };
}

// 输入框实时搜索
const searchInput = document.querySelector("#search");
searchInput.addEventListener("keyup", debounce(function () {
  const query = searchInput.value;
  console.log("Searching for:", query); // 这里可以发起真实的网络请求
}, 500));

效果:在用户停止输入500毫秒后,才会发起一次搜索请求,极大减少了不必要的请求,提升了性能。


2. 页面滚动事件(节流)

应用场景:当用户滚动页面时,scroll事件会频繁触发。如果不加控制,每次触发scroll事件都执行一个耗时的操作(例如,加载更多数据或计算滚动位置),可能导致页面卡顿,影响用户体验。

解决方案:使用节流技术,限制滚动事件处理函数的执行频率,避免每次滚动时都触发大量计算。

// 节流函数
function throttle(func, delay) {
  let lastTime = 0;
  return function (...args) {
    const now = new Date().getTime();
    if (now - lastTime >= delay) {
      func.apply(this, args);
      lastTime = now;
    }
  };
}

// 页面滚动加载更多
window.addEventListener("scroll", throttle(function () {
  const scrollPosition = window.scrollY + window.innerHeight;
  const documentHeight = document.documentElement.scrollHeight;

  // 判断是否接近页面底部,进行数据加载
  if (scrollPosition >= documentHeight - 100) {
    console.log("Loading more content...");
    // 发起加载更多数据的请求
  }
}, 200)); // 每200ms执行一次

效果:即使用户快速滚动页面,也会限制scroll事件处理函数的调用频率,每200ms才执行一次,减少不必要的计算和DOM操作。


3. 窗口调整大小(节流)

应用场景:当用户调整浏览器窗口大小时,resize事件会频繁触发。如果在每次触发时进行重计算(如重新布局、调整元素大小等),可能导致页面性能下降,特别是在响应式设计的页面中。

解决方案:使用节流技术,减少resize事件处理的频率,避免重复执行布局计算。

// 节流函数
function throttle(func, delay) {
  let lastTime = 0;
  return function (...args) {
    const now = new Date().getTime();
    if (now - lastTime >= delay) {
      func.apply(this, args);
      lastTime = now;
    }
  };
}

// 监听窗口调整大小
window.addEventListener("resize", throttle(function () {
  console.log("Window resized:", window.innerWidth, window.innerHeight);
  // 执行需要重新计算的逻辑,如重新调整布局
}, 300)); // 每300ms执行一次

效果:通过节流技术,确保在窗口调整大小时不进行过多的计算,从而提高页面的流畅度。


4. 表单提交按钮防止重复点击(防抖)

应用场景:在一些表单提交场景中,用户可能在短时间内多次点击提交按钮,导致多次请求发出,甚至出现重复提交。常见的场景如支付、注册等。

解决方案:使用防抖技术,防止用户短时间内多次点击按钮,确保表单只提交一次。

// 防抖函数
function debounce(func, delay) {
  let timer;
  return function (...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => func.apply(this, args), delay);
  };
}

// 防止重复提交表单
const submitButton = document.querySelector("#submitBtn");
submitButton.addEventListener("click", debounce(function () {
  console.log("Form submitted!");
  // 发送表单请求
  // submitForm();
}, 1000)); // 1000ms内只触发一次提交

效果:即使用户快速多次点击按钮,也只会触发一次提交操作,防止重复提交。


5. 图片懒加载(节流)

应用场景:在长列表或长页面中,图片通常会根据用户滚动加载。scroll事件的触发频率可能非常高,如果每次滚动都计算图片是否需要加载,可能会造成性能问题。

解决方案:使用节流控制滚动事件的处理频率,确保只在合适的时间点计算哪些图片需要加载。

// 节流函数
function throttle(func, delay) {
  let lastTime = 0;
  return function (...args) {
    const now = new Date().getTime();
    if (now - lastTime >= delay) {
      func.apply(this, args);
      lastTime = now;
    }
  };
}

// 图片懒加载
const images = document.querySelectorAll("img.lazyload");

function lazyLoadImages() {
  images.forEach(img => {
    if (img.getBoundingClientRect().top < window.innerHeight && !img.src) {
      img.src = img.dataset.src; // 从data-src属性加载图片
      img.classList.remove("lazyload");
    }
  });
}

window.addEventListener("scroll", throttle(lazyLoadImages, 300)); // 每300ms触发一次

效果:通过节流,减少每次滚动时对图片的加载检查,确保只有在需要加载图片时才触发计算,优化页面性能。


6. 表单验证(防抖)

应用场景:用户在表单中输入数据时,需要进行实时的字段验证。如果每次用户输入时都触发验证逻辑,会导致页面性能下降,尤其是在多个输入字段的情况下。

解决方案:使用防抖,确保只有在用户停止输入一段时间后才进行表单验证,从而减少不必要的验证请求。

// 防抖函数
function debounce(func, delay) {
  let timer;
  return function (...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => func.apply(this, args), delay);
  };
}

// 实时验证用户名
const usernameInput = document.querySelector("#username");
usernameInput.addEventListener("input", debounce(function () {
  const username = usernameInput.value;
  console.log("Validating username:", username);
  // 发起验证请求
}, 500)); // 输入停止500ms后触发验证

效果:防止在每次用户输入时都进行验证,只在用户停止输入500毫秒后触发一次验证,提升页面响应速度。

六、总结

  • 防抖:适用于需要等待事件完成后执行一次的场景。
  • 节流:适用于需要限制执行频率的场景。