我被面试问懵后,熬夜吃透了节流和防抖
前段时间在面试前端时,面试官问我:"能手写一个带立即执行选项的防抖函数吗?"
我当场懵住了——这两个性能优化函数,咱们天天用在按钮点击、输入框搜索实时联想等事件里,却很少深究实现原理。
本文将结合我熬夜整理的节流和防抖案例,帮你全面重新认识节流和防抖,彻底吃透这两个前端必备技能。
# 认识节流机制
函数节流的核心思想是:事件触发后立即执行一次,然后在设定的时间周期内不再响应新的触发,直到周期结束后才重新开始接收事件。
就像咱们用的水龙头,拧紧之后水会一滴一滴均匀流出,而不是一直喷涌。用代码术语说,就是「把连续触发的事件按时间做平均分配执行」。
举个直观的例子:如果设定 2 秒为执行周期,第一次点击按钮会立即执行,接下来 2 秒内的所有点击都会被忽略,直到 2 秒后才允许再次执行。这种机制特别适合那些需要「匀速执行」的场景。
下面这个例子能清晰展示节流的效果——每 2 秒内不管点击多少次,只会执行一次:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<button id="throttle">测试函数节流</button>
</body>
<script src="https://cdn.bootcss.com/lodash.js/4.17.15/lodash.js"></script>
<script>
// 处理点击事件的回调函数
function handleClick(event) {
console.log("处理点击事件", this, event);
}
/**
* 使用 lodash 中的 throttle()
* 参数一:指定要处理的事件
* 参数二:延迟的时间(毫秒)
* 参数三:配置对象
*/
document.getElementById("throttle").onclick = _.throttle(
handleClick,
2000,
{
/**
* trailing 设置为 false:则最后一次点击不会执行
* 默认值为 true,则会在周期结束后执行最后一次
*/
trailing: false,
}
);
</script>
</html>
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
# 认识防抖机制
如果说节流像水龙头滴水,那防抖就像弹簧——你越是用力按压(频繁触发事件),它就越是要等你完全松手(停止触发)后才会反弹(执行函数)。
函数防抖(debounce) 的核心逻辑是:在事件触发后的一段时间内等待,如果在这段时间内没有再次触发事件,才执行函数。适用于那些需要等待一段时间后才执行的事件,以确保在连续触发事件时只执行一次。
举个直观的例子:比如说我设定了执行周期是 2 秒,那么我第一次点击之后,在后续的 2 秒内,如果我还有点击,那么就会更新最新的点击时刻,重新计算 2 秒,然后再去触发。
比如最常见的搜索框联想功能——用户输入过程中不需要频繁请求接口,只有当输入停顿超过 300 毫秒,才会触发搜索请求。这种"等待最后一次"的机制,完美解决了高频输入场景下的资源浪费问题。
同样我们来看一个防抖的代码案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="debounce">测试防抖函数</button>
</body>
<script src="https://cdn.bootcss.com/lodash.js/4.17.15/lodash.js"></script>
<script>
function handleClick(event) {
console.log("处理点击事件", this, event); // 该函数的this事件源, 即所绑定的dom元素
}
document.getElementById("debounce").onclick = _.debounce(handleClick, 2000);
</script>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在上面的代码中,如果从一开始就一直点击下去, 那么事件处理函数是不会执行的, 只有等到用户停止点击, 然后过了两秒后, 事件处理函数才会触发
# 节流和防抖典型使用场景
下面详细介绍两个典型应用场景,并深入分析如何选择和实现合适的优化方案。
# 防抖场景场景:搜索框输入联想优化
# 节流应用场景:窗口 resize 事件处理
# 节流函数实现
# 时间戳版实现(基础版)
其实 lodash 库的源码大致就是如下的实现方式
<body>
<button id="throttle">测试函数节流</button>
</body>
<script>
function throttle(callback, delay) {
// 一上来,不点击按钮就会执行一次,有且仅会执行一次,后续点击按钮都不会执行
console.log("hello");
// pre不要设置成 Date.now() ,否则第一次点击的时候是不会调用的
let pre = 0;
return function (event) {
// 这个才是真正的【节流函数】或者说是【真正的事件回调函数】, this 是触发事件的节点,即标签。
// 这个函数会高频触发,因为是点击按钮,无法限制
console.log("你点击按钮了");
// 获取当前的时间
const current = Date.now();
if (current - pre > delay) {
/**
* 1. 节流:说白了,就是需要满足【上一次调用handleClick函数已经过了delay的时间差了】,然后才去调用 【handleClick函数】
* 2. 【调用】真正【处理事件的回调函数】, 并且将真正【处理事件的回调函数,即下面的 handleClick 函数里的 this】中的 this
* 3. button.onClick = function() { console.log(this) } // this 是 button 按钮
*/
callback.call(this, event);
// 记录下,调用 handleClick 函数用了多长时间W
pre = current;
}
};
}
// 处理点击事件的回调函数
function handleClick(event) {
console.log("处理点击事件", this, event);
}
document.getElementById("throttle").onclick = throttle(handleClick, 2000);
</script>
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
# 定时器版实现(支持最后一次执行)
underscore 内部源码大致就是如下
function throttleByTimer(fn, interval) {
let timer = null; // 定时器状态
return function (...args) {
// 如果没有定时器,说明可以执行
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null; // 执行后清空定时器,允许下次触发
}, interval);
}
};
}
2
3
4
5
6
7
8
9
10
11
12
13
# 防抖函数实现
# 基础版实现(lodash 风格)
<body>
<button id="debounce">测试防抖函数</button>
</body>
<script>
function debounce(callback, delay) {
return function (event) {
// this 是事件源
// 判断对象上是否有某个属性的方式如下:
// 方式一:
// if (callback.timeoutId) {} 会去原型链找
// 方式二:
// callback.hasOwnProperty('timeoutId') ---- 效率更高,只会在自身找,不会到原型链上找. 普通对象可以使用 hasOwnProperty,根据原型链的查找过程
if (callback.hasOwnProperty("timeoutId")) {
clearTimeout(callback.timeoutId);
}
// 虽然我也不知道为什么要把timeoutId挂载到callback上面,那就先记住吧
callback.timeoutId = setTimeout(() => {
// 必须是箭头函数,否则下面的this就有问题
callback.call(this, event); // 修改callback的this为事件源
// 删除,标识 handleClick 已经处理完
delete callback.timeoutId;
}, delay);
};
}
function handleClick(event) {
console.log("处理点击事件", this, event);
}
document.getElementById("debounce").onclick = debounce(handleClick, 2000);
</script>
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
# 写在最后
我是七小,一个沉迷前端技术的工程师 | AIGC 探索者 | 副业赚钱实践者。
希望本文分享的节流与防抖知识对你有所帮助。
如果你在学习过程中遇到疑问,或者有更好的实践经验,欢迎在评论区交流。
最后求个赞!你的每一个鼓励,都是我持续输出优质内容的动力~