引言
大家好啊,我是前端拿破轮。
今天和大家聊聊节流 和防抖。
只要你是一位前端开发者,并且参加过面试,那么大概率对这两个东西并不陌生。很多面试中经常让我们手写实现节流防抖函数。今天拿破轮就和大家一起把节流防抖搞清楚,从此以后妈妈再也不用担心我的节流防抖了。
老规矩,带着问题来读文章,读完后大家可以回头再看看这几个问题解决没有,能否用自己的话解释清楚。
- 什么是节流防抖?
- 为什么需要节流防抖?
- 如何实现节流防抖?
- 节流防抖的最佳实践是什么?如何在项目中使用?
什么是节流防抖?
概念 | 定义 | 关键词 |
---|---|---|
防抖 | 多次触发事件后,只在最后一次触发结束一段时间后执行回调 | 拖延执行 |
节流 | 在一定时间内,事件只能触发一次。 | 间隔执行 |
防抖(Debounce)
防抖是指在事件被触发n秒后,再执行回调。如果在n秒内又被触发,则重新计时。简单来说,就是"等你停下来再说"。
这么说好像有点抽象。我们来看一个大家都耳熟能详的例子。
王者荣耀的回城机制。

当我们在游戏中点击回城按钮后,英雄会原地不动,等待下面的进度条完成,这就相当于回城事件被触发了 。但是此时回城还没有真正执行 ,得等待下面的进度条完成之后,才会真正回城。如果进度条还没有完成的时候,再次点击了回城按钮,那么之前的进度就会清空,重新开始新的进度条计时。直到最后一次点击回城按钮,并且后续没有再点击,才会在进度条结束后真正回城。
这就是防抖。当我们调用某个函数后,它不会立即执行,而是会等待一段时间,这个时间我们可以自由设置。在这个等待时间内,如果没有再次调用函数,则在等待时间结束后真正执行函数代码。如果在等待过程中再次调用了函数,则重新开始计时等待。
节流(Throttle)
节流是指规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,则只有第一次生效。简单来说,就是"按固定频率执行"。
怎么理解呢?
还是大家耳熟能详的王者荣耀,只不过与之类似的是王者荣耀的技能释放。

