本文github地址:JavaScript_Everything 大前端知识体系与面试宝典,从前端到后端,全栈工程师,成为六边形战士
防抖函数的目的
防止无意义的网络请求,减轻服务器压力
防抖函数的使用场景
用户输入、页面缩放
<input type="text" class="">
<script>
const Input = document.querySelector('input')
Input.oninput = debounce(function(){
console.log('aa')
},2000)
</script>
我们希望不会是用户不停的输入,然后就不停的触发事件。
防抖函数的执行过程
-
事件触发时,相应的函数不会立即触发,而是会等待一段时间
-
当事件频繁的被触发时,函数的执行会被一直向后延迟,有新事件触发时前面的事件会被取消
-
等待一定时间之后没有继续触发事件,执行真正的响应函数
参数和返回值的考虑
参数考虑:需要两个参数,第一个参数为要进行防抖处理的回调函数,第二个参数为延迟时间。
function debounce(callback, time)
返回值考虑:需要返回一个新的函数,该函数是对传入的函数进行包装
return _debounce
function debounce(callback, time){
const _debounce = function(){
callback
}
return _debounce
}
防抖函数的基本实现
- 首先我们是要对函数延迟执行,那么就需要一个定时器,传入的参数要在这个定时器内执行,并且延迟的时间由传入的第二个参数决定
function debounce(callback, time){
const _debounce = function(){
setTimeout(function(){
callback()
},time)
}
return _debounce
}
- 上面的代码相当于对每一个函数都进行了延迟操作,但我们想要做到的是如果在计时器没有结束计时的过程中,如果重新触发了该事件,那么就需要将该事件取消。如何取消呢?由于事件回调函数是被封装在定时器内的,所以我们只需要将定时器清除即可。
function debounce(callback, time){
let timer = null
const _debounce = function(){
if(timer) clearTimeout(timer)
timer = setTimeout(function(){
callback()
timer = null
},time)
}
return _debounce
}
-
一开始执行时,timer为null
-
当进行第一个定时器时,timer赋值为此定时器
-
当触发第二次时,就需要将之前的定时器清除
思考:为什么timer要定义在定时器之后🤔?
因为我们需要形成一个闭包,让内部的定时器函数可以引用外面的变量,并且该变量不会在函数执行后被销毁。因为外面的函数被onclick回调函数引用着,不会被销毁,所以它的作用域不会被销毁,作用域内的私有变量也不会被销毁。
<script>
const Input = document.querySelector('input')
Input.oninput = debounce(function(){
console.log('aa')
},2000)
</script>
优化1 - this绑定修正
如果我们希望打印出用户输入的值:
Input.oninput = debounce(function(){
console.log(this) // Window
},2000)
会发现此时的this指向的全局对象window,这是为什么呢?我们来分析一下代码:我们直接返回了一个函数_debounce,该函数被绑定到Input元素对象,此时_debounce内部的this是指向该元素对象的。这一点我们可以先看下面👇的例子:
直接绑定给元素事件对象的this是会指向该元素的:
Input.oninput = function(){
console.log(this) // <input type="text" class="">
}
但_debounce函数内部的callback()是直接调用的,那么在js的机制里它会将函数内的this默认绑定为全局对象。因此我们需要修正this,让它绑定Input元素,只需要让它绑定为外层函数的this即可。
这也是我们绑定的目标,既然直接绑定给元素事件对象的this会绑定到该元素对象,那我们就用call或者apply去调用函数,并将this绑定给函数,保持函数不是直接调用,默认绑定到window就可以了。
但是要注意一个问题,也是我们可以利用的一个特性,那就是箭头函数是没有绑定this的,它会像上层作用域找到this,而相对于的普通函数有this。因此外层_debounce函数我们用普通函数声明,而内部的函数我们用箭头函数声明,这样用apply绑定this时它就会自动到外层找this.
function debounce(callback, time){
let timer = null
const _debounce = function(){
if(timer) clearTimeout(timer)
timer = setTimeout(()=>{
callback.apply(this)
timer = null
},time)
}
return _debounce
}
优化2 - 参数的问题处理
绑定元素的回调函数会被自动传入参数,比如event对象,而我们目前并没有接受这些参数,但这些参数是很有必要的。那么我们应该如何接受参数呢?我们又在哪里接收呢?
- 首先我们要明确绑定到元素的函数是哪一个?是返回的
_debounce函数,那么自动传入的event参数就会传给这个函数。 - 最终这些参数是要给
callback去使用的,因为在使用apply绑定this的时候我们就可以传入这些参数。 - 参数的个数可能是不确定的,因此我们可以使用剩余参数
...args来接收,而apply也是接收一个数组作为额外参数的。
因此,代码可以被优化为:
function debounce(callback, time){
let timer = null
const _debounce = function(...args){
if(timer) clearTimeout(timer)
timer = setTimeout(()=>{
callback.apply(this,args)
timer = null
},time)
}
return _debounce
}
来看一下效果:
const Input = document.querySelector('input')
Input.oninput = debounce(function(event){
console.log(event)
},2000) // InputEvent {isTrusted: true, data: 'd', isComposing: false, inputType: 'insertText', dataTransfer: null, …}
我们成功获取到了传入的event对象。
优化3 - 取消延迟
功能说明:
如果在延迟时间内,用户进行了其他操作,比如退出当前页面,返回上一级页面,取消执行等,我们当前的延迟回调函数就已经不需要执行了,但是在js的机制中,尽管用户退出了当前页面,但是该延迟回调函数还是会在等待时间结束后继续触发。
功能实现:
-
我们使用的是定时器,因为我们之需要在取消执行的时候取消定时器即可。而如果要用户去取消定时器,则需要调用我们给出的一个取消定时函数的方法,该方法在内部取消定时器。
-
上面的实现中我们返回给用户的是一个
_debounce函数,但函数也是对象,因此如果我们想在该函数上添加其他的功能,只需要给它添加一个方法即可。
function debounce(callback, time){
let timer = null
const _debounce = function(...args){
if(timer) clearTimeout(timer)
timer = setTimeout(()=>{
callback.apply(this,args)
timer = null
},time)
}
// 添加取消功能
_debounce.cancle = function(){
console.log('已取消延时执行函数')
if(timer) timer = null
}
return _debounce
}
我们在调用时候,就要稍微做一些调整。为了方便测试,我们也可以添加一个按钮执行取消操作。
<button>取消</button>
<input type="text" class="">
获取到元素
const Input = document.querySelector('input')
const Button = document.querySelector('button')
调用函数
const debounceFn = debounce(function(event){
console.log(event)
},2000)
Input.oninput = debounceFn
Button.onclick = function(){
debounceFn.cancle() // 点击按钮,取消执行
}
执行结果可以运行: JavaScript_Interview_Everything仓库下的notes/手写代码/HTML/防抖取消功能.html
优化 4 - 立即执行功能
我们可以增加一个功能,即每第一回频繁触发时,让第一次的操作函数立即执行,后面的频繁触发再执行防抖操作。但是中间停顿但进行第二回频繁触发时,第一次的操作函数仍然立即执行。
马上想到的就是我们可以增加一个函数参数,来判断是否第一次触发立即执行。假设默认为第一次自动执行。
function debounce(callback, time, immediate = true){...}
在返回的函数_debounce中我们可以进行判断:
- 第一次立即执行后马上改变
immediate为false,使后续该次定时器内的其他连续操作不再立即执行 - 本次定时器结束后,下一次定时器开始前我们要将
immediate改为true。因为下一次定时器开始,又是新的一轮执行,因此它的第一次也是要立即执行。 - 相应的在取消本地定时执行后,也要将
immediate改为true。
const _debounce = function(...args){
if(timer) clearTimeout(timer)
if(immediate) {
callback.apply(this, args)
immediate = false
return
}
timer = setTimeout(()=>{
callback.apply(this,args)
timer = null
immediate = true
},time)
_debounce.cancle = function(){
console.log('已取消延时执行函数')
if(timer) timer = null
immediate = true
}
}
但是对传入的参数状态进行改变并不是一个好的方式,因此我们新增一个参数进行控制,但是核心逻辑不变:
const _debounce = function(...args){
if(timer) clearTimeout(timer)
if(immediate && !isImmediate) {
callback.apply(this, args)
isImmediate = true
return
}
timer = setTimeout(()=>{
callback.apply(this,args)
timer = null
isImmediate = false
},time)
}
_debounce.cancle = function(){
console.log('已取消延时执行函数')
if(timer) timer = null
isImmediate = false
}
执行结果可以运行: JavaScript_Interview_Everything仓库下的notes/手写代码/HTML/防抖立即执行功能.html
优化 5 - 获取回调函数的返回值
如果我们想给回调函数传递参数并取到它的执行结果呢?就像下面这样,我们debounce传递了两个参数,并且想要在后面的延时函数执行时取到这里的执行结果,该如何做呢?
const debounceFn = debounce(function(name,age){
return `${name} is ${age} years old`
},2000)
在上面的函数中,传入的回调函数我们是这样执行的:
callback.apply(this, args)
如何我们直接获取它的值时不可行的,因为它是一个延时执行的函数,是异步的。对于异步我们自然想到了Promise,我们可以让_debounce返回一个 Promise,然后调用时得到 Promise 然后通过then取到执行结果。
const _debounce = function(...args){
return new Promise((resolve, reject)=>{
try {
if(timer) clearTimeout(timer)
let res;
if(immediate && !isImmediate) {
res = callback.apply(this, args)
isImmediate = true
resolve(res)
}
timer = setTimeout(()=>{
res = callback.apply(this,args)
resolve(res)
timer = null
isImmediate = false
},time)
} catch (error) {
throw new Error(error)
}
})
}
通过Promise获得结果:
const debounceFn = debounce(function(name,age){
return `${name} is ${age} years old`
},2000)
debounceFn('flten',20).then(res=>{
console.log('This is the result: ',res) // This is the result: flten is 20 years old
})
执行结果可以运行: JavaScript_Interview_Everything仓库下的notes/手写代码/HTML/防抖取到回调函数返回结果.html.
本文github地址:JavaScript_Everything 大前端知识体系与面试宝典,从前端到后端,全栈工程师,成为六边形战士