前言
防抖与节流也是面试经常出现的。今天晚上对此做了一个复习。希望能够给你带来一些思考和启发。
1.防抖
什么是防抖
- 防抖:任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。
看到过一个很形象的例子,就是上电梯。如果只有你一个人上电梯进入电梯后过一会(加入时间是5秒)门自动关上电梯上行;如果有很多人上电梯电梯门不会在第一个人进去后,然后经过5s电梯门关上。而是等“很多人”都进入电梯后才会等待5s电梯上行。
拿我们浏览网页来讲,上网搜索某个信息的时候。比如我们要搜索:什么是节流与防抖,那么只有你一口气输入完才会调用相对的接口,或者输入部分达到间隔的时长调用相应接口。
防抖的例子
下面举个栗子加深印象,可以在复制过去自己实践一下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>防抖</title>
</head>
<body>
<button id="debounce">点我防抖!</button>
<script>
window.onload = function() {
// 1、获取这个按钮,并绑定事件
var myDebounce = document.getElementById("debounce");
myDebounce.addEventListener("click", debounce(sayDebounce));
}
// 2、防抖功能函数,接受传参
function debounce(fn) {
// 4、创建一个标记用来存放定时器的返回值
let timeout = null;
return function() {
// 5、每次当用户点击/输入的时候,把前一个定时器清除
clearTimeout(timeout);
// 6、然后创建一个新的 setTimeout,
// 这样就能保证点击按钮后的 interval 间隔内
// 如果用户还点击了的话,就不会执行 fn 函数
timeout = setTimeout(() => {
// fn.call(this, arguments);
fn();
}, 1000);
};
}
// 3、需要进行防抖的事件处理
function sayDebounce() {
// ... 有些需要防抖的工作,在这里执行
console.log("防抖成功!");
}
</script>
</body>
</html>
基本形式
对照注释好好理解
var processor = {
timeoutId: null, // 相当于延时setTimeout的一个标记,方便清除的时候使用
// 实际进行处理的方法
// 连续触发停止以后需要触发的代码
performProcessiong: function () {
// 实际执行的代码
// 这里实际就是需要在停止触发的时候执行的代码
},
// 初始处理调用的方法
// 在实际需要触发的代码外面包一层延时clearTimeout方法,以便控制连续触发带来的无用调用
process: function () {
clearTimeout(this.timeoutId); // 先清除之前的延时,并在下面重新开始计算时间
var that = this; // 我们需要保存作用域,因为下面的setTimeout的作用域是在window,调用不要我们需要执行的this.performProcessiong方法
this.timeoutId = setTimeout(function () { // 100毫秒以后执行performProcessiong方法
that.performProcessiong();
}, 100) // 如果还没有执行就又被触发,会根据上面的clearTimeout来清除并重新开始计算
}
};
// 尝试开始执行
processor.process(); // 需要重新绑定在一个触发条件里
节流
- 节流:指定时间间隔内只会执行一次任务。
先把下面的代码复制到编辑器看看效果。不停的点击kk有什么不同。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>节流</title>
</head>
<body>
<button id="throttle">点我节流!</button>
<script>
window.onload = function() {
// 1、获取按钮,绑定点击事件
var myThrottle = document.getElementById("throttle");
myThrottle.addEventListener("click", throttle(sayThrottle));
}
// 2、节流函数体
function throttle(fn) {
// 4、通过闭包保存一个标记
let canRun = true;
return function() {
// 5、在函数开头判断标志是否为 true,不为 true 则中断函数
if(!canRun) {
return;
}
// 6、将 canRun 设置为 false,防止执行之前再被执行
canRun = false;
// 7、定时器
setTimeout( () => {
fn.call(this, arguments);
// 8、执行完事件(比如调用完接口)之后,重新将这个标志设置为 true
canRun = true;
}, 1000);
};
}
// 3、需要节流的事件
function sayThrottle() {
console.log("节流成功!");
}
</script>
</body>
</html>
实现的方法1:设置时间戳
我们设置三个时间,上一次的时间pre,和当前时间,计算两者时间差。如果时间差大于我们设定的时间戳(第三个时间)就调用相应的方法,如果时间差小于的话就不执行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>地铁进站</title>
</head>
<body>
<button id="addBtn">进站人数+1</button><button id="resetBtn">重置</button>
<p id="personTotal">旅客总人数:0</p>
<p id="personNum">进站人数:0</p>
<script>
var personNum = 0; // 进站人数
var personTotal = 0; // 一共来了多少人
var addBtn = document.getElementById('addBtn'); // 获取添加人数按钮
var personNumP = document.getElementById('personNum'); // 获取显示人数的标签
var personTotalP = document.getElementById('personTotal'); // 获取显示总人数的标签
var resetBtn = document.getElementById('resetBtn'); // 获取重置按钮
/**
* @method 增加进站人数
* @description 每个时间间隔执行的方法
*/
function addPerson() {
personNum ++;
personNumP.innerHTML = `进站人数:${personNum}`;
}
/**
* @method 节流方法(时间戳)
* @param {Function} fn 需要节流的实际方法
* @param {Number} wait 需要控制的时间长度
* @description 根据上一次执行的时间,和这一次执行的时间做比较,如果大于控制的时间,就可以执行
*/
function throttle(fn, wait) {
var prev = 0; // 第一次执行的时候是0,所以第一次点击的时候肯定大于这个数,所以会立马执行
return function () {
var context = this;
var args = arguments;
var now = Date.now(); // 实际执行的时间
personTotal ++;
personTotalP.innerHTML = `旅客总人数:${personTotal}`;
if (now - prev >= wait) { // 执行的时间是不是比上次执行的时间大于需要延迟的时间,大于,我们就执行
fn.apply(context, args);
prev = now; // 执行了以后,重置上一次执行的时间为刚刚执行这次函数的时间,下次执行就用这个时间为基准
}
}
}
/**
* @method 重置
*/
function reset() {
personNum = 0;
personTotal = 0;
personNumP.innerHTML = '进站人数:0';
personTotalP.innerHTML = `旅客总人数:0`;
}
addBtn.addEventListener('click', throttle(addPerson, 1000));
resetBtn.addEventListener('click', reset);
</script>
</body>
</html>
方法2:利用setTimeout
利用setTimeout设置一个事件间隔,利用闭包设置一个标志位,当标志位是true就执行定时器里面的函数(执行前将标志位标记为false),定时器里面执行相应的函数执行完后将标志位设置true。这样就保证了在一定的时间间隔内只执行一次认为,可以结合我一开始贴上的代码思考。
注意使用定时器有一点不好就是第一次点击的时候还是需要等待一段时间才会执行定时器的函数,讲道理第一次是不需要等待的。
所以这两种方法用哪种取决业务需要
节流的作用
那么,节流在工作中的应用?
-
拖拽一个盒子移动,如果使用防抖的话需要一种拖动停止后n秒再执行,那样岂不是跳动了,所以节流比较合理,平均一段时间执行一次
-
懒加载要监听计算滚动条的位置,使用节流按一定时间的频率获取。
用户点击提交按钮,假设我们知道接口大致的返回时间的情况下,我们使用节流,只允许一定时间内点击一次。
这样,在某些特定的工作场景,我们就可以使用防抖与节流来减少不必要的损耗。(至于为什么可以看我的浏览器相关文章重绘和回流)
方法3:rAF
大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。因此,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms
而setTimeout和setInterval的问题是,它们都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行
requestAnimationFrame采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>rAF使用</title>
<style>
#SomeElementYouWantToAnimate {
width: 100px;
height: 100px;
background-color: #000;
}
</style>
</head>
<body>
<div id="SomeElementYouWantToAnimate"></div>
<script>
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
/**
* @method 移动我们的小黑方块
*/
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
</script>
</body>
</html>