JS中的debounce与throttle

简介

debouncethrottle,用中文描述的话,就是 去抖节流

它们有什么用:

针对一些 执行频率非常高 的交互或事件,做性能优化。

二者的概念,网上的说法很多,这里不描述了。我这里主要分析下他们的相同点和不同点,和在什么时候用它们。

debouncethrottle 的相同点:

都是利用函数延迟执行来实现效果,我们暂时可以理解成用 setTimeout

注:lodash 中为了优化性能,在没有传入 wait 参数的情况下,优先使用 requestAnimationFrame,如果浏览器不支持,再降级使用 setTimeout

debouncethrottle 的不同点:

debounce 有一个等待时长,如果在这个等待时长内,再次调用了函数,就取消上一个定时器,并新建一个定时器。所以 debounce 适用于 input, keyup, keydown 等事件, 亦或者 click 事件需要防止用户在某个时间范围内多次点击的时候,也可以用。注:在lodash 的实现中 中并没有取消新建定时器的做法,是用时间来判断的。

throttle 也有一个等待时长,每隔一段这个等待时长,函数必须执行一次。如果在这个等待时长内,当前延迟执行没有完成,它会忽略接下来调用该函数的请求,不会去取消上一个定时器。所以 throttle 适用于 scroll, mousemove 等事件。在lodash 的实现中,还有一个等待的最大时长,这个我们分析源码时再讨论。

resize 事件,使用 debouncethrottle 都行,看你的需求啦。

举个栗子,使用 lodash 处理 resize 事件时,在 wait 参数不是非常小的情况下:

debounce的话,会在用户停止改变浏览器窗口大小时触发,也就是只是在最后触发一次。

throttle的话,会在用户改变浏览器窗口大小的过程中,每隔一段时间触发一次。

其实在 lodash 的实现中: throttle 就是一个定义了最大等待时长的 debounce

接下来,我们先自己实现 简易版的 debouncethrottle,然后再分析lodash 源码中的对应方法

debounce

debounce 有个很重要的特性,就是在规定的等待时长内如果再次调用函数,会取消上一次函数执行。

所以我们这里可以用 clearTimeout 先清除定时器,再重新 setTimeout 建一个新的定时器。

它的原理其实就是在闭包内维护一个 定时器

function debounce(fn, wait) {
    let callback = fn;    
    let timerId = null;

    function debounced() {
        // 保存作用域
        let context = this;
        // 保存参数,例如 event 对象
        let args = arguments;        

        clearTimeout(timerId);        
        timerId = setTimeout(function() {            
            callback.apply(context, args);
        }, wait);
    }
    
    // 返回一个闭包
    return debounced;         
}

// test
let resizeFun = function(e) {
    console.log('resize');
};
window.addEventListener('resize', debounce(resizeFun, 500));
复制代码

throttle

throttle 相对于 debounce 的最大区别就是它不会取消上一次函数的执行。

所以我们可以基于 debounce 去调整一下。

function throttle(fn, wait) {
    let callback = fn;    
    let timerId = null;

    // 是否是第一次执行
    let firstInvoke = true;

    function throttled() {
        let context = this;
        let args = arguments;           

        // 如果是第一次触发,直接执行
        if (firstInvoke) {
            callback.apply(context, args);
            firstInvoke = false;
            return ;
        }

        // 如果定时器已存在,直接返回。        
        if (timerId) {
            return ;
        }

        timerId = setTimeout(function() {  
            // 注意这里 将 clearTimeout 放到 内部来执行了
            clearTimeout(timerId);
            timerId = null;

            callback.apply(context, args);
        }, wait);
    }

    // 返回一个闭包
    return throttled;
}

// test
let resizeFun = function(e) {
    console.log('resize');
};
window.addEventListener('resize', throttle(resizeFun, 500));
复制代码

分析 lodash 中的 debounce

