前言
防抖和节流是前端性能优化的两大利器,核心思想都是限制高频行为。
它们的区别在于:
-
防抖,意在当外界不再变化时,再去做响应。
-
节流,意在不管外界如何变化,始终保持着自己的响应频率。
应用场景:
- 防抖:用户疯狂点击提交按钮,用户每次点击提交都会向服务器发出请求,如果不做限制那么会无意义地消耗服务器资源。因此,我们利用防抖的思想,做到当用户停止点击按钮后,再去放出请求。防抖演示图如下:
- 节流:当用户在更改浏览器窗体大小时,会触发
resize
事件。内置的resize
事件响应频率太高,不仅对用户体验没有提升,反而还消耗了大量主线程的资源,导致卡顿。因此我们更希望他能降低响应的频率,那么节流的思想就很好的对应了这个需求。节流演示图如下(这里用scroll
来做实验):
定时器版本
防抖和节流的定时器都只需要简单的设置和清除一下定时器,首先我们先写防抖。
防抖代码如下:
function debounce(fn, delay) {
let timer = null;
return function(...args) {
if(timer) clearTimeout(timer);
timer = setTimeout(function() {
fn(...args);
},delay)
}
}
在此基础上,节流仅需改动两行代码:
function throttle(fn, delay) {
let timer = null;
return function(...args) {
// 第一处改动
if(timer) return;
timer = setTimeout(function(){
// 第二处改动
timer = null;
fn(...args);
},delay)
}
}
补充说明:不得不提一嘴,上述代码如果fn
内部用到了this
,那么this
指向哪呢?在非严格模式下是全局对象(浏览器环境为window
)。在严格模式下则是undefined
。所以,如果fn
用到了this
,那么它的表现可能会不符合你的预期。为此,我们要么借助bind
、apply
或call
去显式绑定this
,要么用箭头函数
的形式去声明fn
,从而让this
的指向更符合我们的直觉。
用 rAF 去做节流
rAF
,即requestAnimationFrame
,因篇幅受限本文不再对该函数进行分析。大家可以参考昊神的文章。
在这里,我们简单地将rAF
看成一个间隔是 16.7ms 的定时器(当然它们不完全等价),我们把最小间隔时间固定为了约 16.7ms,据此来实现 N*16.7ms 的效果。相比于一般定时器而言,rAF
可是要“守时”的多。
这里只是出于研究的目的去拓宽思路,大家了解下即可,本人也没有去验证其可靠性
function throttle(fn,delay) {
// 估算一下最大计次数
let maxCount = delay/(1000/60),
count = 0;
return function(...args) {
requestAnimationFrame(function() {
count++;
if(count > maxCount) {
fn(...args);
count = 0;
}
});
}
}
补充说明:这段代码存在一个问题,它无法保证最后一次回调函数一定被执行。即若持续触发resize
3 秒,而节流要求的间隔时间为 2 秒,那么我们预期的效果是在 4 秒左右会响应两次,但上述代码只会响应一次,最后一次将被丢失。
下面的动图演示了这个 BUG。
用 rIC 去做防抖
rIC
,即requestIdleCallback
,大家同样可以参考昊神的文章。
rIC
的空闲回调可能执行的时机有两种,要么是每帧之间的间隔时间,要么是用户停止交互后可以匀出个约 50ms 的时间。我们做防抖可以利用后者。
从防抖的目标出发,当用户不再频繁做点击交互时,我们再去进行响应。那么如何能够获知用户不再进行操作呢?就是通过判断空闲回调函数的入参IdleDeadline.timeRemaining()
的值是否介于 16.7ms 到 50ms 之间,虽然它并不完全可靠。
注意:这里只是出于研究的目的去拓宽思路,大家了解下即可,本人也没有去验证其可靠性
function debounce(fn) {
let lock = false;
return function(...args) {
// 上锁防止重复调用
if (lock) return;
const run = function() {
requestIdleCallback(function(deadline) {
// 打印一下空闲时间
console.log(deadline.timeRemaining());
lock = true;
// 判断空闲时间
if (deadline.timeRemaining() > 1000 / 60) {
fn(...args);
// 解锁
lock = false;
} else {
// 递归
run();
}
});
}
run();
}
}
下面让我们来演示一下。
从演示图中可以看出,平均点击2,3次将触发一次响应,离我们真正业务要求还有一定的距离。
写在最后
本文给出了常用的实现防抖和节流的方法,并借助rAF
和rIC
做了简单实验。建议大家可以看看lodash
是怎么封装和实现的防抖与节流的。对于新手来说,也可以从防抖和节流的实现去体验闭包的妙用!
关于我
喜欢聊天、喜欢分享、喜欢前沿的22届小菜鸡,初来乍到希望得到各位大佬的关注。能有实习/校招机会就更好啦!
个人公众号:鼠子的前端CodeLife