防抖和节流
面试经常被问到防抖和节流的问题,其实很多概念都只是讲的很 “高大上”(个人愚见),以至于听起来就很懵。 今天就用大白话和大家聊聊「防抖和节流」到底是个啥。
为啥会出现这么两个概念?
首先,防抖和节流当然不是凭空出现的,它是来解决问题的。解决啥问题呢?
比如网页上有一个下载按钮,这个按钮是下载图片用的,点一下就可以把这张图直接下载到你的电脑里。那么无论你是成心之过还是无意之举,在一秒钟内你用你单身20年无与伦比的手速抱着把鼠标点坏的决心点了 500 次,然后你的电脑里就有了 500 张相同的图片。
怎么样,感觉还爽吗?23333……
这种情况显然不合理,我所说的不合理不是说你没有那么快的手速,而是说程序允许你在短时间内可以连续的触发同一个事件,这件事它不合理。因为且不说你不需要 500 张一模一样的图,短时间内这么做对程序和服务器的带宽都是一种压力。
于是乎,「防抖和节流」它就 lei 了……
它们就是来解决这个问题滴~(当然这种情况很多,不光是点击按钮。)
什么是防抖
防抖,英文:debounce。
非立即执行版
触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
也就是说当一次事件发生后,事件处理器要等一定时间,如果这段时间过去后再也没有事件发生,就处理最后一次发生的事件。
啥意思?
假设你点了一个按钮(一次事件),我现在设定你点了按钮后必须过 1 秒才能生效(执行事件)。结果还差 0.01 秒就到达指定时间,也就是过了 0.99 秒,这时候你按捺不住自己内心的躁动与渴望又点了一次按钮(又发生了一次事件),那么恭喜你,之前的等待作废,需要重新再等待 1 秒(指定时间)。直到...直到你点了一次按钮之后 1 秒都没有动作,然后才会生效。
立即执行版
触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数。如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
啥意思?
emmm...这么聪明的你应该能举一反三了吧!
其实就是点了按钮会马上执行一次。然后...然后又和非立即执行版一个“德行”了...
你再点,他又得重新等 1 秒,你一直点,它就一直不执行,直到你点完之后一秒内不点了才会执行。
话不多说
非立即执行版:
function debounce1(fn, delay) {
//注意下面不要用箭头函数,注意this和arguments的指向
let timer = null; //通过闭包保存一个标记
return function () {
if (timer) {
clearTimeout(timer);
}
//若有timer,就清除timer,所以会重新执行下面的定时器,重新定时。
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
}
}
//对比两种写法
function debounce2(fn, delay) {
let timer = null;
//注意下面不要用箭头函数,注意this和arguments的指向
return function (...args) {
let context = this;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
}
}
立即执行版:
function debounce_(fn, delay, immediate) {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
immediate = true;
}, delay);
//以上代码每次点击都会执行。
//若定时器没有结束,则immediate为false,下面代码else永远不会执行。
//而return false之后若再重新执行此函数,又会清除掉定时器,再重新开一个timer,则会重新计算时间。
if (!immediate) {
return false;
} else {
fn.apply(this, arguments);
immediate = false;
}
}
}
合并版:
//最终合并完整版
function debounce(fn, delay, immediate) {
let timer = null;
let canRun = immediate;
return function () {
if (timer) {
clearTimeout(timer);
}
if (immediate) {
timer = setTimeout(() => {
canRun = true;
}, delay);
//用canRun代替了immediate
if (!canRun) {
return false;
} else {
fn.apply(this, arguments);
canRun = false;
}
} else {
timer = setTimeout(() => {
fn.apply(this, arguments);
},delay);
}
}
}
什么是节流
节流,英文:throttle。
节流就是类似控制阀门一样定期开放的函数,也就是让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活,喜欢玩 moba 类游戏的同学厉害了,这个东西就类似于技能冷却(技能 cd)。
大白话
实际上这个函数的作用就是如此,它可以将一个函数的调用频率限制在一定阈值内,例如 1 秒,那么 1 秒内这个函数一定不会被调用两次。
说白了就是一个按钮你一直不停的点点点点点,其实它一秒就能执行一次。换做讲防抖开始时候那个极不贴切的下载图片的例子就是,即便你十秒内点了一万次,其实你也就只能一秒下载一张图,一共十张而已。
上代码
两种思路
第一种思路:
判断当前时间和上一次执行时间的时间差,如果这个时间差大于阈值,才让其执行。
意思就是当你点击按钮的时候,我判断一下你现在是几点几分几秒点击的按钮,然后减去上一次你点击按钮的时间,看看这个时间差有没有到 1 秒钟。没有上次点击咋办?凉拌(白眼)!
function throttle_(fn, time) {
let pre = 0;
return function (...args) {
if (Date.now() - pre > time) {
fn.apply(this, args);
pre = Date.now();
}
}
}
第二种思路:
设置一个开关阀门(标记)。如果你点击了一次按钮,就把阀门关了,等 1 秒后再开阀门,也就是技能冷却的思路。
function throttle(fn, delay) {
let canRun = true,
timer = null; // 通过闭包保存标记
return function () {
// 在函数开头判断标记是否为true,不为true则return
if (!canRun) {
return false;
}
//定时器的作用:当delay秒过后,canRun才能变为true,代码才能执行到这里,才能继续往下执行
canRun = false; // 立即设置为false
clearTimeout(timer);
timer = setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
fn.apply(this, arguments);
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远false,在开头被return掉
canRun = true;
}, delay);
};
}
2023 简洁版
/**
* @name 防抖 适用于input输入框不断输入发送请求等场景
* @param fn 需要防抖的函数
* @param daley 需要延迟的时间(毫秒)
* @return 返回设置好延迟的防抖的函数
*/
function debounce(fn, delay) {
let timer = null;
return function () {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this, arguments);
timer = null;
}, delay);
};
}
/**
* @name 节流 适用于drag,scroll等场景
* @param fn 需要节流的函数
* @param daley 需要间隔的时间(毫秒)
* @return 返回设置好间隔的节流的函数
*/
function throttle(fn, delay) {
let timer = null;
return function () {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
};
}
写在最后
若文中有什么错误的地方,欢迎批评指导。若您有更好的方案或者写法,也欢迎留言。