rwson

rwson

一个前端开发

javascript函数节流和函数去抖

underscore这个库中提供了两个关于控制函数执行频率的方法, throttledebounce

throttledebounce是解决请求和响应速度不匹配问题的两个方案。差异在于选择不同的策略。

比如生活中的最常见的电梯, 分别用这两种策略解释下:

  • throttle: 开门按钮按下 -> 有人进来 -(等10秒, 不管有没有人进来)-> 准时关门运行
  • debounce: 开门按钮按下 -> 有人进来, 关门按钮按下 -(开始计时10秒)-> 开门按钮按下, 又有人进来, 关门按钮按下 -> 重新计时 -(开始计时10秒) -> 10秒时间到, 关门运行

如果我们需要做一个过滤的功能,类似于下面这个效果

throttle

我们需要给input绑定一个keyup事件,然后根据它的value操作页面或者过滤数据重新渲染页面,但是在数据比较多的时候, 如果在keyup里面不做一定限制的话, 在性能方面就会有一些影响, 这时候, 就需要"函数节流"这个东西, 限制在多少秒内触发一次某个函数。

就拿我们上面的效果来说:

	//  HTML
	<input type="text" id="input" />
	<ul id="ul">
		<li>111111111</li>
		<li>22222222</li>
		<li>333333</li>
		<li>4444</li>
		<li>1234</li>
		<li>5678</li>
		<li>9999</li>
		<li>6789</li>
		<li>01234</li>
	</ul>
	
	
	//  javascript
	window.onload = function() {
		var input = document.getElementById("input");
		var ul = document.getElementById("ul");
		var li = ul.getElementsByTagName("li");
		var len = li.length;
		var value, timeout = null;
	   input.onkeyup = function(ev) {
	   		clearTimeout(timeout);
		   	timeout = setTimeout(function() {
		   		clearTimeout(timeout);
		   		value = ev.target.value.trim();
		   		for (var i = 0; i < len; i++) {
		   			if (li[i].innerHTML.indexOf(value) > -1) {
		   				li[i].style.display = "block";
		   			} else {
		   				li[i].style.display = "none";
		   			}
		   		}
		   	}, 200);
	   };
	};

下面可以把这个函数再次进行封装:

/**
 * 函数节流
 * @param  {Function} fn           [要执行的函数]
 * @param  {Number}   delay        [连续执行间隔]
 * @param  {Number}   mustRunDelay [隔多久至少执行一次]
 * @return {Function}
 */
function throttle(fn, delay, mustRunDelay) {
    var timer = null;
    var start;
    return function() {
        var args = arguments,
            curr = +(new Date());
        clearTimeout(timer);
        if (!start) {
            start =  curr;
        }
        if (curr - start >= mustRunDelay) {
            fn.apply(null, args);
            //	重置上次执行时间戳
            start = curr;
        } else {
            timer = setTimeout(function() {
                fn.apply(null, args);
            }, delay);
        }
    };
};

封装完之后, 我们这样改下我们的代码:

    input.onkeyup = throttle(function(e) {
  		//	...
    }, 300, 400);

在平时可以有好多地方用到throttle,比如浏览器的resize,鼠标滚轮事件等等。

debounce

说完了throttle, 我们再来看看debounce的应用场景, 还是用刚才上面的场景做示例:

	//  HTML
	<input type="text" id="input" />
	<ul id="ul">
		<li>111111111</li>
		<li>22222222</li>
		<li>333333</li>
		<li>4444</li>
		<li>1234</li>
		<li>5678</li>
		<li>9999</li>
		<li>6789</li>
		<li>01234</li>
	</ul>
	
	
	//  javascript
	window.onload = function() {
		var input = document.getElementById("input");
		var ul = document.getElementById("ul");
		var li = ul.getElementsByTagName("li");
		var len = li.length;
		var value, timeout = null;
	   input.onkeyup = function(ev) {
        	//	注意这里
        	if(timeout) {
        		return;
        	}
        	//	注意这里
		   	timeout = setTimeout(function() {
		   		clearTimeout(timeout);
		   		timeout = null;
		   		value = ev.target.value.trim();
		   		for (var i = 0; i < len; i++) {
		   			if (li[i].innerHTML.indexOf(value) > -1) {
		   				li[i].style.display = "block";
		   			} else {
		   				li[i].style.display = "none";
		   			}
		   		}
		   	}, 200);
	   };
	};

和上面的例子一对比, 发现改动的地方很少, 就是判断上个timeout的句柄是否存在(上一次处理函数还没有执行), 存在就不往下走, 如果不存在, 延时200毫秒后执行处理函数, 并清除本次timeout的句柄, 这种情况我们在很多地方也会遇到, 比如我们要在一个输入框, 在我们最后一次输入之后300毫秒一次异步请求去根据输入的值获取数据。

throttle一样, 我们一起来封装个debounce, 以便后面调用:

/**
 * [函数去抖]
 * @param  {Function} fn    [要执行的函数]
 * @param  {Number}   delay [最后一次事件处理函数在多少毫秒之后被触发]
 */
function debounce(fn, delay) {
	if (fn.timeoutHandler) {
		return;
	}
	fn.timeout = setTimeout(function() {
		clearTimeout(fn.timeoutHandler);
		fn.timeoutHandler = null;
		fn();
	}, delay);
}

总结

throttledebounce都可以在性能优化方面, 所以在遇到一些频繁操作时(比如鼠标连续点击, 输入框输入, 滚动事件), 考虑到这块会对浏览器或者服务器减轻一些压力, 当然具体用哪个方法还得看具体的业务场景。