函数的防抖和节流

861 阅读5分钟

在前端开发过程中,经常会需要绑定一些持续触发的事件,如 resizescrollmousemove等等,但是处于性能方向的考虑,并不希望在事件持续触发的过程中那么频繁地去执行函数。对于这种情况,一般来讲,防抖节流是比较好的解决方案。

事件频繁触发情况
<!DOCTYPE html>
<html>
<head>
	<title>防抖和节流</title>
</head>
<body>
	<div id="content" 
	     style="height:150px;
	      line-height:150px;
	      text-align:center; 
	      color: #fff;
	      background-color:#ccc;
	      font-size:80px;">
	</div>
	<script type="text/javascript">
		let num = 1;
        let content = document.getElementById('content');
        function add () {
         	content.innerHTML = num++;
        }
        content.onmousemove = add;
	</script>
</body>
</html>

在上述代码中,div元素绑定了mousemove事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数,效果如下:

从执行的结果上,可以看到,在没有通过其它操作的情况下,函数被频繁地执行导致页面上数据变化特别快,对于这种情况,可以采用防抖和节流的方式,实现优化。

防抖
防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间
  • 非立即执行版
非立即执行版: 触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间

代码如下:

<!DOCTYPE html>
<html>
<head>
	<title>防抖和节流</title>
</head>
<body>
	<div id="content" 
	     style="height:150px;
	      line-height:150px;
	      text-align:center; 
	      color: #fff;
	      background-color:#ccc;
	      font-size:80px;">
	</div>
	<script type="text/javascript">
		let num = 1;
        let content = document.getElementById('content');
        function add () {
         	content.innerHTML = num++;
        }

        //防抖事件 - 非立即执行版
        function fdEvent (func,wait) {
        	let timeout;
        	return function () {
        		let context = this;
        		let args = arguments

        		if(timeout) clearTimeout(timeout);

                timeout = setTimeout(function() {

                  func.apply(context, args)

                }, wait);
        	}
        }
        content.onmousemove = fdEvent(add,1000);

	</script>
</body>
</html>

效果如下:

从执行的结果上,可以看到,在触发事件后函数 1 秒后才执行,如果在触发事件后的 1 秒内又触发了事件,则会重新计算函数的执行时间。

  • 立即执行版
立即执行版: 触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果

代码如下:

<!DOCTYPE html>
<html>
<head>
	<title>防抖和节流</title>
</head>
<body>
	<div id="content" 
	     style="height:150px;
	      line-height:150px;
	      text-align:center; 
	      color: #fff;
	      background-color:#ccc;
	      font-size:80px;">
	</div>
	<script type="text/javascript">
		let num = 1;
        let content = document.getElementById('content');
        function add () {
         	content.innerHTML = num++;
        }

        //防抖事件 - 立即执行版
        function fdEvent (func,wait) {
        	let timeout;
        	return function () {
        		let context = this;
        		let args = arguments;

        		if(timeout) clearTimeout(timeout);

                let callNow = !timeout;
                timeout = setTimeout(function() {
                    timeout = null;
                }, wait);

                if (callNow) func.apply(context, args)
        	}
        }
        content.onmousemove = fdEvent(add,1000);

	</script>
</body>
</html>

效果如下:

在开发过程中,根据不同的需求场景来决定我们需要哪一个版本的防抖函数,一般来讲,基本的防抖函数都能够符合大部分的需求。但是也可以实现非立即执行版和立即执行版相互结合的方式,来实现防抖函数。代码如下:

/**
 * @desc 函数防抖
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param immediate true 表立即执行,false 表非立即执行
 */
function fdEvent(func,wait,immediate) {
    let timeout;

    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(() => {
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}
节流
节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
  • 时间戳版
<!DOCTYPE html>
<html>
<head>
	<title>防抖和节流</title>
</head>
<body>
	<div id="content" 
	     style="height:150px;
	      line-height:150px;
	      text-align:center; 
	      color: #fff;
	      background-color:#ccc;
	      font-size:80px;">
	</div>
	<script type="text/javascript">
		let num = 1;
        let content = document.getElementById('content');
        function add () {
         	content.innerHTML = num++;
        }

        //节流事件 - 时间戳版
        function throttle (func,wait) {
        	let previous = 0;
        	return function () {
        		let now = Date.now();
                let context = this;
                let args = arguments;
                if(now - previous > wait){
                    func.apply(context, args);
                    previous = now;
                }
        	}
        }
        content.onmousemove = throttle(add,1000);

	</script>
</body>
</html>

效果如下:

从执行的结果上,可以看到,在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次

  • 定时器版
<!DOCTYPE html>
<html>
<head>
	<title>防抖和节流</title>
</head>
<body>
	<div id="content" 
	     style="height:150px;
	      line-height:150px;
	      text-align:center; 
	      color: #fff;
	      background-color:#ccc;
	      font-size:80px;">
	</div>
	<script type="text/javascript">
		let num = 1;
        let content = document.getElementById('content');
        function add () {
         	content.innerHTML = num++;
        }

        //节流事件 - 定时器版
        function throttle (func,wait) {
            let timeout;
        	return function () {
                let context = this;
                let args = arguments;
                if (!timeout){
                    timeout = setTimeout(() =>{
                        timeout = null;
                        func.apply(context, args)
                    },wait)
                }
        	}
        }
        content.onmousemove = throttle(add,1000);

	</script>
</body>
</html>

效果如下:

从执行的结果上,可以看到,在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次 。

从两次执行的效果上,可以看到,其实时间戳版和定时器版的节流函数的区别就是:

时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候

同样地,两种方式,也是结合使用:

/**
 * @desc 函数节流
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param type 1 表时间戳版,2 表定时器版
 */
function throttle(func, wait ,type) {
    if(type===1){
        let previous = 0;
    }else if(type===2){
        let timeout;
    }
    return function() {
        let context = this;
        let args = arguments;
        if(type===1){
            let now = Date.now();

            if (now - previous > wait) {
                func.apply(context, args);
                previous = now;
            }
        }else if(type===2){
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    func.apply(context, args)
                }, wait)
            }
        }
    }
}