防抖
防抖是js优化的重要的一部分,也是面试中手写代码最常考的题目。那么我们为什么要防抖?防抖是什么意思?
比如我们在监听onkeyup
事件中,监听input
中输入的文字,我们可以在console
中可以看到input
的文字打印。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input id="input1" type="text">
</body>
<script src="index.js"></script>
</html>
const input1 = document.getElementById('input1');
// console.log(input1);
input1.addEventListener('keyup', function() {
console.log(input1.value);
})
非常简单。但是每打一个字就监听到了,就很耗性能。
有人就问了,诶?那我能不能等我打完一小部分字段了以后,然后浏览器才去监听呢?
又有人说了,用
setTimeout
不就完了,过个一秒再监听。所以,防抖这个词就来了。
所以我们在刚才的index.js中的代码中,input1
的监听中,加入一个定时器试试:
const input1 = document.getElementById('input1');
// console.log(input1);
let timer = null;
input1.addEventListener('keyup', function() {
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
console.log(input1.value);
// 清空定时器
timer = null;
}, 500)
})
先演示一下:
可以看到,当我直接依次输入123456的时候,停顿了一会,然后浏览器才监听到我输入了,并打印123456,接着我又输入了789停止,半秒后,又打印了123456789,很妙。就是说,你一直输一直输,只要你中间输入的间断不超过500毫秒(咱们自己定的时间),它就不会出来,就一直等着你输,什么时候你输入的间断,暂停的或者停止的时间超过500毫秒,它才会把最终的结果打印出来。
用很通俗的话说一下这里面的原理:
①先弄个定时器,监听到keyup
的时候看一下现在定时器存在吗?不存在,是null
,那我们不管,往下走。
②接下来给定时器赋值setTimeout
的异步任务,这个异步任务是500毫秒之后执行,执行的结果就是打印当前的input
中的value
。打印完之后,要清空定时器。
流程是酱的:
①输入个1,停止:
timer
为null
,if
语句不触发,往下进行,500毫秒后输出value
,然后清空timer
,这是第一次,而且只输入一个1的情况下,比较简单。
②输入123,停止:
timer
为null
,if
语句不触发,往下进行,500毫秒之后执行,timer
有值了,这个时候,我们立马就输入2了,keyup
又监听了,这时候500毫秒还没有到,监听之后,timer
是有值的,if
条件判断为true,因为之前输入1的时候,timer
就有值了,所以我们把之前输入1的那个timer
给清除掉,然后下面重新设置timer
,500毫秒之后执行,但是还没开始执行的时候,又输入3了,立马又会出现keyup
事件,进入if
,有timer
,这个timer
是输入2的时候定义的,然后又清空了,重新设置了定时任务,500毫秒只有执行,这时候已经没有值进来了,所以500毫秒到了以后,输出刚才的123。接下来如果再输入一个4,是和第一次输入一个1是一样的。
写的比较啰嗦,但是这就是传说中的防抖。
但是有个问题,就是我们为了一个input1
,就写了这么多行代码,感觉好麻烦啊,如果我们又10个input
,我们是不是得写10个这样的监听函数?
所以新的问题就来了,我们需要改进一下我们的防抖函数,将它简单的封装一下:
我们需要将防抖函数封装成一个公共方法,那么这个方法的参数应该有个函数fn
,和一个delay
时间延迟,然后最终要返回一个函数。
代码如下:
// 防抖
function debounce(fn, delay = 500) {
// timer 是在闭包中的
let timer = null;
return function() {
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, arguments);
// 清空定时器
timer = null;
}, delay)
}
}
input1.addEventListener('keyup', debounce(function() {
console.log(input1.value);
}), 600);
节流
但我们拖拽一个元素时,要随时拿到该元素被拖拽的位置,直接用drag
事件,则会频发触发,很容易导致卡顿。节流的意思是:无论拖拽的速度有多快,都会每隔100ms触发一次。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#div1 {
border: 1px solid #ccc;
width: 200px;
height: 100px;
}
</style>
</head>
<body>
<div id="div1" draggable="true">可拖拽</div>
</body>
<script src="index.js"></script>
</html>
const div1 = document.getElementById('div1');
// console.log(div1);
div1.addEventListener('drag', function(e) {
console.log(e.offsetX, e.offsetY);
})
可以看到,
drag
基本上随时都在触发,这时候我们如果再调用别的js的功能,很容易造成负载验证,卡死。所以我们和防抖一样,加个
setTimeout
:
const div1 = document.getElementById('div1');
// console.log(div1);
let timer = null;
div1.addEventListener('drag', function(e) {
if(timer) {
return;
}
timer = setTimeout(() => {
console.log(e.offsetX, e.offsetY);
// 清空定时器
timer = null;
}, 100)
})
只不过这次如果不存在timer
,我们直接return
出去,就不管了,自生自灭,不响应了。知道100毫秒之后,打印了,并且timer
被赋值为null
,然后才会有timer
的下一次setTimeout
...具体的流程就不说了,和防抖是一样的。
同样,我们需要将节流函数封装一下,工具化:
// 节流
function throttle(fn, delay = 100) {
let timer = null;
return function() {
if(timer) {
return;
}
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay)
}
}
div1.addEventListener('drag', throttle(function(e) {
console.log(e.offsetX, e.offsetY);
}), 200)