前言:
这两天看面试题,又遇到了防抖和节流,之前也是似懂非懂,于是到处看如何实现防抖和节流,可是很可惜,大部分只有代码没有实际场景和注释,于是自己弄懂后记录下来。
防抖函数
- 定义:顾名思义,在频繁触发的前提下,让函数只在特定的时间内没有触发执行条件才执行一次代码。
- 使用场景:频繁操作点赞和取消点赞,因此需要获取最后一次操作结果并发送给服务器。
自己模拟了一个使用场景,即点击提交按钮会触发ajax请求,但是如果用户频繁点击提交按钮,难道就要触发n次吗,显然不可取。
- 那么如何做呢?先写下初始代码。通过 console.log(1) 去模拟ajax 请求的场景。现在只要点击按钮就会打印 1 ,n 次点击就打印 n 次 1 。
<input type="text">
<button id="submit">提交</button>
let btn=document.getElementById('submit')
btn.addEventListener('click',submit)
function submit(){
console.log(1)
}
- 有了场景,就要开始思考了,如何让 submit 函数防抖呢?首先给它包装一个 debounce 函数。但是这里有一个问题,在页面刷新时,会发现浏览器会自动打印 1 和 debounce。这是为什么呢?
btn.addEventListener('click',debounce(submit,1000))
function debounce(fn,delay){
fn()
console.log("debounce")
}
-
这是因为 addEventListener 中接收的第二个参数是立即执行函数,所以会导致 debounce 函数被执行。
-
那么就得想办法如何让 debounce 不执行。其实改写为回调函数即可。
function debounce(fn,delay){
return function(){
fn()
}
}
- 接着开始思考,如何在点击按钮时,让函数延迟执行呢?
function debounce(fn,delay){
return function(){
setTimeout(() => {
fn()
}, delay);
}
}
- 可是光延时哪儿行,该触发 n 次还是触发 n 次。就得想办法在第二次点击时让计时器重新计时。首先在回调函数外面定义一个 timer 用来接收计时器,由于 debounce 在页面加载时就会自动执行,所以 timer 初始值就是 null , 并且在后续点击按钮时,是直接触发的回调函数,不会去重新定义 timer 。
function debounce(fn,delay){
let timer=null
return function(){
if(timer){
clearTimeout(timer)
}
timer=setTimeout(() => {
fn()
}, delay);
}
}
-
大致上,已经完成了防抖的基本操作,也能理解了,但是会有几个小问题。
-
关于 submit 函数的 this 指向问题。可以发现这里的 this 实际是 window 对象,为什么呢?这是因为 submit 是在计时器中被调用的。可是计时器中的 this 为什么指向按钮呢,这是因为计时器是箭头函数,箭头函数是不具备 this 的,this 取决于外层的函数,这里外层函数为回调函数,而这个回调函数指向 btn 。那么 fn() 是直接调用,所以它的 this 指向 window 。
function submit(){
console.log(this);
}
function debounce(fn,delay){
let timer=null
return function(){
if(timer){
clearTimeout(timer)
}
timer=setTimeout(()=>{
console.log(this);
fn()
}, delay);
}
}
- 解决 submit 的 this 指向问题。这里只需要使用 apply 函数去改变 this 指向即可。
timer=setTimeout(()=>{
console.log(this);
fn.apply(this)
}, delay);
- 关于事件对象的获取。如果改写为如下就可以获取到 e 。
function submit(){
console.log(e);
}
function debounce(fn,delay){
return function(e){
console.log(e);
}
}
- 按照防抖函数的写法。debounce 可以获取到 e ,而 submit 是获取不到的。所以要思考如何传递参数。
function submit(){
console.log(e);
}
function debounce(fn,delay){
let timer=null
return function(e){
if(timer){
clearTimeout(timer)
}
timer=setTimeout(()=>{
console.log(e);
fn.apply(this)
}, delay);
}
}
- arguments 可以用来传递参数,在回调函数中,arguments 就是传递给函数的所有参数集合。
function submit(e){
console.log(e);
}
function debounce(fn,delay){
let timer=null
return function(){
if(timer){
clearTimeout(timer)
}
timer=setTimeout(()=>{
fn.apply(this,arguments)
}, delay);
}
}
具体 html 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖</title>
</head>
<body>
<input type="text">
<button id="submit">提交</button>
<script>
let btn=document.getElementById('submit')
btn.addEventListener('click',debounce(submit,1000))
function submit(e){
console.log(11111);
}
function debounce(fn,delay){
let timer=null
return function(){
if(timer){
clearTimeout(timer)
}
timer=setTimeout(()=>{
fn.apply(this,arguments)
}, delay);
}
}
</script>
</body>
</html>
节流函数
这一块真的弄懂了防抖,其实也能很快写出来
- 定义:频繁触发,但只在特定的时间内才执行一次代码
- 场景:一些频繁触发的事件,但是规定在一段时间内只触发一次。和防抖函数有点类似,但是节流函数的第一次是执行的,然后在某个时间内,不管事件被触发多少次都不执行。
直接上 html 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖</title>
</head>
<body>
<input type="text">
<button id="submit">提交</button>
<script>
let btn=document.getElementById('submit')
btn.addEventListener('click',throttle(submit,3000))
function submit(e){
console.log(e);
console.log(this);
console.log("执行");
}
function throttle(fn,delay){
// 首先定义一个起始时间startTime
let startTime=0
return function (){
// 定义一个当前的时间currentTime
let currentTime=new Date().getTime()
// 为了能让第一次事件就执行,就需要做一个判断,
// 由于startTime为0,而currentTime是一个大大大大...大的数,
// 所以肯定能够成功
if(currentTime-startTime>delay){
// 如果执行了,那么就调用fn
fn.apply(this,arguments)
// 同时,也要给startTime重新赋值,
// 现在startTime就是第一次事件发生的时间戳
// 等到下一次判断的时候,currentTime已经是第二次的时间戳了,
// 所以通过第n次减去第(n-1)次就能得出两次事件触发的时间间隔了
startTime=currentTime
}else {
console.log("正在节流,请稍等");
}
}
}
</script>
</body>
</html>