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

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

当我们使用马可波罗狠狠地用一技能扫一梭子之后,一技能会陷入冷却,在冷却期间,无论我们再怎么点击,点击多少次,都无法再释放出技能。除非技能冷却结束,刷新了新的技能,我们才可以再次释放。
这就是节流。当我们调用某个函数后,在设置的一段时间内,如果多次调用,也只有第一次生效。除非超出当前设定的时间,才能再次调用。
相信通过上面的两个例子,大家对于什么是防抖和节流应该有了比较直观的认识,可以再好好体会一下。
为什么需要节流和防抖
节流和防抖的目的是差不多的,就是限制某些函数的调用频率。
我们下面以防抖为例,来看一下为什么需要防抖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| <!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书写的一个简单的后端服务器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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),也就是它们接受一些参数,返回值是一个函数。
节流
经典节流通过时间戳来实现。第一次触发后立即执行,此后在我们设定的时间内触发无效,直到超出设定时间后,触发才会有新的执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const throttle = (fn, delay) => { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime >= delay) { lastTime = now; fn.apply(this, args); } } }
|
这里一定要注意两个点:
- 在进入
if (now - lastTime >= delay)的判断分支之后,一定要记得更新lastTime
- 在调用
fn时一定要将其this绑定到我们返回的函数上,否则会出现错误。我们这里使用的是apply进行绑定,所以传递的第二个参数值是一个数组。
防抖
经典的防抖通过计时器来实现。只有最后一次触发一段时间后才会执行。
1 2 3 4 5 6 7 8 9 10 11 12
| const debounce = (fn, delay) => { let timer = null; return function (...args) { clearTimeout(timer);
timer = setTimeout(() => { fn.apply(this, args); }, delay) } }
|
这里也有两个点需要注意:
- 每次函数调用时要先清空计时器
setTimeout的回调函数必须使用箭头函数才能绑定到我们返回的函数的this。当然手动保存再赋值也是可以的。
节流和防抖的最佳实践是什么?如何在项目中使用
我们以React项目为例,说明在项目中具体怎么样使用节流和防抖比较好。
在React中,我们通常使用自定义Hooks来实现节流防抖,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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; };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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。
下面是在项目中具体的使用方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import React, { useState } from 'react'; import { useDebounce } from './path-to-hooks';
export function SearchInput() { const [query, setQuery] = useState('');
const debouncedSearch = useDebounce((value: string) => { console.log('搜索请求发送:', value); }, 500);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setQuery(e.target.value); debouncedSearch(e.target.value); };
return ( <input type="text" value={query} onChange={handleChange} placeholder="请输入搜索内容" /> ); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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中不需要。
好了,这篇文章就到这里啦,如果对您有所帮助,欢迎点赞,收藏,分享👍👍👍。您的认可是我更新的最大动力。由于笔者水平有限,难免有疏漏不足之处,欢迎各位大佬评论区指正。
往期推荐✨✨✨
我是前端拿破轮,关注我,一起学习前端知识,我们下期见!