防抖和节流
这是另一个常见的前端面试问题。它测试面试者对 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} />
这种方法看起来不错,但也存在一些问题:
- 每次按键事件都会调用一次 API。如果用户输入 15 个字符,那么每个用户就需要调用 15 次 API。这种方式永远无法扩展。
- 当这 15 个 API 调用的结果返回时,您只需要最后一个。前 14 个调用的结果将被丢弃。这会占用大量用户带宽,网络速度较慢的用户会感受到明显的延迟。
- 在 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)} />
我们扩展了现有的代码以利用去抖动功能。
该debounce
函数是通用实用函数,它接受两个参数:
fn
:应该延迟的函数调用。delay
:延迟时间(以毫秒为单位)。
在函数内部,我们使用setTimeout
来延迟实际的 function( fn
) 调用。如果fn
在计时器耗尽之前再次调用 ,则计时器将重置。
在我们更新的实现中,即使用户输入 15 个字符,我们也只会调用 1 次 API(假设每次按键耗时少于 500 毫秒)。这解决了我们在构建此功能时遇到的所有问题。
在生产代码库中,您无需编写自己的实用函数。您的公司很可能已经使用了包含这些方法的debounce
JS 实用库,例如 lodash 。
节流
嗯,去抖动对于性能来说很棒,但在某些情况下,我们不想等待x
几秒钟才收到更改通知。
想象一下,你正在构建一个像 Google Docs 或 Figma 这样的协作工作区。其中一个关键特性是用户应该实时了解其他用户所做的更改。
到目前为止,我们只知道两种方法:
- 新手方法:每当用户移动鼠标指针或输入内容时,就调用一次 API。你已经知道这会带来多大的麻烦了。
- 去抖动方法:它确实解决了性能方面的问题,但从用户体验的角度来看,它很糟糕。你的同事可能写了 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)} />
我们修改了现有代码以使用throttle
函数。它接受两个参数:
fn
:实际要节流的功能。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);
}
}
}
}
此更新的实施确保我们不会错过任何数据。
Lodash 还提供了节流实用功能。
概括
- 去抖动和节流是性能优化技术。
- 这两种方法都基于类似的原理——延迟事情。
t
防抖动会在收到最后一个事件后等待,而节流fn
则会定期执行t
。- 去抖动用于搜索功能,节流用于实时应用程序(不限于这些)。