防抖和节流
防抖和节流在前端日常开发中使用的的频率相当高,在小程序还有Hybrid应用开发中也会使用到它。像underscore中就提供了这样的高阶函数。
防抖常见的使用场景有:
- 监听scroll的滚动
- 搜索框输入查询
- 表单验证
- 按钮点击事件
- 浏览器窗口缩放(resize事件)
防抖(debounce)
原理:事件响应函数在第一次被触发时,并不会立即执行,而是在一个计时器阈值后执行。例如我们设置的计时器的阈值是300ms,如果在这300ms内没有再次触发这个事件响应函数,那么就执行这个响应函数;如果在300ms内再次触发这个事件响应函数,那么当前的计时取消,重新开始计时。
这样,我们如果在300ms时间内大量触发同一个响应事件,最终只会执行一次函数。
使用underscore的高阶函数来实现防抖:
//使用underscore的高阶函数来实现防抖
container.onmousemove = _.debounce(mouseMove, 300);
//设置为true,则会立即触发一次函数执行,默认为false
container.onmousemove = _.debounce(mouseMove, 300, true);
我们可以模仿underscore中的实现,将防抖抽取成一个debounce.js
文件,里面这样实现即可:
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function() {
//改变内部函数的this的指向
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;
}
下面,我们演示如何使用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#container {
width: 100%;
height: 400px;
line-height: 400px;
text-align: center;
background-color: #111;
color: #fff;
font-size: 40px;
}
</style>
</head>
<body>
<div id="container"></div>
<button id="button">取消防抖</button>
<script src="https://cdnjs.gtimg.com/cdnjs/libs/underscore.js/1.7.0/underscore.js"></script>
<script src="./debounce.js"></script>
<script>
let count = 0;
let container = document.getElementById('container');
let button = document.getElementById('button');
function mouseMove(event) {
container.innerHTML = count++;
console.log(this);
console.log(event);
return '哈哈哈哈哈'
}
// 会多次触发鼠标移动事件
// container.onmousemove = mouseMove;
//使用underscore的高阶函数来实现防抖
// container.onmousemove = _.debounce(mouseMove, 300);
//设置为true,则会立即触发一次函数执行,默认为false
// container.onmousemove = _.debounce(mouseMove, 300, true);
let debounced = debounce(mouseMove, 3000);
container.onmousemove = debounced;
button.onclick = function() {
console.log('取消防抖');
debounced.cancel();
}
</script>
</body>
</html>
我们创建一个html,然后引用我们的debounce文件。对比使用underscore框架,效果一致。
节流(throttle)
原理:如果你持续触发一个响应事件,那么每隔一段时间,就会执行这个事件的回调函数。
我们可以从使用场景来区别防抖和节流。假如我们有一个很长的列表,用户在不间断(触发间隔小于300ms)的滚动。我们如果使用防抖(设置超时阈值为300ms),那么我们的事件回调只有等用户停止滚动(间隔大于300ms)后才会执行,如果用户总是在不间断的滚动,那么理论上我们的事件回调就不会执行;现在,我们希望用户在间断的滚动过程中,每隔(2000ms)一段时间,就显示当前滚动距顶部的距离,那么就需要使用节流了。
节流常见的使用场景有:
- DOM元素的拖拽
- 计算鼠标的移动距离
- 监听scroll的滚动
- 搜索框输入查询
使用underscore的高阶函数来实现防抖:
container.onmousemove = _.throttle(mouseMove, 2000,{
leading: false,
trailing: false
});
默认情况下,在underscore中,leading默认为true,表示当你触发事件时会立即执行响应回调函数;trailing默认为true,表示当你停止触发事件(此时正处于下一次2000ms的回调间隔内),会执行这最后一次的回调。需要注意的是,leading和trailing不能同时为false,否则会有bug。现象是“第一次”可以达到我们想要的同时为false的效果,“第二次”则不能达到同时为false的效果。
节流可以有2种方式实现,一种是使用时间戳,一种是定时器,下面我们将分别实现它。
使用时间戳的方式实现节流
// 相当于underscore中leading: true ,trailing: false的情况
function throttle(func, wait) {
var context, args;
var old = 0;
return function() {
context = this;
args = arguments;
var now = new Date().valueOf();
if (now - old > wait) {
old = now;
func.apply(context,args);
}
}
}
使用定时器的方式实现节流
// 相当于underscore中leading: false ,trailing: true的情况
function throttle(func, wait) {
var context, args, timeout;
return function() {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(function() {
timeout = null;
func.apply(context,args);
}, wait);
}
}
}
结合定时器和时间戳来实现节流
// 相当于underscore中leading: true ,trailing: true的情况
function throttle(func, wait) {
var context, args, timeout;
var old = 0;
return function() {
context = this;
args = arguments;
var now = new Date().valueOf();
if (now - old > wait) {
old = now;
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
func.apply(context,args);
} else if (!timeout) {
timeout = setTimeout(function() {
timeout = null;
old = new Date().valueOf()
func.apply(context,args);
}, wait);
}
}
}
仿照underscore框架实现最终版
模仿underscore,可以通过options来控制第一次是否执行和最后一次是否执行的情况:
function throttle(func, wait,options) {
if (!options) options = {};
var context, args, timeout;
var old = 0;
return function() {
context = this;
args = arguments;
var now = new Date().valueOf();
if (!old && options.leading === false) {
old = now;
}
if (now - old > wait) {
// 第一次会执行
old = now;
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
func.apply(context,args);
} else if (!timeout && options.trailing !== false) {
// 最后一次会执行
timeout = setTimeout(function() {
timeout = null;
old = new Date().valueOf()
func.apply(context,args);
}, wait);
}
}
}
下面,我们演示如何使用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#container {
width: 100%;
height: 400px;
line-height: 400px;
text-align: center;
background-color: #111;
color: #fff;
font-size: 40px;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="https://cdnjs.gtimg.com/cdnjs/libs/underscore.js/1.7.0/underscore.js"></script>
<script src="./throttle.js"></script>
<script>
let count = 0;
let container = document.getElementById('container');
let button = document.getElementById('button');
function mouseMove(event) {
container.innerHTML = count++;
console.log(this);
// console.log(event);
// return '哈哈哈哈哈'
}
//使用underscore的高阶函数来实现节流
// container.onmousemove = _.throttle(mouseMove, 2000,{
// leading: true,
// trailing: false
// });
container.onmousemove = throttle(mouseMove, 2000, {
leading: false,
trailing: true
});
</script>
</body>
</html>
总结
防抖和节流应该算是对性能的优化。至于何时使用,还要结合需求场景。
我们注意到,对于搜索框输入查询,我们即可以使用防抖方案,也可以使用节流方案。但是,二者的业务需求是不同的。例如如果要支持输入“实时搜索”,那么使用节流方案就比较合适;如果,我们只是希望用户在超过一段时间后当成“输入完成”才去搜索,那么使用防抖方案是比较合适的。