Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lodash源码阅读(3)-的debounce, throttle #3

Open
sevenCon opened this issue Jun 17, 2019 · 0 comments
Open

lodash源码阅读(3)-的debounce, throttle #3

sevenCon opened this issue Jun 17, 2019 · 0 comments

Comments

@sevenCon
Copy link
Owner

sevenCon commented Jun 17, 2019

防抖和节流的区别

  • 防抖, 指的是在一个频繁触发的事件后, 等待不再触发了, 再去执行某函数, 比如滚动刷洗, 触发多次scroll事件, 等待scroll事件不再触发了, 发送请求.
  • 节流, 指的是在一个频繁触发的事件中, 规定时间内只有一次有效.比如点击刷新按钮, 500ms内多次刷新, 只发送一个请求

这两者是有明显区别的.
debounce 即是防止跳动.
throttle则是掐死.
这2者的目的, 都是为了减少请求或者触发事件的次数.可以总结为, throttle 是减少频率
debounce 是汇总, 最后一次发送

简单实现

// 简单debounce
function debounce(fn, wait = 300){
    let timer, fnWrapper;

    function clear(){
        clearTimeout(timer);
    }

    fnWrapper = function (){
        if(timer) clear();

        timer = setTimeout(function(){
            fn();
        }, wait);
    }

    return fnWrapper;
}

如果在wait时间内有debounce过来, 则关闭定时器, 重启一个定时器, wait时间之后,执行fn

// 简单throttle
function throttle(fn, wait){
    let wrapperFn,          
        lastTime = 0;   

    wrapperFn = function(){
        if(Date.now() - lastTime < wait) return;
        lastTime = Date.now();
        return fn();
    }

    return wrapperFn;
}

距离上一次调用的时间间隔要大于wait

以上是基本理念, 和实际实现有区别

lodash 的实现

