# 前言

对于防抖节流,相信大家都已经听了太多太多,网上在这一块的资料也有很多。在真正使用它之前我也曾按照网上的教程手写过几个不同的版本,最近在工作中真正要用上的时候,才发现之前对其的理解还不够深刻。所以今天在这里好好的梳理一下它们,贴上一个比较全的实现方式,另外介绍一下实际工作中是怎样使用的。

# why 防抖节流?

首先想要说明的是为什么需要使用到防抖节流,若是已经清楚的小伙可以直接跳过阅读后面的内容,若你和我一样在使用场景上还存在一些疑惑的话可以仔细看看这一块内容,我会尽量说的清晰一些。

场景一

有这么一个需求,用户在点击某个按钮的时候会触发一些任务事件。但若是他在很短的时间内连续的点击按钮,则会不停的触发任务事件,但我们想要这个任务事件在一定时间内只能触发一次(不管按钮被点击了几次),然后过了这个时间再次点击就又可以触发了。

如图:

此时,像这种频繁的触发任务事件,但却只想要它在一定时间内只执行一次,过了这个时间又能继续触发的情况我们就称之为防抖

但在实际工作中,我们发现上面避免按钮重复点击的情况一般都可以给按钮增加一个loading的效果来避免,用的更多的可能是下面的场景。

场景二

用户在输入框输入文字之后,需要将输入的文字发送给后台并获取到数据。我们知道这样的需求一般都是通过监听输入框内容的改变,然后将最新的内容传递给后台。但若是每输入一个字或者一个拼音就发送一个请求是否就会产生大量多余的请求,此时,使用防抖无疑是非常有必要的。

# 防抖

# 非立即执行防抖

相信大家看到上面的需求就知道,防抖的实现肯定离不开定时器了,所以我们先来实现一个简易版的防抖:

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 500) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  return function(...args) { // 接收额外的参数
    if (timer) clearTimeout(timer) // 如果已经设定过定时器了就清空上一次的定时器
    timer = setTimeout(() => { // 开始一个新的定时器,延迟执行用户传入的方法
      func.apply(this, args)
    }, wait)
  }
}

可以看到,上面的debounce函数利用定时器将要执行的函数func放到事件队列中,等过了wait时长后就会执行。若是在还没过wait时长就再次触发了就会清空掉上一个定时器(也就是取消了上次的func),从而重新计算func执行的时间。

了解了原理之后,我们就知道上面的函数并不会在一开始就执行,而是在过了wait时长之后才执行,像这种情况,我们称它为非立即执行防抖

案例1🌰

<body>
    <input id="text" onkeyup="changeInput(event)" />
    <p id="result"></p>
    <script>
        let text = document.querySelector('#text')
        let result = document.querySelector('#result')
        function changeInput (event) {
            // console.log(event.target.value)
            debouncePostValue(event.target.value)
        }
        function debounce (func, await = 500) {
            let timer = null;
            return function (...param) {
                if (timer) clearTimeout(timer)
                timer = setTimeout(() => {
                    func.apply(this, param)
                }, await)
            }
        }
        const debouncePostValue = debounce(postValue)
        function postValue (value) {
            result.innerHTML = value
        }
    </script>
</body>

# 立即执行防抖

立即执行防抖与上面的相似,只不过在第一次触发的时候会执行一次,然后要等待await时长之后才能再次触发。

function debounce (func, await = 500) {
    let timer = null;
    return function (...param) {
        if (timer) clearTimeout(timer)
        let callNow = !timer
        timer = setTimeout(() => { // 在await之后将定时器清除
            timer = null
        }, await)
        if (callNow) func.apply(this, param)
    }
}

案例2🌰

<body>
    <input id="text" onkeyup="changeInput(event)" />
    <p id="result"></p>
    <script>
        let text = document.querySelector('#text')
        let result = document.querySelector('#result')
        function changeInput (event) {
            // console.log(event.target.value)
            debouncePostValue(event.target.value)
        }
        function debounce (func, await = 500) {
            let timer = null;
            return function (...param) {
                if (timer) clearTimeout(timer)
                let callNow = !timer
                timer = setTimeout(() => { // 在await之后将定时器清除
                    timer = null
                }, await)
                if (callNow) func.apply(this, param)
            }
        }
        const debouncePostValue = debounce(postValue)
        function postValue (value) {
            result.innerHTML = value
        }
    </script>
</body>

# 终极防抖

若是将是否立即执行作为一个参数传入函数中,就可以合成最终的一个防抖函数。这里我用到的是yck大佬提供的一种防抖写法:

/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

  // 延迟执行函数
  const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的情况下,函数会在延迟函数中执行
    // 使用到之前缓存的参数和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
      timer = later()
      // 如果是立即执行,调用函数
      // 否则缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    // 这样做延迟函数会重新计时
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

下面有一个案例是演示了如何调用,并传递参数:

<body>
    <button onclick="clickBtn()">点击</button>
    <p id="result"></p>
    <script>
        let num = 0
        function clickBtn () {
            debouncePostValue('点击了')
        }
        /**
         * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
         *
         * @param  {function} func        回调函数
         * @param  {number}   wait        表示时间窗口的间隔
         * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
         * @return {function}             返回客户调用函数
         */
        function debounce(func, wait = 50, immediate = true) {
            let timer, context, args

            // 延迟执行函数
            const later = () => setTimeout(() => {
                // 延迟函数执行完毕,清空缓存的定时器序号
                timer = null
                // 延迟执行的情况下,函数会在延迟函数中执行
                // 使用到之前缓存的参数和上下文
                if (!immediate) {
                    func.apply(context, args)
                    context = args = null
                }
            }, wait)

            // 这里返回的函数是每次实际调用的函数
            return function (...params) {
                // 如果没有创建延迟执行函数(later),就创建一个
                if (!timer) {
                    timer = later()
                    // 如果是立即执行,调用函数
                    // 否则缓存参数和调用上下文
                    if (immediate) {
                        func.apply(this, params)
                    } else {
                        context = this
                        args = params
                    }
                    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
                    // 这样做延迟函数会重新计时
                } else {
                    clearTimeout(timer)
                    timer = later()
                }
            }
        }
        const debouncePostValue = debounce(postValue, 500, true)
        function postValue(laterParam) {
            result.innerHTML = `${laterParam}:${++num}`
        }
    </script>
</body>

原文地址:《yck前端面试之道-防抖》

# 节流

阅读全文