当我们使用马可波罗狠狠地用一技能扫一梭子之后,一技能会陷入冷却,在冷却期间,无论我们再怎么点击,点击多少次,都无法再释放出技能。除非技能冷却结束,刷新了新的技能,我们才可以再次释放。
这就是节流。当我们调用某个函数后,在设置的一段时间内,如果多次调用,也只有第一次生效。除非超出当前设定的时间,才能再次调用。
相信通过上面的两个例子,大家对于什么是防抖和节流应该有了比较直观的认识,可以再好好体会一下。
为什么需要节流和防抖
节流和防抖的目的是差不多的,就是限制某些函数的调用频率。
我们下面以防抖为例,来看一下为什么需要防抖。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>联想搜索 - 防抖对比</title>
<style>
body {
font-family: sans-serif;
padding: 20px;
}
input {
width: 300px;
padding: 8px;
margin-bottom: 10px;
}
ul {
margin-top: 5px;
padding-left: 20px;
}
li {
line-height: 1.6;
}
.section {
margin-bottom: 40px;
}
#log {
margin-top: 20px;
font-family: monospace;
background: #f9f9f9;
padding: 10px;
border: 1px solid #ccc;
height: 100px;
overflow: auto;
}
</style>
</head>
<body>
<h1>联想搜索:防抖 vs 不防抖</h1>
<div class="section">
<h3>🔄 输入框(使用防抖)</h3>
<input type="text" id="debounced-input" placeholder="输入关键词..." />
<ul id="debounced-result"></ul>
</div>
<div class="section">
<h3>⚡ 输入框(不使用防抖)</h3>
<input type="text" id="normal-input" placeholder="输入关键词..." />
<ul id="normal-result"></ul>
</div>
<div id="log"></div>
<script>
// 防抖函数
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
function log(msg) {
const logEl = document.getElementById('log');
const timestamp = new Date().toLocaleTimeString();
logEl.innerHTML += `[${timestamp}] ${msg}<br>`;
logEl.scrollTop = logEl.scrollHeight;
}
// 请求函数
async function fetchSuggestions(query, targetUl, label) {
if (query === '') {
targetUl.innerHTML = '';
return;
}
const res = await fetch(`http://localhost:10003/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
targetUl.innerHTML = data.map(item => `<li>${item}</li>`).join('');
log(`${label} 触发请求,关键词:${query}`);
}
// 带防抖的输入框
const debouncedInput = document.getElementById('debounced-input');
const debouncedResult = document.getElementById('debounced-result');
debouncedInput.addEventListener('input',
debounce((e) => fetchSuggestions(e.target.value.trim(), debouncedResult, '✅ 防抖'), 300)
);
// 不带防抖的输入框
const normalInput = document.getElementById('normal-input');
const normalResult = document.getElementById('normal-result');
normalInput.addEventListener('input',
(e) => fetchSuggestions(e.target.value.trim(), normalResult, '❌ 无防抖')
);
</script>
</body>
</html>
上面是一个html文件,展示的是我们在使用搜索功能时一个很常见的场景。当我们在输入框中输入部分字符时,会在下面进行联想搜索可能的结果。

那这些可能的结果是怎么来的呢?通常需要利用Ajax
向后端发送请求来获取。所以我们可以监听input
的input
事件,当有输入变化时,向后端发送请求。下面是使用express
书写的一个简单的后端服务器。
js
import express from 'express';
import cors from 'cors';
const app = express();
const PORT = 10003;
app.use(cors());
const keywords = [
'apple', 'application', 'apply', 'applet', 'banana', 'band', 'bank',
'cat', 'car', 'cart', 'camera', 'code', 'coding', 'color',
];
app.get('/search', (req, res) => {
const query = req.query.q?.toLowerCase() || '';
const matched = keywords.filter((word) => word.includes(query));
setTimeout(() => {
res.json(matched);
}, 300);
})
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
})
这种情况就是典型的需要进行防抖的场景。因为用户可能在很短的时间内快速输入多个字符,如果内容一有变化就发送请求,将会导致请求次数过于频繁,造成性能严重下降和请求浪费。
我们先来测试一下没有防抖的输入框。

我们可以看到,在没有防抖的情况下,当我们在输入框中写入apple
这五个字符的过程中,整整发送了五个请求!这显然不合适。那为什么防抖可以解决呢?回顾防抖的概念,在触发函数后开始计时,如果在计时内再次触发,则之前的计时作废,重新开始计时。直到计时结束后,才会真正执行函数。
在我们输入框的场景中,如果用户正在快速输入,那么他就会不断地打断计时,导致计时重新开始,只有输入完成间隔一段时间后,才会真正发送请求。下面是使用了防抖的输入框:

我们在输入框中输入apple后,只在最后输入完之后发送了一次请求,达到了我们的目的。
节流的话是用在需要第一次就触发的场景。核心思想和防抖是类似的。这里不再赘述,大家感兴趣可以自己体验。
如何实现节流防抖
这里我们实现最经典的节流和防抖。两者都应该是一个高阶函数(HOF),也就是它们接受一些参数,返回值是一个函数。
节流
经典节流通过时间戳来实现。第一次触发后立即执行,此后在我们设定的时间内触发无效,直到超出设定时间后,触发才会有新的执行。
js
const throttle = (fn, delay) => {
// 上一次调用的时间
let lastTime = 0;
return function (...args) {
// 获取现在的时间戳
const now = Date.now();
// 如果当前时间距离上次调用的时间大于等于delay则执行
if (now - lastTime >= delay) {
// 本次调用时间会作为后续调用的lastTime
lastTime = now;
fn.apply(this, args);
}
}
}
这里一定要注意两个点:
- 在进入
if (now - lastTime >= delay)
的判断分支之后,一定要记得更新lastTime
- 在调用
fn
时一定要将其this
绑定到我们返回的函数上,否则会出现错误。我们这里使用的是apply
进行绑定,所以传递的第二个参数值是一个数组。
防抖
经典的防抖通过计时器来实现。只有最后一次触发一段时间后才会执行。
js
const debounce = (fn, delay) => {
let timer = null;
return function (...args) {
// 清除之间的计时
clearTimeout(timer);
// 重新开始计时
timer = setTimeout(() => {
fn.apply(this, args);
}, delay)
}
}
这里也有两个点需要注意:
- 每次函数调用时要先清空计时器
setTimeout
的回调函数必须使用箭头函数才能绑定到我们返回的函数的this
。当然手动保存再赋值也是可以的。
节流和防抖的最佳实践是什么?如何在项目中使用
我们以React项目为例,说明在项目中具体怎么样使用节流和防抖比较好。
在React中,我们通常使用自定义Hooks来实现节流防抖,如下所示:
ts
// useThrottle
import { useCallback, useRef } from 'react';
export const useThrottle = <T extends (...args: unknown[]) => unknown>(
fn: T,
delay: number
): ((...args: Parameters<T>) => void) => {
const lastTime = useRef(0);
const throttled = useCallback(
(...args: Parameters<T>) => {
const now = Date.now();
if (now - lastTime.current >= delay) {
lastTime.current = now;
fn(...args);
}
},
[fn, delay]
);
return throttled;
};
ts
// useDebounce
import { useCallback, useEffect, useRef } from "react";
export const useDebounce = <T extends (...args: unknown[]) => unknown>(
fn: T,
delay: number
): ((...args: Parameters<T>) => void) => {
const timer = useRef<ReturnType<typeof setTimeout>>(void 0);
const debounced = useCallback((...args: Parameters<T>)=> {
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => {
fn(...args);
}, delay);
}, [fn, delay]);
useEffect(() => {
return () => clearTimeout(timer.current);
}, [])
return debounced;
};
注意在React的Hooks中不需要手动绑定this,因为函数式组件环境下不会使用this。
下面是在项目中具体的使用方式。
tsx
import React, { useState } from 'react';
import { useDebounce } from './path-to-hooks'; // 假设你把它放在 hooks 文件夹下
export function SearchInput() {
const [query, setQuery] = useState('');
// 传入一个函数和防抖时间
const debouncedSearch = useDebounce((value: string) => {
console.log('搜索请求发送:', value);
// 这里可以调用 API 请求接口
}, 500);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
debouncedSearch(e.target.value); // 输入时调用防抖函数
};
return (
<input
type="text"
value={query}
onChange={handleChange}
placeholder="请输入搜索内容"
/>
);
}
tsx
import React, { useEffect } from 'react';
import { useThrottle } from './path-to-hooks';
export function ScrollTracker() {
const handleScroll = useThrottle(() => {
console.log('滚动事件触发', window.scrollY);
}, 1000);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);
return <div style={{ height: '200vh' }}>滚动页面查看控制台</div>;
}
总结
本文从是什么,为什么,怎么做,以及项目使用的最佳实践4个方面总结了节流和防抖的相关知识。其中在正常环境下手写节流防抖的实现函数是面试的常考题目,需要重点掌握。
关于this
的绑定要注意,普通场景下需要进行this绑定,但是React的函数式组件Hooks中不需要。
好了,这篇文章就到这里啦,如果对您有所帮助,欢迎点赞,收藏,分享👍👍👍。您的认可是我更新的最大动力。由于笔者水平有限,难免有疏漏不足之处,欢迎各位大佬评论区指正。
往期推荐✨✨✨
我是前端拿破轮,关注我,一起学习前端知识,我们下期见!