在前端开发中,经常会遇到一些高频触发的事件,然而我们往往并不需要关心每次事件触发的具体过程,尤其是当我们希望减少不必要的函数调用次数或者控制函数的执行频率时。针对这类问题,我们可以运用防抖和节流,它们是前端开发中常用的两种性能优化技术。本文将通过两个简单的案例,介绍防抖和节流函数的适用场景以及使用方法,并对它们进行了封装,以便日后直接调用。这些技术的灵活应用将有助于改善前端开发中的用户体验,优化页面性能,并减少不必要的资源消耗。

之前看到过一个对防抖节流最好理解的例子:防抖就是游戏里的回城键,每一次按下回城键都会打断上一次回城,并且重新开始回城,只有在最后一次的计时结束后才能回到家;而节流就是游戏里的技能冷却,在冷却时间内无论点击技能多少次都只会释放一次,只有等到冷却时间结束后才能触发第二次释放。

1.防抖

防抖就是指某些事件在一定时间内频繁触发,但往往我们只需要关心最后一次的状态,此时就可以使用防抖技术,将一定时间内的多次触发合并为一次操作。

典型的防抖示例有:搜索框的联想功能、表单的过程验证、浏览器窗口事件等。
下面以密码强弱验证为例:
正常情况下的代码:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		密码:<input type="password" class="input">
		强度:<span class="show">弱</span>
		<div class="count">总共执行了0次check函数</div>
	</body>
	<script>
		//获取dom元素
		let input = document.querySelector('.input');
		let show = document.querySelector('.show');
		let count = document.querySelector('.count');
		//检验密码强弱
		const check = (password) => {
			let msg = ['弱', '中', '强'];
			let lever = -1;
			if (password.length < 6) {
				return msg[0];
			} //如果密码长度小于6位,提示消息为空
			if (password.match(/[a-z]/g)) {
				lever++;
			} //验证是否包含字母
			if (password.match(/[0-9]/g)) {
				lever++;
			} // 验证是否包含数字
			if (password.match(/(.[^a-z0-9])/g)) {
				lever++;
			} //验证是否包含字母,数字,字符
			return msg[lever];
		}
		//记录check函数执行次数
		let time = 1;
		input.addEventListener('input', () => {
			//自定义事件
			show.innerText = check(input.value);
			count.innerText = `总共执行了${time}次check函数`;
			time++;
		})
	</script>
</html>


可以发现,每次输入一个字符都会执行一次check函数检验密码强弱。
增加防抖函数后:(更改上面代码中的input事件函数)

let timer = null;
input.addEventListener('input', () => {
	clearTimeout(timer);
	timer = setTimeout(() => {
		//自定义事件
		show.innerText = check(input.value);
		count.innerText = `执行了${time}次check函数`;
		time++;
	}, 800)
	//停止输入 0.8秒后执行
})


输入同样长度的字符串,可以发现,只有当停止输入或者输入完毕0.8s后才会执行check函数,大大减少了函数的执行次数。

2.节流

节流顾名思义,就是减少流量使用,将频繁触发的事件频率降低,每隔一段时间才执行一次。
典型的节流示例有:轮播图点击切换、上拉加载更多、鼠标点击事件等。
下面以鼠标点击事件为例:
正常情况下的代码:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		<button class="btn" style="width: 80px; height: 30px;">发送请求</button>
		<div class="count">发送了0次请求</div>
	</body>

	<script>
		let btn = document.querySelector('.btn');
		let count = document.querySelector('.count');
		let time = 1;
		btn.addEventListener('click', () => {
			//自定义事件
			count.innerText = `发送了${time}次请求`;
			time++;
		})
	</script>
</html>


疯狂点击按钮50次,可以发现,每当点击一次按钮都会发送一次请求。
增加节流函数后:(更改上面代码中的btn事件函数)

let timer = null;
btn.addEventListener('click', () => {
	if (timer) return;
	timer = setTimeout(() => {
		//自定义事件
		count.innerText = `发送了${time}次请求`;
		time++;
		timer = null;   //重点
	}, 800);
	//每隔0.8秒才会执行一次
})


此时,同样点击按钮50次,无论点击频率有多快,也至少要等0.8秒后才能再次执行。大大减少了事件的频率。

3.封装防抖节流函数

(1)封装防抖函数

function debounce(func, delay) {
  let timer = null;
  return function() {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, arguments);  // 这里如果直接调用func函数,其里面的this会指向window
    }, delay);
  };
}

运用到上述案例:

input.addEventListener('input', debounce(function() {
  show.innerText = check(input.value);
  count.innerText = `执行了${time}次check函数`;
  time++;
}, 800));

(2)封装节流函数

function throttle(func, delay) {
  let timer = null;
  return function() {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, arguments);
        timer = null;
      }, delay);
    }
  };
}

运用到上述案例:

btn.addEventListener('click', throttle(function() {
  count.innerText = `发送了${time}次请求`;
  time++;
}, 800));

4.总结

防抖: n 秒后再执行该事件,若在 n 秒内被重复触发,则重新计时。
节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效。
相同点:都可以通过使用 setTimeout 来实现;都可以降低回调执行频率,节省计算资源。
不同点:函数防抖:一段时间内连续触发事件,只执行最后一次;函数节流:一段时间内只执行一次。