// lodash 源码实现的解读
function debounce(func, wait, options) {
    var lastArgs,
        lastThis,
        maxWait,
        result,
        timerId,
        lastCallTime,
        lastInvokeTime = 0,
        leading = false,
        maxing = false,
        trailing = true;

    if (typeof func != 'function') {
        throw new TypeError(FUNC_ERROR_TEXT);
    }
    wait = toNumber(wait) || 0;
    if (isObject(options)) {
        leading = !!options.leading;
        maxing = 'maxWait' in options;
        maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
        trailing = 'trailing' in options ? !!options.trailing : trailing;
    }

    /**
     * 执行函数
     * 执行完毕清空配置信息
     * 但保留和更新lastInvokeTime
     */
    function invokeFunc(time) {
        var args = lastArgs,
            thisArg = lastThis;

        lastArgs = lastThis = undefined;
        lastInvokeTime = time;
        result = func.apply(thisArg, args);
        return result;
    }

    /**
     * 第一次调用
     * 同时开启一个setTimeout, 在wait之后调用timerExpired
     * 如果在wait之间存在debounce的调用, 那么lastArgs, thisArg 就会记录最近的一次调用的信息
     * 在本次wait等待时间完毕了之后, 用lastArgs, thisArg等参数调用最近一次的func, 完毕之后,清空调用信息, 
     * 和cancel不同的是, 保留lastInvokeTime时间
     */
    function leadingEdge(time) {

        lastInvokeTime = time;

        // 在wait时间之后再执行一次timerExpired , 检查是否应该执行trailinFn, 防抖
        timerId = setTimeout(timerExpired, wait);

        return leading ? invokeFunc(time) : result;
    }

    /**
     * 过期剩余时间
     * 如果有maxing, 证明这是最大等待时间
     * 那么返回的就是 距离上次调用debounce和距离最大等待时间之间的最小值
     */
    function remainingWait(time) {
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime,
            timeWaiting = wait - timeSinceLastCall;

        return maxing
            ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
            : timeWaiting;
    }

    /**
     * 这是判断是否到执行函数的时间
     * 1. 第一次调用
     * 2. 距离上次调用debounce的时间 > wait 等待时间
     * 3. 系统时间回调, 导致timeSinceLastCall<0的情况
     * 4. 等待时间超过最大时长
     */
    function shouldInvoke(time) {
        var timeSinceLastCall = time - lastCallTime,      /* 调用debounce的时间 */
            timeSinceLastInvoke = time - lastInvokeTime;  /* 调用func的时间 */

        return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
            (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
    }

    /**
     * 判断下一轮执行func的时间, 用递归调用setTimeout的方式来实现
     * 如果没有到shouldInvoke=true的时候, 重新计算setTimeout的剩余等待时间
     * 到了showInvoke的时间, 则执行防抖的延迟执行
     */
    function timerExpired() {
        var time = now();
        if (shouldInvoke(time)) {
            return trailingEdge(time);
        }
        // 如果还没到下一轮func的调用的时间, 
        // 那么重新开始定时器, 时间是距离maxWait和距离下一次执行func的最小值
        timerId = setTimeout(timerExpired, remainingWait(time));
    }

    // 这个是防抖的延迟执行, 也就是需要经过wait, 或者最大的等待时间 maxWait的时间之后,才会触发
    function trailingEdge(time) {
        timerId = undefined;

        // Only invoke if we have `lastArgs` which means `func` has been
        // debounced at least once.
        // 用lastArgs来记住最近的一次debounce的时间和时间参数, 在wait时间之后再进行一次防抖.
        if (trailing && lastArgs) {
            return invokeFunc(time);
        }
        lastArgs = lastThis = undefined;
        return result;
    }

    // 手动取消, 会重置所有的配置, cancel之后, 再起调用debounce相等于首次调用debounce, 会触发leadingEdge
    function cancel() {
        if (timerId !== undefined) {
            clearTimeout(timerId);
        }
        lastInvokeTime = 0;
        lastArgs = lastCallTime = lastThis = timerId = undefined;
    }
    
    // 手动调用
    function flush() {
        return timerId === undefined ? result : trailingEdge(now());
    }

    function debounced() {
        var time = now(),
            isInvoking = shouldInvoke(time);  // 判断可以被调用

        lastArgs = arguments;                 // 调用的参数
        lastThis = this;                      // 调用的this
        lastCallTime = time;                  // 调用debounce的时间, 需要区分lastInvokeTime, 这个fn的执行时间

        if (isInvoking) {
            /**
             * 第一次调用, 或者手动flush之后的一次调用, 或者在执行了trailinEdge 成功invokeFunc之后的调用
             */
            if (timerId === undefined) {
                return leadingEdge(lastCallTime);
            }

            /**
             * shouldInvoke=true, 但是存在timerId, 比如 系统时间回调的情况下, 或者maxWait比wait时间短的情况下,之间调用invokeFunc, 同时更新timerId
             */
            if (maxing) {
                // Handle invocations in a tight loop.
                timerId = setTimeout(timerExpired, wait);
                return invokeFunc(lastCallTime);
            }
        }

        /**
         * 这一步是为了处理, 在手动flush的情况下, timerId会被重置, 但是又没有到shouldInvoke当前情况下,
         * 重新开始一次防抖时间轮询, 期间如果有debounce被调用, 但是还是shouldInvoke=false, 那么timerExpired
         * 的trailinEdge就有可能被调用.
         */
        if (timerId === undefined) {
            timerId = setTimeout(timerExpired, wait);
        }
        return result;
    }
    debounced.cancel = cancel;
    debounced.flush = flush;
    return debounced;
}

小结主要流程

  1. 第一次执行, leading:true,马上调用一次func函数, 同时开始一个递归的setTimeout, 时间是wait
  2. 到wait时间, 执行setTimeout的回调函数, 如果在wait期间, 有调用debounce, 那么会更新lastCalltime, 和lastArgs, lastThis, 检查调用最近一次debounce的时间和当前时间相差是否大于wait时间.
  3. 如果大于, 则调用func.
  4. 否则计算上次debounce和wait的时间差, 用这个时间差重新开启一个setTimeout, 在这个时间差之后,在检查是否应该执行func

流程图

image

注: 本人的github地址为: lodash源码阅读(3)-的debounce, throttle, 如有版权问题或其他问题, 请求给作者留言, 感谢!

@sevenCon sevenCon changed the title lodash 的debounce, throttle源码解读 lodash源码阅读(3)-的debounce, throttle Jul 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant