跟着underscore学防抖

150 阅读4分钟

防抖

防抖原理

一句话概括一下就是:将多个连续事件组合成一个执行,如果在小于delay时间内连续事件不停止,则不触发事件执行

应用

  • window的resize、scroll
  • input时停止输入
  • 鼠标事件mousemove等

示例

鼠标移动事件增加防抖演示 delay时间为1s

代码如下:

const debounce = function(fn, wait) {
    let timer = null
    return function() {
        const context = this
        const args = arguments
        if(timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(() => {
            fn.apply(context, args)
        }, wait)
    }
}
const countEl = document.getElementById('count')
let num = 1
const count = function() {
    countEl.innerHTML = num++
}

document.addEventListener('mousemove', debounce(count, 1000), false)

实现效果:

当鼠标一直移动不停时,函数不会被调用

debounce2.gif

返回值

有时候需要获取一个包装函数的返回值,那么,我们需要添加一个返回值result,代码如下:

const debounce = function(fn, wait) {
    let result
    let timer
    return function() {
        const context = this
        const args = arguments
        if(timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(() => {
            result = fn.apply(context, args)
        }, wait)
        return result
    }
}

细心的同学可能会发现。这里的返回值会是undefined,因为返回操作是同步操作,而赋值是异步操作。那么underscore是怎么解决的呢?事实上,使用underscoredebounce也是同样的结果,这个异步结果也是正常的。

立即执行

支持函数立即执行,不需要等到wait之后执行。

const debounce = function(fn, wait, immediate) {
    let result
    let timer

    return function() {
        const context = this
        const args = arguments
        if(!timer && immediate) {
            result = fn.apply(context, args)
        }
        if(timer) {
            clearTimeout(timer)
        }

        timer = setTimeout(() => {
            result = fn.apply(context, args)
        }, wait)

        return result
    }
}

如果直接加立即执行,则首次会在wait时间内执行两次,即wait前一次,wait后的定时器一次,效果见下图:

de.gif

underscore的表现是立即执行时,在wait时间内仅执行一次,现在我们改造一下,如果是立即执行,并且是第一次,那么清除定时器(事实上不是首次立即执行)。

const debounce = function(fn, wait, immediate) {
    let result
    let timer

    return function() {
        const context = this
        const args = arguments
        if(!timer && immediate) {
            result = fn.apply(context, args)
        }
        if(timer) {
            clearTimeout(timer)
        }

        timer = setTimeout(() => {
        // 需要立即执行时,清除定时器并改变immediate的值,保证首次立即执行
            if(immediate) {
                timer = null
                immediate = false
            } else {
                result = fn.apply(context, args)
            }
        }, wait)

        return result
    }
}

我以为的immediate是这样的,实际执行过程中,发现immediate改变的是每个wait时间内的执行时机由wait后 ——> 到wait前执行。那么,我们还需要再改造一下: 我们要实现的效果如图:

def.gif

  • 延时时间内,鼠标移动则立即执行函数,计数立即变化
  • 超过延时时间后,再次触发鼠标移动会再次立即执行函数
  • 延时时间内再次触发鼠标移动事件,函数不会执行
 const debounce = function(fn, wait, immediate) {
    let result
    let timer

     let debounceFn = function() {
       let context = this
       let args = arguments

        timer && clearTimeout(timer)
        if(immediate) { 
            // 定义函数是否立即执行 
            // 无定时器时 立即执行
            let isCall = !timer
            // 延时时间内不执行 赋值timer
            timer = setTimeout(function() {
                timer = null // 延时时间过后清除定时器 可以立即执行
            }, wait)
            if(isCall) {
                result = fn.apply(context, args)
            }

        } else {
            timer = setTimeout(function() {
                result = fn.apply(context, args)
            }, wait)
        }
        return result
    }

    return debounceFn
}

观察underscore的代码,可以知道: 可以根据用户的停顿时间,也就是函数执行时间和触发事件的时间差(也就是下面代码中的passed)来控制函数的执行时机。

代码实现如下:

const debounce = function(fn, wait, immediate) {
    let result
    let timer
    let context
    let args
    let previous // 存储每次事件触发的时机(此例为鼠标移动)

    // 延时执行的函数 全局存储定时器
    const later = function() {
    // 计算定时器执行的时间与事件触发的时机的时间差
        const passed = Date.now() - previous
        // 时间差小于延时时 重新设置定时器 延后执行
        if(passed < wait) {
                timer = setTimeout(later, wait-passed) 
            } else { 
            // 时间差超过延时时间 清除定时器 
            // 非立即执行时执行函数
                timer = null
                if(!immediate)result = fn.apply(context, args)
            }
    }
    return function() {
        context = this
        args = arguments
        previous = Date.now()
        if(!timer) {
            timer = setTimeout(later, wait)
            if(immediate) result = fn.apply(context, args)
        }

        return result
    }
}

cancel定时器

增加cancel方法:

const debounce = function(fn, wait, immediate) {
...

debounceFn.cancel = function() {
    clearTimeout(timer)
    timer = null
}
return debounceFn
}

调用cancel之后,延时执行的函数被清除,不会执行。仅对当次有效。

如果immediatetrue,那么执行cancel方法后,则可以直接触发立即执行,不需要等待wait时间后触发立即执行。 效果如图: 延时时间为5s,点击清除定时器按钮,移动鼠标则可以立即执行函数

de-cancel.gif

至此,underscore的debounce函数就完整实现了!😄

总结

防抖是我们开放中常用到的功能,为了更好地理解其原理和实现,最好自己实践一遍。任何学习都是这样,纸上得来终觉浅,绝知此事要躬行!

参考文章: juejin.cn/post/684490…