作者:Sean
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天
前言:防抖和节流是前端开发中重要的两个概念,但是其概念与其名称一样,没有浅显易懂。防抖和节流是两个不同的功能,都用于限制用户的频繁操作和资源请求,减轻服务器的压力,是前端开发中重要的性能优化方法。下面就让我们探究一下防抖和节流吧。
1. 防抖(debounce)
1.1 什么是防抖
让我们用一个例子来认识防抖:
需求
用户在搜索框中输入内容之后,在下拉框中显示输入内容相关的信息
那么,很显然,这需要获取用户输入的字符,然后以该字符向服务器发送请求,服务器返回与该字符相关的信息。
**但是,我们需要在用户每输入一个字符就发送一次请求吗?**这显然是不合理的,如果用户数量大,那么服务器将不堪重负。
我们就需要一点点优化,在用户输入一个字符的未来一段时间内,只要用户持续输入内容,就不发送请求。这时候就需要**“防抖”**,即在这规定的时间内,只有用户持续输入内容,发送请求的行为就会被打断,规定时间需要重新计时。这就像是游戏中的“吟唱读条“,在吟唱过程中如果受到了打断,吟唱则需要重头开始。
1.2 如何实现防抖
那么如何实现防抖呢?刚刚提到了计时的概念,那很显然需要用到定时器。让我们直接看代码:
function debounce(func, delay) {
let timer = null;
return function (...args) {
// 闭包返回函数
let context = this; // 保存this
timer && clearTimeout(timer); // 清除定时器
timer = setTimeout(() => {
// 设置定时器
func.call(context, ...args); // 绑定this指向和传参
}, delay);
};
}
分析
-
首先,我们需要为防抖函数传入要执行的函数并设定一个延迟时间。并且用闭包的形式将里面的被定时器包裹的函数返回。这是因为我们触发事件需要的是这个内部函数,而不是外面的防抖函数。
-
设置定时器,并将执行函数放进去。为了达到防抖效果,需要在执行函数前消除定时器,这样在用户每次执行事件时,定时器就会重新计时。
-
考虑定时器内部this 指向问题,需要在外部用一个变量保存 this,在定时器内部用 call 绑定。
-
考虑执行函数的参数传入问题。使用剩余参数在 call 绑定 this 指向时一并传入。
1.3 第一次执行
以上就是基本的防抖函数了,但是它有一个问题,那就是在用户第一次执行事件时,它就会起到一个“防抖”的效果,这在一些场合是我们不希望看见的。于是,我们需要考虑一下如何让函数在第一次事件中能够立刻执行。
function debouceImme(func, delay) {
let timer = null;
return function (...args) {
let context = this;
const callNow = !timer;
timer && clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
}, delay);
callNow && func.call(context, ...args);
};
}
分析
- 使用了一个
callNow限制了函数的执行,只有当 timer 被清除,callNow才为真,函数才执行。 - 而定时器的作用缺变成了将自身清除,而不是限制函数的执行。这样与
callNow结合后,在计时器计时期间,callNow始终不为真,函数就无法执行,起到了防抖的作用 - 而第一次执行时,timer 被定义成 null,所以函数可以直接执行。
1.4 封装
将以上两种情况都考虑并封装起来:
function debounce(func, delay, immediate) {
let timer;
return function (...args) {
let context = this;
timer && clearTimeout(timer);
if (immediate) {
let callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
callNow && func.call(context, ...args);
} else {
timer = setTimeout(() => {
func.call(context, ...args);
}, delay);
}
};
}
2. 节流(throttle)
2.1 什么是节流
理解了防抖,拿什么是节流呢?还是用一个例子:
需求
用户使用手机验证码登录时,请求一次验证码之后要60s之后才能再次点击请求,考虑到一些需求,不能将按钮直接设置为disabled
同理,我们还是可以使用定时器来解决这个需求,即定时器只要在计时,这个函数就不会进入执行。不管用户在如何点击按钮也不会触发事件。这就像是游戏中技能的“冷却时间”,一个技能在释放完毕之后,要经过一段时间才能再次释放。
2.2 如何实现节流
直接上代码:
function throttle(func, delay){
let timer;
return function (...args) {
let context = this;
if (timer) return
timer = setTimeout(()=>{
func.call(context, ...args)
timer = null;
}, delay)
}
}
分析
- 相对于上面的防抖,节流就很好理解了。在设置完定时器之后,只要这个定时器还在计时,重复执行这个时间都会触发
if(timer) return直接返回。
2.3 第一次执行
同样,上面的节流函数在第一次执行事件就直接进入了计时。为了让第一次就能直接触发事件,我们需要使用新的思路来重新这个函数。
function throttleimme(func, delay) {
let pre = 0;
return function (...args) {
let now = new Date(),
if(now - pre > delay) {
func(...args)
pre = now
}
}
}
分析
这里直接将定时器替换成简单的时间减算。
- 首先第一次执行时,
now - pre肯定大于延迟的值,则必定能进入内部执行函数 - 执行完函数后,将刚刚设置的
now时间赋值给了pre,下次再点击时,又重新了设置now,新的now减去旧的now的到的时间就是上一次执行完时间后所过的时间,如果不够设置的间隔时间,是无法再次触发事件的。
2.4 封装
function throttle(func, delay, imme) {
if (imme) {
let pre = 0;
} else {
let timer;
}
return function (...args) {
if (imme) {
let now = new Date();
if (pre - now > delay) {
func(...args);
pre = now;
}
} else {
let context = this;
setTimeout(() => {
func.call(context, ...args);
}, delay);
}
};
}
注意
-
当使用防抖函数时,内部的执行函数不能够加
( ),否则函数会立刻执行 -
如果要传入参数,应当使用
debounce(func.bind(this, ...args), delay)