防抖与节流(一)

下面我来叙述一个场景,你的用户们由于各种原因,使用鼠标在几秒钟不停的按了数十次,不停点击发起请求,身为前端开发,你不担心你的页面崩溃嘛?!!肯定担心!!!

由此, 就到了我们的性能优化环节——防抖。

  
  

    一、何为防抖?

    概念:任务频繁触发的情况下,只有任务触发的间隔超过制定的时间间隔的时候,任务才会被执行。

    防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐!

    除了mousedown,还有一些在前端开发中会遇到的频繁的事件触发也需要优化,比如:window.resizewindow.scrollmousemovekeyupkeydown……

    二、学会防抖!

    1. 首先,我们来举个示例代码,来了解事件如何频繁的触发

    //有问题的版本
    CSS:
    #container{width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;}
    HTML:
    <div id="container"></div>
    <script src="debounce.js"></script>
    JS:
    var count = 1;
    var container = document.getElementById('container');
    function getUserAction() {
    		container.innerHTML = count++;
    };
    container.onmousemove = getUserAction;
    
      
      

    我们来看看效果:
    效果图
    从左边滑到右边就触发了 165 次 getUserAction 函数!

    因为这个例子很简单,所以浏览器完全反应的过来,可是如果是复杂的回调函数或是 ajax 请求呢?假设 1 秒触发了 60 次,每个回调就必须在 1000 / 60 = 16.67ms 内完成,否则就会有卡顿出现。

    为了解决这个问题,一般有两种解决方案:

    • debounce 防抖
    • throttle 节流

    2. 回忆一下,防抖的原理触发高频函数事件后,n秒内函数只能执行一次,如果在n秒内这个事件再次被触发的话,那么会重新计算时间,要等你触发完事件 n 秒内不再触发事件,才执行。

    你肯定想,根据这个原理,写个防抖不是小case?然后唰唰唰一段类似下面的代码就敲出来了!

    //第一次更改的初级版本
    function debounce(func, wait) {
        var timeout;
        return function () {
            clearTimeout(timeout)
            timeout = setTimeout(func, wait);
        }
    }
    //以刚才的165例子,试试这个'防抖'
    container.onmousemove = debounce(getUserAction, 1000);
    //现在随你怎么移动,反正你移动完 1000ms 内不再触发,我才执行事件。
    
      
      

    我们再来看看效果:效果图
    顿时就从 165 次降低成了 1 次!!!(棒棒哒,我们接着研究它。)

    3. 有一个问题,不知阁下发现了没有,this指向问题来了!!!

    如果我们在 getUserAction 函数中 console.log(this),在不使用 debounce 函数的时候,this 的值为:<div id="container"></div>

    但是,注意:如果使用我们的 debounce 函数,this 就会指向 Window 对象!

    所以我们需要将 this 指向正确的对象,来吧,改代码咯~

    //第二次更改this指向的版本
    function debounce(func, wait) {
    	var timeout;
    	return function () {
    	var context = this;   
    	clearTimeout(timeout)
            	timeout = setTimeout(function(){
                	func.apply(context)
            	}, wait);
        	}
    	}
    

    4. this 指向问题已经解决,但是,我们怎么可以忘记对象呢?!!事件处理函数中会提供事件对象 event,对象event有没有问题呢 ?有啥问题?一起来看看吧!

    //我们修改下 getUserAction 函数:
    	function getUserAction(e) {
        	console.log(e);
        	container.innerHTML = count++;
    	};
    //如果我们不使用 debouce 函数,这里打印 MouseEvent 对象:
    MouseEvent { isTrusted:true,screenX:572,screenY:223,clientX:572,clientY:126...}
    //但是在我们实现的 debounce 函数中,却只会打印 undefined!
    
     
     

    所以我们再修改一下代码:

    //第三次更改event对象的版本
    function debounce(func, wait) {
    	var timeout;
    	return function () {
    		var context = this;
    		var args = arguments;
    		clearTimeout(timeout)
    		timeout = setTimeout(function(){
    			func.apply(context, args)
    		}, wait);
       	 }
    }
    
     
     

    到此为止,我们修复了两个小问题:

    • this 指向
    • event 对象

    5. 这个时候,代码已经很是完善了,但是为了让这个函数更加完善,我们接下来思考一个新的需求:

    我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。

    想想这个需求也是很有道理的嘛,那我们加个 immediate 参数判断是否是立刻执行。

    //第四次更改立刻执行的版本
    function debounce(func, wait, immediate) {
    	var timeout;
    	return function () {
         	var context = this;
        	var args = arguments;
    		if (timeout) clearTimeout(timeout);
            	if (immediate) {
                	// 如果已经执行过,不再执行
                	var callNow = !timeout;
                	timeout = setTimeout(function(){
                    	timeout = null;
                	}, wait)
                	if (callNow) func.apply(context, args)
            	}
            	else {
                	timeout = setTimeout(function(){
                    	func.apply(context, args)
                	}, wait);
            	}
        	}
    	}
    
     
     

    完成,我们再来看看效果:
    效果图
    6. 问题来咯,注意一点:就是 getUserAction 函数可能是有返回值的所以我们也要返回函数的执行结果,但是当 immediatefalse 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined所以我们只在 immediatetrue 的时候返回函数的执行结果

    //第五次更改返回值问题的版本
    function debounce(func, wait, immediate) {
    	var timeout, result;
    	return function () {
            	var context = this;
            	var args = arguments;
    			if (timeout) clearTimeout(timeout);
            	if (immediate) {
                	// 如果已经执行过,不再执行
                	var callNow = !timeout;
                	timeout = setTimeout(function(){
                    	timeout = null;
                	}, wait)
                	if (callNow) result = func.apply(context, args)
            	}
            	else {
                	timeout = setTimeout(function(){
                    	func.apply(context, args)
                	}, wait);
            	}
            	return result;
        	}
    	}
    
     
     

    7. 到此刻,只需再考虑最后一个实际需求,我们就能完成最后版本的防抖代码了。这个小需求是:我希望能取消 debounce 函数,但是,比如说我 debounce 的时间间隔是 10 秒钟,immediatetrue,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行啦,是不是很开心?

    //第六次更改取消问题的版本。  最后版本
    function debounce(func, wait, immediate) {
    	var timeout, result;
    	var debounced = function () {
            	var context = this;
            	var args = arguments;
    			if (timeout) clearTimeout(timeout);
            	if (immediate) {
                	// 如果已经执行过,不再执行
                	var callNow = !timeout;
                	timeout = setTimeout(function(){
                    	timeout = null;
                	}, wait)
                	if (callNow) result = func.apply(context, args)
            	}
            	else {
                	timeout = setTimeout(function(){
                    	func.apply(context, args)
                	}, wait);
            	}
            	return result;
        	};
    		debounced.cancel = function() {
            	clearTimeout(timeout);
            	timeout = null;
        	};
    		return debounced;
    }
    
     
     

    那么该如何使用这个 cancel 函数呢?依然是以最最最上面的 demo 为例:

    	var count = 1;
    	var container = document.getElementById('container');
    	function getUserAction(e) {
        	container.innerHTML = count++;
    	};
    	var setUseAction = debounce(getUserAction, 10000, true);
    	container.onmousemove = setUseAction;
    	document.getElementById("button").addEventListener('click', function(){
        	setUseAction.cancel();
    })
    
     
     

    一起看看最终效果把:
    效果

    终于,我们已经完整实现了一个 underscore 中的 debounce 函数,恭喜,撒花!

    那么,underscore又是啥呢?

    JavaScript是函数式编程语言,支持高阶函数和闭包。函数式编程非常强大,可以写出非常简洁的代码。例如Array的map()和filter()方法,但是,Array有map()和filter()方法,可是Object没有这些方法。此外,低版本的浏览器例如IE6~8也没有这些方法,怎么办
    方法一,自己把这些方法添加到Array.prototype中,然后给Object.prototype也加上mapObject()等类似的方法。
    方法二,直接找一个成熟可靠的第三方开源库,使用统一的函数来实现map()、filter()这些操作。

    采用方法二,选择的第三方库就是underscore

    正如jQuery统一了不同浏览器之间的DOM操作的差异,让我们可以简单地对DOM进行操作,underscore则提供了一套完善的函数式编程的接口,让我们更方便地在JavaScript中实现函数式编程。

    jQuery在加载时,会把自身绑定到唯一的全局变量$上,underscore与其类似,会把自身绑定到唯一的全局变量_上,这也是为啥它的名字叫underscore的原因。

    想了解更多underscore,请看这里
    这篇写的也不错吼,讲防抖与节流区别的
    本文学习自这位大佬,感谢!

    • 1
      点赞
    • 0
      收藏
      觉得还不错? 一键收藏
    • 0
      评论

    “相关推荐”对你有帮助么?

    • 非常没帮助
    • 没帮助
    • 一般
    • 有帮助
    • 非常有帮助
    提交
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值