function debounce(func, wait, options) {
    let lastArgs,   // 存储 func 函数执行时的参数, 执行 debounced 函数的时候,被赋值
        lastThis,     // 存储 func 函数执行时的作用域, 执行 debounced 函数的时候,被赋值
        maxWait,      // 最长等待时间
        result,       // 存储 func 函数的返回值
        timerId,      // 定时器 id
        lastCallTime  // 最近一次 执行 debounced 函数时的时间

    // 最近一次执行 func 时的时间戳
    // 正常情况下,lastCallTime 与 lastInvokeTime 是相差无几的。
    let lastInvokeTime = 0
    // 是否 在延迟开始前 调用函数 - 它的作用,类似我上面实现的 throttle 方法中的 firstInvoke
    let leading = false
    // options 是否 传入了 maxWait 
    let maxing = false
    // 是否 在延迟结束后 调用函数
    let trailing = true
    // 可以看到, debounce 函数,默认是 leading = false, trailing = true。也就意味着,默认在延迟结束后调用 func 函数

    // 如果 没有传入 wait, 且 wait 不等于 0, 且浏览器支持 requestAnimationFrame时
    // useRAF 会等于 true, 表示启用  requestAnimationFrame 
    const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')

    // 如果 没有传入 func, 直接抛出错误
    if (typeof func != 'function') {
        throw new TypeError('Expected a function')
    }

    // 设置 wait 的默认值为 0
    wait = +wait || 0

    // 初始化 options 选项的 默认参数
    if (isObject(options)) {
        leading = !!options.leading
        maxing = 'maxWait' in options
        // 处理 maxWait 参数,如果用户自己定义了 maxWait, 则和 wait 参数比较,取他们的最大值
        // 这里是为了防止,用户 传入的 wait 大于 maxWait 的情况
        maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
        trailing = 'trailing' in options ? !!options.trailing : trailing
    }

    // 封装执行函数, 用于立即执行 func
    function invokeFunc(time) {
        // 参数
        const args = lastArgs
        // 作用域
        const thisArg = lastThis
        // 重置
        lastArgs = lastThis = undefined
        // 记录 func 函数执行时的时间戳
        lastInvokeTime = time
        // 执行函数
        result = func.apply(thisArg, args)
        return result
    }

    // 开启定时器
    function startTimer(pendingFunc, wait) {
        if (useRAF) {
            return root.requestAnimationFrame(pendingFunc)
        }
        return setTimeout(pendingFunc, wait)
    }

    // 清除定时器
    function cancelTimer(id) {
        if (useRAF) {
            return root.cancelAnimationFrame(id)
        }
        clearTimeout(id)
    }

    // 在延迟开始前调用
    // 对 invokeFunc 的封装,返回 invokeFunc 函数的返回值
    function leadingEdge(time) {
        // 记录 函数被调用时 的时间戳
        lastInvokeTime = time
        // 开启一个定时器
        timerId = startTimer(timerExpired, wait)
        // 如果 leading 为 true,则表示需要在延迟开始前 先执行一次 func 函数
        return leading ? invokeFunc(time) : result
    }

    // 在延迟结束后调用
    // 对 invokeFunc 的封装,返回 invokeFunc 函数的返回值
    function trailingEdge(time) {
        timerId = undefined

        // 这里加了个 lastArgs 的判断,lastArgs 会在 debounced 函数执行时赋值
        if (trailing && lastArgs) {
            return invokeFunc(time)
        }

        // 重置 参数和 作用域
        lastArgs = lastThis = undefined
        return result
    }

    // 
    function remainingWait(time) {
        // 计算 time 与最近一次调用 debounced 函数的时间差
        const timeSinceLastCall = time - lastCallTime
        // 计算 time 与最近一次调用 func 函数的时间差
        const timeSinceLastInvoke = time - lastInvokeTime
        // 用 wait 减去已经等待的时间
        const timeWaiting = wait - timeSinceLastCall

        return maxing
            // 如果设置了最大等待时长,
            // 则需要 比较 ( wait 减去已经等待的时间 ) 和 ( maxWait 减去已经等待的时间 ),取最小值
            ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
            // 否则直接返回 wait 减去已经等待的时间
            : timeWaiting
    }

    // 根据时间判断是否可以执行函数
    function shouldInvoke(time) {
        // 计算 time 与最近一次调用 debounced 函数的时间差
        const timeSinceLastCall = time - lastCallTime
        // 计算 time 与最近一次调用 func 函数的时间差
        const timeSinceLastInvoke = time - lastInvokeTime

        return (
            // 判断是不是第一次执行 debouned 函数,如果是第一次执行,肯定可以调用 func 函数
            lastCallTime === undefined
            // 判断距离最近一次调用 debounced 函数的时间差,是否大于等于 wait,如果是的话,也就意味着可以调用 func 函数
            || (timeSinceLastCall >= wait)
            // 正常情况 timeSinceLastCall 不会小于 0, 除非手动调整了系统时间
            || (timeSinceLastCall < 0)
            // 如果设置了 maxWait,判断距离上一次调用 func 函数的时间差,是否超过了最大等待时长
            || (maxing && timeSinceLastInvoke >= maxWait))
    }

    // 封装执行函数,用于 wait 延迟结束后执行
    function timerExpired() {
        const time = Date.now()
        // 根据时间来判断是否可以执行 func 函数
        if (shouldInvoke(time)) {
            return trailingEdge(time)
        }
        // 重新计算时间,重新建一个定时器
        timerId = startTimer(timerExpired, remainingWait(time))
    }

    function debounced(...args) {
        const time = Date.now()
        const isInvoking = shouldInvoke(time)

        lastArgs = args
        lastThis = this
        lastCallTime = time

        // 如果可以执行 func。第一次执行的时候,isInvoking 肯定是 true
        if (isInvoking) {
            // 第一次执行时
            if (timerId === undefined) {
                return leadingEdge(lastCallTime)
            }
            // 如果可以执行 func,且又有 timerId 。说明 func 可以执行了,可是又没有执行 trailingEdge
            // 如果又已经设置了 maxWait,就立即执行 func
            // 什么时候会出现这种情况, 我还不明白。
            if (maxing) {
                // 开启一个定时器
                timerId = startTimer(timerExpired, wait)
                // 立即执行 func 函数
                // 为什么设置 maxWait 就需要理解执行 func ,下面分析 throttle 的时候就明白了
                return invokeFunc(lastCallTime)
            }
        }

        // 什么情况下 不是第一次执行, 却又没有 timerId 呢?
        // 因为 trailingEdge 函数内部会执行 timerId = undefined
        // 如果刚好 trailingEdge 函数执行之后,又触发了 debounced ,就会出现这种情况
        if (timerId === undefined) {
            timerId = startTimer(timerExpired, wait)
        }

        return result
    }

    // 返回一个闭包
    return debounced
}
复制代码

