热乎的防抖与节流 🧢

◎ 你知道吗?

在日常开发中,我们有时要监听mousemove或者keyup或者scroll等触发频率非常高的事件,对于这种类型的事件,我们往往不希望事件触发时的回调函数每次都会被调用,因为这常常会带来很多问题,比如回调函数中发送一个请求,请求只要一次就好了,发送多了增加服务器压力,页面也会有明显的卡顿,所以我们需要解决方案。

第一个解决方案就是利用防抖 (debounce) 函数,防抖函数的原理也很好理解:

当事件被连续触发时,回调函数不会被调用,只有当事件超过一段时间内没有被触发过,回调函数才会被调用一次。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function debounce(fn, delayTime) {
	var timeout;
	return function() {
		var context = this;
		var args = arguments;
		clearTimeout(timeout);
		timeout = setTimeout(function() {
			callback.apply(context, args);
		}, delayTime);
	}

function callback() { console.log('invoked'); }
var handleClick = debounce(callback, 2000);
elDiv.onmousemove = handleClick;

通过代码我们可以看出来,如果你在delayTime的时间间隔内再次触发事件,在第二次触发事件的回调函数内,clearTimeout清除了第一次的回调函数内设置的延时器(setTimeout),所以如果我们一直连续快速触发事件,那么回调函数永远不会被调用!除非你不触发事件一段时间,那么回调函数才会被调用一次。再讲一下代码细节吧,setTimeout方法调用的回调函数内的this指向window,显然不是我们想要的,所以我们通过context变量来保存this,然后通过callback.apply将this绑定到context变量上;事件的回调函数的第一个参数是Event对象,所以我们将arguments对象(一个对象,保存着我们的实参)赋值给args变量,然后传给apply方法的第二个参数。

但有时我们希望事件触发时,回调函数可以立即被调用,之后如果还连续触发事件,回调函数依旧不会被执行,直到一段时间内事件没有被触发,回调函数才会被执行一次:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function debounce(fn, delayTime) {
	var timeout;
	return function() {
		var context = this;
		var args = arguments;
		clearTimeout(timeout);
        if (!timeout) {
			callback.apply(context, args);
        }
        timeout = setTimeout(function() {
            timeout = null;
		}, delayTime);

	}

function callback() { console.log('invoked'); }
var handleClick = debounce(callback, 2000);
elDiv.onmousemove = handleClick;

第二个解决方案是节流(throttle),节流与防抖稍微有一点区别,节流意味着在规定的时间内,函数只会被调用一次,不论你在这段时间内,触发事件一次或者是一万次!我们的防抖却不一样,如果你一直不停地触发事件,回调函数永远不会被调用,在节流中,如果你一直不停地触发时间,我们会有规律的一段时间就调用一次回调函数。show your code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function throttle(cb, delayTime) {
    let timeout = null;

    return function() {
        let args = arguments;
        let context = this;
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null;
                cb.apply(context, args);
            }, delayTime);
        }
    }
}

我们观察上面代码会发现,当第一次函数被调用时,timeout被赋值为setTimeout的返回值,而在之后的delayTime间隔内,由于有if (!timeout)的限制,所以条件内的语句不会被调用,直到setTimeout的回调函数被调用,将timeout置为null,之后才可以继续被调用。当然了,上面的代码也不会在第一次的时候就调用回调函数,我们来写一个立即调用的版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function throttle(cb, delayTime) {
    let timeout = null;

    return function() {
        let args = arguments;
        let context = this;
        if (!timeout) {
            cb.apply(context, args);
            timeout = setTimeout(() => {
                timeout = null;
            }, delayTime);
        }
    }
}

如果用户在input框内键入搜索内容时,我们可以使用防抖方法,因为用户连续的输入我们认为他还在输入,所以不发请求,等到他停下以后,输入结束,我们发送请求。当用户改变窗口的大小时,我们也可以使用防抖方法,等到用户停下改变窗口的动作,我们才执行相应的自适应回调函数。 而当我们用户不停地滚动页面时,我们可能需要判断页面是否到底,这时候可以使用节流方法,我们可以每N秒就检查一次是否到底,这样可以节约性能。使用场景真的太多了,建议大家能够深刻体会这两者区别,然后因地施材。如果真的搞不清的话,就瞎🐔8用一个就可以啦,问题不大。

updatedupdated2020-07-102020-07-10