JavaScript专题(三)防抖
一、为什么需要防抖
- 高频的函数操作可能产生不好的影响
- 如:resize、scroll、mousedown、mousemove、keyup、keydown……
为此,我们举个示例代码来了解事件如何频繁的触发:
我们写一个 index.html 文件:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<title>debounce</title>
<style>
#wrapper {
width: 100%;
height: 200px;
line-height: 200px;
text-align: center;
color: #fff;
background-color: #444;
font-size: 30px;
}
</style>
</head>
<body>
<div id="wrapper"></div>
<script>
var count = 1;
var oDiv = document.getElementById("wrapper");
function getUserAction() {
oDiv.innerHTML = count++;
}
oDiv.onmousemove = getUserAction;
</script>
</body>
</html>
从左边滑到右边就触发了近100次getUserAction 函数!看如下Gif:
因为这个例子很简单,所以浏览器完全反应的过来,但假设:
- 它的触发频次极高,1分钟2000次,且涉及到大量的位置计算、DOM 操作等工作,
- 存在接口请求,单个函数执行时间较长,但每个函数触发的间隔很近。
这种在一瞬间(短时间内)对浏览器或服务器造成了过多压力的交互就需要进行优化了,为了解决这个问题,一般有两种解决方案: - debounce 防抖
- throttle 节流
他们的目的都是:降低一个函数的触发频率,以提高性能或避免资源浪费。
二、防抖的原理
今天重点讲讲防抖的实现。
防抖的原理就是:你尽管触发事件,但是我一定在事件触发n秒无操作后才执行。举个例子:
我们规定3s为防抖的标准,那么:
- 第一次要求执行事件 - 此时倒计时3s
- 倒计时2s
- 倒计时1s
- 0.5s时事件再次被触发 - 此时倒计时3s
- …3s内无事发生
- 执行事件,共用了5.5s
三、自己实现一个防抖
3.1 第一版
我们根据上一节提到的核心思想,实现第一版代码:
function debounce(func, wait) {
var timer;
return function () {
clearTimeout(timer)
timer = setTimeout(func, wait);
}
}
如果我们要使用它,第一节的例子为例:
oDiv.onmousemove = debounce(getUserAction, 2000);
此时大家可以再次测试一下,事件持续发生时,只有在完全停止2s后,才会触发事件:
写到这里,作为针对部分高频事件的需求来说,已经结束了。我们来看看他的效果:
3.2 第二版
大家都知道,dom节点在触发事件的时候,this指向它本身,本例中则指向oDiv,但是在本例中:我们看一下
var count = 1;
var oDiv = document.getElementById("oDiv");
function getUserAction() {
oDiv.innerHTML = count++;
console.log('this', this); // 此时输出 Window...
}
oDiv.onmousemove = debounce(getUserAction, 2000);
function debounce(func, wait) {
var timer;
return function () {
clearTimeout(timer)
timer = setTimeout(func, wait);
}
}
毕竟经过了一层匿名函数的包裹,this已经指向了window,为了减少影响,我们尝试修正它
function debounce(func, wait) {
var timer;
return function () {
var _this = this; // 记录当前this
clearTimeout(timer)
timer = setTimeout(function(){
func.apply(_this); //将 func的this改为_this
}, wait);
}
}
第三版
解决的this指向问题,我们的函数仍然不够“完美”,JavaScript中,事件处理函数会提供event对象,我们简称为e。
// 使用了 debouce 函数
function getUserAction(e) {
console.log(e); // undefined
oDiv.innerHTML = count++;
};
为了保证它的原汁原味,我们再改第三版:
var count = 1;
var oDiv = document.getElementById("oDiv");
function getUserAction(e) {
oDiv.innerHTML = count++;
console.log('e', e); // MouseEvent
}
oDiv.onmousemove = debounce(getUserAction, 2000);
function debounce(func, wait) {
var timer;
return function () {
var _this = this; // 记录当前this
var arg = arguments; // 记录参数
clearTimeout(timer)
timer = setTimeout(function () {
func.apply(_this, arg); //将 func的this改为_this
}, wait);
}
}
到此为止,我们在尽可能保留Dom事件原有能力的情况下,给函数加上了防抖效果,它可以解决大部分我们日常开发的防抖问题,但我们需要更“完美”
四、防抖进阶
4.1 立即执行
这个需求就是:
- 立即执行
- 保持n秒空白期
- 将n秒空白期置后
想想这个需求也是很有道理的嘛,那我们加个immediate参数判断是否是立刻执行。
function debounce(func, wait, immediate) {
var timer;
return function () {
var _this = this;
var args = arguments;
if (timer) clearTimeout(timer); // 常规流程,间隔内触发时清掉重置定时
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timer; // 1. callNow 初始值是 true, 同步立即执行;随后 timer 才开始执行
timer = setTimeout(function(){
timer = null; // wait 期间,timer 是一个 ID 数字,所以 callNow 为 false,func 在此期间永远不会执行
}, wait) // wait 之后,timer 赋值 null,callNow 为 true,func 又开始立即执行。
if (callNow) func.apply(_this, args)
}
else {
timer = setTimeout(function(){
func.apply(_this, args)
}, wait);
}
}
}
再来看下此时他是什么效果:
4.2 添加简单验证
function debounce(func, wait, immediate) {
var timer;
// 检查函数
if (typeof func !== 'function') {
throw new TypeError('Expected a function');
}
// 保证wait存在
wait = +wait || 0;
const debounced = function () {
var _this = this;
var args = arguments;
if (timer) clearTimeout(timer); // 常规流程,间隔内触发时清掉重置定时
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timer; // 如果不存在定时器,则callNow为true
timer = setTimeout(function () {
timer = null; // 为了保证之后的时效性,手动添加timer
}, wait)
// 因为不存在timer,证明是首次执行,所以直接调用
if (callNow) func.apply(_this, args)
}
else {
timer = setTimeout(function () {
func.apply(_this, args)
}, wait);
}
}
return debounced
}
4.3 添加取消事件方法
如果你希望能取消被防抖的事件,我们可以这样写:
function debounce(func, wait, immediate) {
var timer;
// 检查函数
if (typeof func !== 'function') {
throw new TypeError('Expected a function');
}
// 保证wait存在
wait = +wait || 0;
const debounced = function () {
var _this = this;
var args = arguments;
if (timer) clearTimeout(timer); // 常规流程,间隔内触发时清掉重置定时
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timer; // 如果不存在定时器,则callNow为true
timer = setTimeout(function () {
timer = null; // 为了保证之后的时效性,手动添加timer
}, wait)
// 因为不存在timer,证明是首次执行,所以直接调用
if (callNow) func.apply(_this, args)
}
else {
timer = setTimeout(function () {
func.apply(_this, args)
}, wait);
}
}
const cancel = function(){
clearTimeout(timer);
timer = null;
}
const pending = function(){
return timer !== undefined;
}
debounced.cancel = cancel;
debounced.pending = pending;
return debounced
}
我们再来看看效果:
写到这里这个简单的防抖方法就算OK了,它确实还不算完美,如果在改进上有任何建议,不妨在评论区留言吧~