怎么写防抖函数

966 阅读7分钟

如何写防抖函数

防抖的指的是一定时间内,重复的触发某事件,只让最后一次生效。常见的使用场景就是如果用户手抖(一般手抖就是短时间内的多次点击)连续点击了某个按钮,重复发出请求,做了防抖处理后,只会让最后一次的点击,避免了不必要的请求~

思考过程

知道了什么是防抖,如何实现防抖呢?

点击事件绑定的函数 fn,多次要执行这个函数 fn 的时候,咱们只让最后一次生效。可是怎么让前面触发的 fn 不执行呢?用 return ?假设短时间内的时间长度为 t,那么 t 时间内触发 fn 都返回,大概是这个样子

// btn.onclick = fn 本来的写法

btn.onclick = function () {
    // ...emmmm 不知道咋写
    // if(t) return // t 是个时间长度
    // fn()
}

新手一般都会这么想吧,但是发现代码写不出来......:flushed:

咋办呢,再来捋一下思路。需求是短时间内连续点击的情况下,只让最后一次生效。

我们假设短时间内是 3s 内 ,点击一次后,就是说 3s 内再点击都不会 fn 都不会生效,那什么时候生效?是不是只要这中间不点击,3sfn 就会生效。

一个东西 3s 后生效,仔细想想怎样做让一个东西一定时间后生效,当然就是 setTimeout 啦~

let t = 3000
btn.onclick = setTimeout(fn, t)

但是这样不就变成了每次点击都是t时间后执行了,而且每次点击都是延时执行,并没有实现需求啊。别着急,继续想想。

继续前面假设的情况,如果 3s 内又点击了一下该做什么?取消这次的事件。设定了 setTimeout,如何取消 setTimeout?自然是 clearTimeout3s内触发的都给我取消掉!

let t = 3000
let timer = null

btn.onclick = function () { // 3s 内点击都不会不停的触发事件绑定的函数,这里暂时给不给函数名字都行
    clearTimeout(timer) // 每次一进来上一次的就被取消了...
    timer = setTimeout(fn,t)
}

这样就实现防抖的功能啦!

封装

但是但是,观察代码就会发现ttimer这些变量和 btn 点击时间绑定的函数好像不是一家人......和这个函数有关的变量ttimer 都写在了函数外面,和函数分开了,也不好下次调用啊,难道每次实现个防抖,都要定义几个变量?想想下次怎么再使用这个方法才方便呢,这就是封装,把这次想好的套路封装一下。

首先个这个方法,也就是函数起个名字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在函数内部只生成一次,也就是对于cleatTimeroutsetTimerout使用的都是同一个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)

思考过程

一样的 countfn应该是一家人,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) 
    }
}
  1. 如何接受参数呢,js有个神奇的arguments,即使函数内部没有处理形参,如果调用函数的时候依然传了参数,依然可以通过 arguments这个伪数组获取到传进来的参数。

  2. 另外 debounce 的作用执行后返回好一个函数,这个函数处理 timer,目的是连续的调用debounce的时候,不断清掉timer重写timer,真正要执行的函数依然是fn,因此一定是fn([这里要传参数])这种形式

  3. 一定是

优化封装

所以修改如下:

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、函数调用,函数的执行,函数的传参......

以上只是实现一个简单的防抖函数的思路,还可以控制更多,比如让第一次的点击生效之后再做防抖,或者给防抖加个开关,需要防抖的时候才调用(项目中需要全局使用的时候,更灵活的功能)等等。

如有错误,欢迎指正~~