对节流和防抖函数的理解

2,034 阅读3分钟

概念

节流函数

间隔固定的时间执行传入的方法

目的是防止函数执行的频率过快,影响性能.常见于跟滚动,鼠标移动事件绑定的功能.

防抖函数对于接触过硬件的人也许更好理解,硬件按钮按下时,由于用户按住时间的长短不一,会多次触发电流的波动,加一个防抖函数就会只触发一次,防止了无意义的电流波动引起的问题.

按键防反跳(Debounce)为什么要去抖动呢?机械按键在按下时,并非按下就接触的很好,尤其是有簧片的机械开关,会在接触的瞬间反复的开合多次,直到开关状态完全改变。

应用在前端时,常见的场景是,输入框打字动作结束一段时间后再去触发查询/搜索/校验,而不是每打一个字都要去触发,造成无意义的ajax查询等,或者与调整窗口大小绑定的函数,其实只需要在最后窗口大小固定之后再去执行动作.

自己的实现

防抖函数

关键点在于每次触发时都清空延时函数的手柄,只有最后一次触发不会清空手柄,所以最后一次触发会等默认的1s后去执行debounce传入的参数函数f. debounce内部返回的闭包函数,是真正每次被调用触发的函数,不再是原本的f,所以这里的arguments取闭包函数环境变量中的arguments并在执行f时传给f,在setTimeout函数的外面取得.

let debounce = function(f, interval = 1000) {
        let handler = null;
        return function() {
            if (handler) {
                clearTimeout(handler);
            }
            let arg = arguments;
            handler = setTimeout(function() {
                //使用appy一方面是为了传入arg参数
                //一方面是为了改变this的指向
                //否则,执行函数指向是window而不是input dom对象了.
                f.apply(this, arg);
                clearTimeout(handler);
            }, interval)
        }
    }

应用:

 let input = document.querySelector('#input');
    input.addEventListener('input', debounce(function(e) {
        console.log("您的输入是",e.target.value)
    }))

重新写一遍

function debounce(fn){
		let timer = null;
		return function(){
			clearTimeout(timer);
			let arg = arguments;
			timer = setTimeout(()=>{
				// fn.apply(this,arg);
				fn()
			},1000);
		}
	}

	function sayHi() {
		console.log("test this 指向",this);
    }

    var inp = document.getElementById('inp');
    inp.addEventListener('input', debounce(sayHi)); // 防抖

此时控制台随着输入打印的结果是:

1 Arguments [InputEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
test.html:12 
1 Arguments [InputEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
test.html:12 
1 Arguments [InputEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
test.html:12 
1 Arguments [InputEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
test.html:16 
2 Arguments [InputEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ] Arguments [InputEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
test.html:24
thissss Window {window: Window, self: Window, document: document, name: "", location: Location, …}alert: ƒ alert()atob: ƒ atob()blur: ƒ blur()btoa: ƒ btoa()caches: ...

如果改成fn.apply(this,arg);,则输出为:

test this 指向 <input id=​"inp">​

更高级的实现还会考虑到,以leading和trailing作为参数,起始先执行一次函数并消除后面的抖动,还是最后执行一下函数,消除前面的抖动,如同我这里的例子.后面分析loadash的防抖函数时会详细解析.

节流函数

let throttle = function(f,gap = 300){
            let lastCall = 0;
            return function(){
                let now = Date.now();
                let ellapsed = now - lastCall;
                if(ellapsed < gap){
                    return
                }
                f.apply(this,arguments);
                lastCall = Date.now();
            }
        }

闭包函数在不断被调用的期间,去记录离上一次调用间隔的时间,如果间隔时间小于节流设置的时间则直接返回,不去执行真正被包裹的函数f.只有间隔时间大于了节流函数设置的时间gap,才调用f,并更新调用时间.

应用:

  document.addEventListener('scroll', throttle(function (e) {
        // 判断是否滚动到底部的逻辑
        console.log(e,document.documentElement.scrollTop);
  }));

同样的,节流函数也可以设置leading和trailling,保证开始的执行或者最后一次的执行.

lodash源码分析

以上是对节流防抖函数最基础简单的实现,我们接下来分析一下lodash库中节流防抖函数的分析.

节流函数的使用

$(window).on('scroll', _.debounce(doSomething, 200));
function debounce(func, wait, options) {
        var lastArgs,
            lastThis,
            result,
            timerId,
            lastCallTime = 0,
            lastInvokeTime = 0,
            leading = false,
            maxWait = false,
            trailing = true;

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

        function invokeFunc(time) {
            var args = lastArgs,
                thisArg = lastThis;

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

        function leadingEdge(time) {
            console.log("leadingEdge setTimeout")
            // Reset any `maxWait` timer.
            lastInvokeTime = time;
            // Start the timer for the trailing edge.
            timerId = setTimeout(timerExpired, wait);
            // Invoke the leading edge.
            return leading ? invokeFunc(time) : result;
        }

        function remainingWait(time) {
            var timeSinceLastCall = time - lastCallTime,
                timeSinceLastInvoke = time - lastInvokeTime,
                result = wait - timeSinceLastCall;
                console.log("remainingWait",result)
            return maxWait === false ? result : Math.min(result, maxWait - timeSinceLastInvoke);
        }

        function shouldInvoke(time) {
            console.log("shouldInvoke")
            var timeSinceLastCall = time - lastCallTime,
                timeSinceLastInvoke = time - lastInvokeTime;
            console.log("time",time,"lastCallTime",lastCallTime,"timeSinceLastCall",timeSinceLastCall)
            console.log("time",time,"lastInvokeTime",lastInvokeTime,"timeSinceLastInvoke",timeSinceLastInvoke)
            console.log("should?",(!lastCallTime || (timeSinceLastCall >= wait) ||
                (timeSinceLastCall < 0) || (maxWait !== false && timeSinceLastInvoke >= maxWait)))
            // Either this is the first call, activity has stopped and we're at the
            // trailing edge, the system time has gone backwards and we're treating
            // it as the trailing edge, or we've hit the `maxWait` limit.
            return (!lastCallTime || (timeSinceLastCall >= wait) ||
                (timeSinceLastCall < 0) || (maxWait !== false && timeSinceLastInvoke >= maxWait));
        }

        function timerExpired() {
            console.log("timerExpired")
            var time = Date.now();
            if (shouldInvoke(time)) {
                return trailingEdge(time);
            }
            console.log("Restart the timer.",time,remainingWait(time))
            // Restart the timer.
            console.log("timerExpired setTimeout")
            timerId = setTimeout(timerExpired, remainingWait(time));
        }

        function trailingEdge(time) {
            clearTimeout(timerId);
            timerId = undefined;

            // Only invoke if we have `lastArgs` which means `func` has been
            // debounced at least once.
            console.log("trailing",trailing,"lastArgs",lastArgs)
            if (trailing && lastArgs) {
                return invokeFunc(time);
            }
            lastArgs = lastThis = undefined;
            return result;
        }

        function cancel() {
            if (timerId !== undefined) {
                clearTimeout(timerId);
            }
            lastCallTime = lastInvokeTime = 0;
            lastArgs = lastThis = timerId = undefined;
        }

        function flush() {
            return timerId === undefined ? result : trailingEdge(Date.now());
        }

        function debounced() {
            var time = Date.now(),
                isInvoking = shouldInvoke(time);
            console.log("time",time);
            console.log("isInvoking",isInvoking);
            lastArgs = arguments;
            lastThis = this;
            lastCallTime = time;

            if (isInvoking) {
                if (timerId === undefined) {
                    return leadingEdge(lastCallTime);
                }
                // Handle invocations in a tight loop.
                clearTimeout(timerId);
                console.log("setTimeout")
                timerId = setTimeout(timerExpired, wait);
                return invokeFunc(lastCallTime);
            }
            return result;
        }
        debounced.cancel = cancel;
        debounced.flush = flush;
        return debounced;
    }

ref

1.Debouncing and Throttling Explained Through Examples

2.实例解析防抖动(Debouncing)和节流阀(Throttling)

3.lodash源码