如何实现JS中的防抖和节流函数?

92 阅读3分钟

防抖(debounce)和节流(throttle)是网页开发中两种比较常见的控制函数调用频率的方法。防抖比较适合搜索框搜索的场景;节流比较适合控制按钮点击频率的场景。

下面我们将来分别实现这两个函数。

实现防抖函数

防抖函数的实现分 4 步。

首先,防抖函数本质上是对原函数的封装,因此我们二话不说先返回一个函数,我们叫它 debouncedFn

function debounce(fn, delay = 220) {
    return function debouncedFn() {
        // TODO
    }
}
  • fn 是要做防抖处理的函数
  • delay 表示调用延迟

我们先实现防抖函数的首次触发——触发后,等待 delayms 后调用 fn

function debounce(fn, delay = 220) {
    return function debouncedFn(...args) {
        setTimeout(() => {
            fn(...args)
        }, delay)
    }
}
  • 我们将用户调用的参数 args 传递给原始函数 fn

不过现在的实现有个缺点,就是调用 fn 时,会丢失上下文环境 this。这个问题可以通过 fn.apply() 方法解决:

function debounce(fn, delay = 220) {
    return function debouncedFn(...args) {
        setTimeout(() => {
            fn.apply(this, args)
        }, delay)
    }
}
  • 调用 fn 时,我们将当前上下文环境 this 作为函数 fn 的上下文,并传入参数 args 进行调用

解决了首次的触发调用实现后,再解决后续的触发调用问题。

先要明白原理:在首次触发后的 delayms 内,如果再次触发,就要重新等待 delayms 才会调用函数 fn——也就是说会清除前一次的定时器,重新启动一个定时器。

根据这个思路,我们需要一个变量保存上一次的定时器 ID,我们暂且叫它 timer

function debounce(fn, delay = 220) {
    let timer = null

    return function debouncedFn(...args) {
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, args)
        }, delay)
    }
}

这样,在每次触发 debounceFndelayms 内,如果再次触发,就会清空上一次的定时器,重新启动一个定时器,过了 delayms 后调用原始函数 fn

当然,这种写法同样适应于 delayms 后 fn 被调用后,再次触发 debounceFn 的场景——fn 被调用后,再次触发时,clearTimeout(timer) 实际上不起任何作用(因为定时器回调已被执行),随后就是新创建了一个定时器,效果等同于调用 fndelayms 内的再次触发。

到此为止,debounce 函数就实现完毕了。

实现节流函数

节流函数的实现分 5 步。

首先,节流函数本质上是对原函数的封装,因此我们二话不说先返回一个函数,我们叫它 throttledFn

function throttle(fn, delay = 220) {
    return function throttleFn() {
        // TODO
    }
}
  • fn 是要做节流处理的函数
  • delay 表示节流区间(一个区间内无论触发多少次,只唯一调用一次 fn

我们先实现节流函数的首次触发——触发后,无需等待,直接调用 fn

function throttle(fn, delay = 220) {
    return function throttleFn(...args) {
        fn.apply(this, args)
    }
}

调用时,为了保留函数上下文,使用了 fn.apply() 方法调用 fn

当然,调用 fn 的同时,我们会同步启动一个定时器,是做什么用的呢?不卖关子了,其实就是标记当前是否处在一个节流区间

function throttle(fn, delay = 220) {
    return function throttleFn(...args) {
        fn.apply(this, args)
        setTimeout(() => {
            // TODO
        }, delay)
    }
}

我们引入一个变量 timer 用于存储定时器 ID,这个定时器 ID 会在 delayms 后,被清除(设置为 null)。由此,我们就可以用这个 timer,标记 fn 被调用后的处在的那个间隔区间。

function throttle(fn, delay = 220) {
    let timer = null

    return function throttleFn(...args) {
        fn.apply(this, args)
        timer = setTimeout(() => {
            timer = null
        }, delay)
    }
}

在这个区间范围内,fn 只能被调用一次,这可以通过判断 timer 的有无来实现。

function throttle(fn, delay = 220) {
    let timer = null

    return function throttleFn(...args) {
        if (!timer) {
            fn.apply(this, args)
            timer = setTimeout(() => {
                timer = null
            }, delay)
        }
    }
}

如果 timer 值是 null!timertrue),就表示当前区间还没有调用函数 fn;如果 timer 有值,就表示当前区间内已经调用过一次 fn 了,要忽略后续触发。

到此为止,debounce 函数就实现完毕了。