防抖和节流

2025-06-07

防抖和节流

这是另一个常见的前端面试问题。它测试面试者对 JS、性能和 FE 系统设计的知识。

这是前端面试问答系列的第2题。如果你想提升准备水平或保持更新,可以考虑注册FrontendCamp


去抖动和节流的工作原理相同 - 延迟内容 - 但仍然具有非常不同的方法和用例。

这两个概念对于开发高性能应用程序都很有用。你每天访问的几乎所有网站都以某种方式使用了 Debouncing 和 Throttling。

去抖动

去抖动的一个众所周知的用例是预先输入(或自动完成)。

假设您正在为一个拥有数千种产品的电商网站构建搜索功能。当用户尝试搜索某些内容时,您的应用会发起 API 调用,获取所有与用户查询字符串匹配的产品。

const handleKeyDown = async (e) => {
 const { value } = e.target;
 const result = await search(value);
 // set the result to a state and then render on UI
}

<Input onKeyDown={handleKeyDown} />
Enter fullscreen mode Exit fullscreen mode

这种方法看起来不错,但也存在一些问题:

  1. 每次按键事件都会调用一次 API。如果用户输入 15 个字符,那么每个用户就需要调用 15 次 API。这种方式永远无法扩展。
  2. 当这 15 个 API 调用的结果返回时,您只需要最后一个。前 14 个调用的结果将被丢弃。这会占用大量用户带宽,网络速度较慢的用户会感受到明显的延迟。
  3. 在 UI 上,这 15 个 API 调用将触发重新渲染。这会导致组件卡顿。

解决这些问题的方法是Debouncing

基本思路是等到用户停止输入。我们会延迟 API 调用。

const debounce = (fn, delay) => {
 let timerId;
 return function(...args) {
  const context = this;

  if (timerId) {
    clearTimeout(timerId);
  };
  timerId = setTimeout(() => fn.call(context, ...args), delay);
 }
}

const handleKeyDown = async (e) => {
 const { value } = e.target;
 const result = await search(value);
 // set the result to a state and then render on UI
}

<Input onKeyDown={debounce(handleKeyDown, 500)} />
Enter fullscreen mode Exit fullscreen mode

我们扩展了现有的代码以利用去抖动功能。

debounce函数是通用实用函数,它接受两个参数:

  1. fn:应该延迟的函数调用。
  2. delay:延迟时间(以毫秒为单位)。

在函数内部,我们使用setTimeout来延迟实际的 function( fn) 调用。如果fn在计时器耗尽之前再次调用 ,则计时器将重置。

在我们更新的实现中,即使用户输入 15 个字符,我们也只会调用 1 次 API(假设每次按键耗时少于 500 毫秒)。这解决了我们在构建此功能时遇到的所有问题。

在生产代码库中,您无需编写自己的实用函数。您的公司很可能已经使用了包含这些方法的debounceJS 实用库,例如​​ lodash 。

节流

嗯,去抖动对于性能来说很棒,但在某些情况下,我们不想等待x几秒钟才收到更改通知。

想象一下,你正在构建一个像 Google Docs 或 Figma 这样的协作工作区。其中一个关键特性是用户应该实时了解其他用户所做的更改。

到目前为止,我们只知道两种方法:

  1. 新手方法:每当用户移动鼠标指针或输入内容时,就调用一次 API。你已经知道这会带来多大的麻烦了。
  2. 去抖动方法:它确实解决了性能方面的问题,但从用户体验的角度来看,它很糟糕。你的同事可能写了 300 字的文字,而你最终只收到一次通知。这还能算是实时的吗?

这就是Throttling关键所在。它介于上述两种方法之间。其基本思路是——定期通知——不是在按键结束时,也不是每次按键时,而是定期通知。

const throttle = (fn, time) => {
 let lastCalledAt = 0;

 return function(...args) {
  const context = this;
  const now = Date.now();
  const remainingTime = time - (now - lastCalledAt);

  if (remainingTime <= 0) {
   fn.call(context, ...args);
   lastCalledAt = now;
  }
 }
}

const handleKeyDown = async (e) => {
 const { value } = e.target;
 // save it DB and also notify other peers
 await save(value);
}

<Editor onKeyDown={throttle(handleKeyDown, 1000)} />
Enter fullscreen mode Exit fullscreen mode

我们修改了现有代码以使用throttle函数。它接受两个参数:

  1. fn:实际要节流的功能。
  2. time:允许执行该函数的时间间隔。

实现很简单。我们将函数上次调用的时间存储在 中lastCalledAt。下次调用函数时,我们检查 是否time已经过去,只有过了 才执行fn

我们快完成了,但是这个实现有一个 bug。如果最后一个函数调用带有一些数据,time并且是在间隔内进行的,而之后没有调用,该怎么办?在我们目前的实现中,我们会丢失一些数据。

为了解决这个问题,我们将参数存储在另一个变量中,并在未收到事件时启动稍后调用的超时。

const throttle = (fn, time) => {
 let lastCalledAt = 0;
 let lastArgs = null;
 let timeoutId = null;

 return function(...args) {
  const context = this;
  const now = Date.now();
  const remainingTime = time - (now - lastCalledAt);

  if (remainingTime <= 0) {
   // call immediately
   fn.call(context, ...args);
   lastCalledAt = now;
   if (timeoutId) {
     clearTimeout(timeoutId);
     timeoutId = null;
   }
  } else {
    // call later if no event is received
    lastArgs = args;
    if (!timeoutId) {
      timeoutId = setTimeout(() => {
        fn.call(context, ...lastArgs);
        lastCalledAt = Date.now();
        lastArgs = null;
        timeoutId = null;
      }, remainingTime);
    }
  }
 }
}
Enter fullscreen mode Exit fullscreen mode

此更新的实施确保我们不会错过任何数据。

Lodash 还提供了节流实用功能。


概括

  1. 去抖动和节流是性能优化技术。
  2. 这两种方法都基于类似的原理——延迟事情。
  3. t防抖动会在收到最后一个事件后等待,而节流fn则会定期执行t
  4. 去抖动用于搜索功能,节流用于实时应用程序(不限于这些)。

资源

FrontendCamp
lodash

文章来源:https://dev.to/thesanjeevsharma/debouncing-and-throttle-4alb
PREV
您应该尝试的 5 个最酷的 VsCode 主题。
NEXT
Javascript 中的 call()、apply() 和 bind()