注意:

lastCallTime 存储的是 debounced 函数执行时的时间戳。

lastInvokeTime 存储的是 func 函数执行时的时间戳。

lodashdebounced 还提供了3个api,供外部调用

// 取消 debounce
function cancel() {
    if (timerId !== undefined) {
        cancelTimer(timerId)
    }
    lastInvokeTime = 0
    lastArgs = lastCallTime = lastThis = timerId = undefined
}

// 执行 func
function flush() {
    return timerId === undefined ? result : trailingEdge(Date.now())
}

// 判断是否正在等待中
function pending() {
    return timerId !== undefined
}

// 暴露出三个方法  
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
复制代码

分析 lodash 中的 throttle

throttle 其实就是设置了 leadingmaxWaitdebounce

function throttle(func, wait, options) {
    let leading = true
    let trailing = true

    if (typeof func != 'function') {
        throw new TypeError('Expected a function')
    }

    // 初始化 leading 和 trailing 的默认值
    if (isObject(options)) {
        leading = 'leading' in options ? !!options.leading : leading
        trailing = 'trailing' in options ? !!options.trailing : trailing
    }

    // 默认情况下,leading 为 true, trailing 为 true。
    // 表示 在延迟开始前,和延迟结束后,都需要调用 func
    // 还传入了一个 maxWait
    return debounce(func, wait, {
        'leading': leading,
        'maxWait': wait,
        'trailing': trailing
    })
}
复制代码

debounce 的源码,我没有完全看明白,有几个地方是我的猜测。比如 debounced 函数内的 if (maxing),不明白为什么要这样判断。

非常希望有明白的道友,告诉下我。

如果有错误的地方,还请指出。

谢谢阅读。

参考

lodash文档

“浅入浅出”函数防抖(debounce)与节流(throttle)

从lodash源码学习节流与防抖

聊聊lodash的debounce实现

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值