如何写防抖函数
防抖的指的是一定时间内,重复的触发某事件,只让最后一次生效。常见的使用场景就是如果用户手抖(一般手抖就是短时间内的多次点击)连续点击了某个按钮,重复发出请求,做了防抖处理后,只会让最后一次的点击,避免了不必要的请求~
思考过程
知道了什么是防抖,如何实现防抖呢?
点击事件绑定的函数 fn,多次要执行这个函数 fn 的时候,咱们只让最后一次生效。可是怎么让前面触发的 fn 不执行呢?用 return ?假设短时间内的时间长度为 t,那么 t 时间内触发 fn 都返回,大概是这个样子
// btn.onclick = fn 本来的写法
btn.onclick = function () {
// ...emmmm 不知道咋写
// if(t) return // t 是个时间长度
// fn()
}
新手一般都会这么想吧,但是发现代码写不出来......:flushed:
咋办呢,再来捋一下思路。需求是短时间内连续点击的情况下,只让最后一次生效。
我们假设短时间内是 3s 内 ,点击一次后,就是说 3s 内再点击都不会 fn 都不会生效,那什么时候生效?是不是只要这中间不点击,3s 后 fn 就会生效。
一个东西 3s 后生效,仔细想想怎样做让一个东西一定时间后生效,当然就是 setTimeout 啦~
let t = 3000
btn.onclick = setTimeout(fn, t)
但是这样不就变成了每次点击都是t时间后执行了,而且每次点击都是延时执行,并没有实现需求啊。别着急,继续想想。
继续前面假设的情况,如果 3s 内又点击了一下该做什么?取消这次的事件。设定了 setTimeout,如何取消 setTimeout?自然是 clearTimeout!3s内触发的都给我取消掉!
let t = 3000
let timer = null
btn.onclick = function () { // 3s 内点击都不会不停的触发事件绑定的函数,这里暂时给不给函数名字都行
clearTimeout(timer) // 每次一进来上一次的就被取消了...
timer = setTimeout(fn,t)
}
这样就实现防抖的功能啦!
封装
但是但是,观察代码就会发现t、timer这些变量和 btn 点击时间绑定的函数好像不是一家人......和这个函数有关的变量t,timer 都写在了函数外面,和函数分开了,也不好下次调用啊,难道每次实现个防抖,都要定义几个变量?想想下次怎么再使用这个方法才方便呢,这就是封装,把这次想好的套路封装一下。
首先个这个方法,也就是函数起个名字debounce,语义化嘛,或者你也可以叫你喜欢的名字。
涉及到这个方法的变量有哪些呢,短时间内的时间t(是个时间段),真正要执行的方法(函数)fn。这两个也就是要传给debounce的参数。下面来看看怎么封装。
function debounce(fn, t) { // 这样是不对的,重点就是怎么把 timer 也放在函数里面
let timer = null
clearTimeout(timer)
timer = setTimeout(fn, t)
}
// 使用
let t = 3000
function fn() {
console.log('哈哈哈')
}
// 想想该怎么写使用呢
// 写法一
btn.onclick = debounce(fn, t) // 这样是把 debounce 执行后的结果给 click 事件,发现 fn 执行一次后就不在执行
// 写法二
btn.onclick = function () { // 这样运行一下试试
debounce(fn, t)
}
使用写法二会发现每次触发debounce,都执行了let= timer ,每次生成了一个 timer,然后每次都clearTimeout一下,再setTimeout一下,结果就是每次的点击都被延时......
怎么让 timer在函数内部只生成一次,也就是对于cleatTimerout和setTimerout使用的都是同一个timer,闭包可以解决这个问题(想想闭包的定义: 可以访问到其他函数作用域的函数)
function debounce(fn, t) {
let timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(fn, t)
}
}
// 使用
let t = 3000
function fn() {
console.log('哈哈哈')
}
btn.onclick = debounce(fn, t)
// debounce 执行后的结果是个匿名函数,也就是 click 实际绑定的函数,
// 每次点击,都会触发匿名函数的执行,匿名函数始终有使用 timer
// 也就是利用了闭包的特性读取函数内部的变量,同时这些变量再内存中不会被垃圾回收
// 如果写成这样就错啦
btn.onclick = function () {
debounce(fn, t) // 每次点击都执行一次 debounce,结果都是生成一个匿名函数!并没有用!
}
这样一个基础的防抖函数就写好了!
防抖函数如何传参
现在我们知道了一个基础的防抖函数式这样的
function debounce(fn, t) {
let timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(fn, t)
}
}
// 使用
let t = 500
function fn() {
console.log('哈哈哈')
}
btn.onclick = debounce(fn, t)
如果fn还要传参数呢,比如点击一个按钮打印的数值+1,同时对这个按钮做了防抖处理
let t = 3000
let count = 0
function fn() {
count++
console.log(`做了防抖处理!,现在的 count 是: ${count}`)
}
btn.onclick = debounce(fn, t)
思考过程
一样的 count和fn应该是一家人,fn的作用就是让 count 加一,从语义化的角度考虑,现在改下函数名叫addOne对于传给 addOne的变量,addOne要做的事就是让count加一
function addOne(count) {
count++
console.log(`做了防抖处理!,现在的 count 是: ${count}`)
}
// 使用
let myCount = 0 // 这里为了和形参区别一下,换了个名字
btn.onclick = debounce(() => {
addOne(myCount)
}, t)
// 下面这样写不对!!!因为加了括号就是让函数执行了一遍!这样 debounce 的以第一个参数就是 undefined,因为 addOne 没有 return,默认返回 undefined
btn.onclick = debounce(addOne(myCount), t)
// 这样写也不对!!!注意第一个参数,箭头函数这样只是定义了一个函数,传的是形参......并不是把外面定义的 myCount 传进去
btn.onclick = debounce((myCount) => {
addOne(myCount)
}, t)
按上面正确的写法你会发现一样没有生效,来看看目前的debounce长啥样
function debounce(fn, t) {
let timer = null
return function () {
clearTimeout(timer)
// 整个函数只有这里用了 fn,但是 fn 连个括号都没有,怎么接受参数??
// 但是如果 fn 直接写成 fn(),和前面一样的道理就是把执行的结果作为 setTimeout 的第一参数了
// 人家 setTimeout 第一个参数也要求是函数!
timer = setTimeout(fn, t)
}
}
-
如何接受参数呢,
js有个神奇的arguments,即使函数内部没有处理形参,如果调用函数的时候依然传了参数,依然可以通过arguments这个伪数组获取到传进来的参数。 -
另外
debounce的作用执行后返回好一个函数,这个函数处理timer,目的是连续的调用debounce的时候,不断清掉timer重写timer,真正要执行的函数依然是fn,因此一定是fn([这里要传参数])这种形式 -
一定是
优化封装
所以修改如下:
function debounce(fn, t) {
let timer = null
return function () { // debounce 执行后返回的函数没有传参数
// console.log(arguments) 一样可以打印看看 arguments
clearTimeout(timer)
timer = setTimeout(() => { // 即使传参也不能直接也成 fn(...啥啥啥)
// 对于 apply, 我是这么记的,把 fn 临时的放在 this 对象上并执行
fn.apply(this, arguments)
// 对于 this 指向谁,我是这么记的,哪个对象调用这个方法,方法里面的 this 就指向谁
}, t)
}
}
这些就可以愉快的的使用啦~
总结
// 防抖函数
function debounce(fn, t) {
let timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments)
}, t)
}
}
// 使用1 不传参的时候
function fn() {
console.log('防抖成功')
}
btn.onclick = debounce(fn, 1000) // 连续点击按钮,只会打印最后一次
// 使用2 传参的时候
let myCount = 0
function addOne(count) {
count++
}
// 方式1
btn.onclick = debounce(() => {
addOne(myCount)
}, 500)
// 方式2 // 哈哈哈可以思考下为啥,但是这里如果 addOne 要处理 btn 这个 DOM 对象,会有问题
const debounceAddOne = debounce(addOne, 500)
btn.onclick = function () {
debounceAddOne(myCount)
}
要写好一个防抖,明白防抖的原理,正确使用防抖函数。真的需要扎实的基础知识,回顾涉及的概念有闭包、apply、arguments、函数调用,函数的执行,函数的传参......
以上只是实现一个简单的防抖函数的思路,还可以控制更多,比如让第一次的点击生效之后再做防抖,或者给防抖加个开关,需要防抖的时候才调用(项目中需要全局使用的时候,更灵活的功能)等等。
如有错误,欢迎指正~~