前言
当我们在浏览网页信息,在搜索栏搜索我们需要的信息时,我们时常会出现卡顿的现象。作为网络攻城狮的我们肯定知道是因为发送的请求还在路上,或者后端还未解析完成。但很大一部分人的做法是--反复点击搜索键,于是糟糕的事情发生了,当你一直点击时你会一直给其后端服务器发送请求。那么问题来了,如果存在这么一个情况----数亿人在浏览这个网页时都出现网络出现卡顿的情况,他们都选择了不断点击重复发送请求,那么服务器将会可能出现瘫痪的情况。如何解决这个问题?这就是我们这篇文章学习的重点--防抖
- 注意:基础较弱小伙伴可以移步以下篇章
防抖
怎么解决用户一直点一直发送请求的方式呢?只要实现----当用户一直点击按钮时我们不发送请求,但用户冷静几秒后再点击按钮时可以发送请求。和时间相关肯定需要利用定时器来操作。
主要步骤
让我来梳理一下主要步骤:
- 先搓个按钮
- 当用户点击按钮时发送请求,所以需要监听鼠标的点击事件
- 设置计时器,当用户在点击按钮的1s之内再次点击按钮时计时器清零
实操步骤
- 手搓一个按钮先
<button id="btn">提交</button>
- 给这个按钮绑定点击事件
- 拿到btn的DOM结构,使得我们能对变量btn进行操作
- 给
btn
这个 DOM 元素添加一个点击事件监听器
let btn = document.getElementById('btn')
btn.addEventListener('click', function(){});
3.在监听器中设置计时器,记录用户点击后经过的时间
我们把监听器中形式功能的函数拿到外面来写,设置点击后1s发送请求
setTimeout(
function () {
console.log('提交')
},
1000
)
单纯的发送请求并不能达到我们的想要的结果,我们的目的无非就两个: 闭环当点击按钮时触发定时器
- 当在1s内点击按钮时毁灭这个定时器,重新创造一个定时器计时
闭包循环
所以,我们需要使用闭包来达成一个循环,将执行操作的函数放在我们打造的防抖函数里,形成闭包。
- 我们定义一个函数debounce来作为我们的防抖函数,当全局执行完成时他必须被调用
- 在一开始时设定timer来控制计时器的有无
- 在防抖函数的内部设置一个函数,这个函数需要返回出来,遵守addEventListener的语法
- 在这个返回函数中放置清除计时器
clearTimeout()
和计时器 setTimeout() - 当这个计时器生效时发送‘提交’信号
代码实现如下:
let btn = document.getElementById('btn');
btn.addEventListener('click', debounce());
//防抖函数
function debounce() {
let timer = null;
return function () {
//如果第二次的时间没到一秒,就销毁上一个定时器
clearTimeout(timer)
timer = setTimeout(
function () {
console.log('提交')
}
, 1000)
}
}
我们可以优化一下我们的代码:
let btn = document.getElementById('btn')
btn.addEventListener('click', debounce(handle));
function debounce(fn) {
let timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(fn, 1000)
}
}
function handle() {
console.log('提交')
}
这样的话,当鼠标第一次点击时,变量timer会变成一个计时器;当在1s时间内(计时器未工作完成时)再次点击按钮会再次执行防抖函数的内置函数,此时会把上一个未完成工作的计时器销毁,只有等上一个计时器工作完成后才会重新创造一个计时器,重新倒计时发射信号。
闭包工作原理如图:
服务器瘫痪的情况出现过多次,著名的新浪微博,b站都出现过这种情况。当服务器出现这种情况时,我们程序猿不得不从百忙之中又抽身跑回公司疯狂修改bug,防抖的实现真是程序猿的福音。但是,难道防抖的实现到这里就结束了吗?不不,我们实现了防抖的同时还带来了一点点小问题。
防抖中的this指向问题
我们就以这段代码为例
let btn = document.getElementById('btn')
btn.addEventListener('click', debounce(handle));
function debounce(fn) {
let timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(fn, 1000)
}
}
function handle() {
console.log('提交',this)
}
首先一个问题handle里面的this指向谁?显而易见,他指向了window,但我们在进行防抖操作之前,这里的this是指向btn的,我们改变了this的指向,就必须将他掰正。
还记的显示绑定的三个方法吗?call(),apply(),bind(),在这里我们只需要将handle里的this绑定到return的那个函数上就行,因为他的this指向是btn。
除此之外,我们可以将定时器内函数改为箭头函数,利用箭头函数没有this,箭头函数内的this是包含他的函数的this这一概念,将this掰到你需要绑定到的this上加以修正
let btn = document.getElementById('btn')
btn.addEventListener('click', debounce(handle));
function debounce(fn) {
let timer = null
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this)
}, 1000)
}
}
function handle() {
console.log('提交')
}
那计时器里面不是箭头函数该怎么解决呢?
timer = setTimeout(
function () {
}, 1000
)}
}
在这里我们就不能再将this绑定到外部函数的this了,因为自己含有this。但是我们可以定义外部函数的this再进行绑定。在这里我们可以将return函数的this重新赋值为that
return function () {
const that = this
clearTimeout(timer);
timer = setTimeout(
function () {
fn.apply(that)
}, 1000
)
}
修正事件参数
任何一个事件在被触发的时候都会有事件参数,这是引擎内置的,让我们更好的理解代码。在我们做防抖之前,handle函数里面有一个事件参数e他是指向click的
btn.addEventListener('click', function(){});
function handle(e) {
console.log('提交',e)
}
但是当我们防抖后改变了事件参数e,变成了undefined ,这是因为handle函数变成了debounce的内置,事件参数指向匿名函数。我们需要用call重新绑定
function debounce(fn) {
let timer = null;
return function(e) {
const that = this;
// 如果第二次的时间没到1s,就销毁上一次的定时器
clearTimeout(timer);
timer = setTimeout(function() {
fn.call(that, e);
}, 1000);
}
}
完美~
小结
如何手搓一个防抖?
- defounce 返回一个函数体,跟debounce形成闭包
- 子函数体中每次先销毁上一个定时器再创建一个新的setTimeout
- 还原 原函数的this指向
- 还原 原函数的参数
这篇文章理解吃力的小伙伴还请移步以下篇章:
理解以上篇章内容再次移步想必会有事半功倍的效果