爱生活,爱编程,学习使我快乐
我们在平时开发的时候,会有很多场景会频繁触发事件,比如说搜索框实时发请求,onmousemove, resize, onscroll等等,有些时候,我们并不能或者不想频繁触发事件,怎么办呢?这时候就应该用到函数防抖和函数节流了!
先看一个例子
demo入口
<div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
<script>
let num = 1;
let content = document.getElementById('content');
var lastDate = new Date().getTime();
function count() {
content.innerHTML = num++;
var nowDate = new Date().getTime();
// 输出两次相隔执行时间差
console.log(nowDate-lastDate)
lastDate = nowDate
};
content.onmousemove = count;
</script>
这段代码, 在灰色区域内鼠标随便移动,就会持续触发 count() 函数,导致的效果如下:
控制台输出的count()函数执行间隔时间如下:
接下来我们通过防抖和节流限制频繁操作。
短时间内多次触发同一事件,只执行最后一次(非立即执行),或者只执行最开始的一次(立即执行),中间的不执行。
【概念】在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
【生活中的实例】如果有人进电梯(触发事件),那电梯将在10秒钟后出发(执行事件监听器),这时如果又有人进电梯了(在10秒内再次触发该事件),我们又得等10秒再出发(重新计时)。
有两个版本:非立即执行版和立即执行版。
demo入口
// 非立即执行版(延迟执行)
function debounce(func, wait) {
var timer;
return function() {
var context = this; // 注意 this 指向
var args = arguments; // arguments中存着event
if (timer) clearTimeout(timer);
timer = setTimeout(function() {
func.apply(this, args)
}, wait)
}
}
// 使用方式如下:
content.onmousemove = debounce(count,1000);
非立即执行版的意思是:触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。只到下个 n 秒内没有再触发事件,才会被执行。效果如下:
demo入口
经过“函数防抖”处理后,控制台输出的count()函数执行间隔时间如下:
可以看出执行间隔变大,减少了函数的执行频率,从而实现对性能的优化。
使用事件监听的方式(有时需要对事件绑定和解绑,如在vue和react中)
// 重新声名一个防抖后的函数
var handleMousemove = debounce(count,1000)
// 绑定
window.addEventListener('mousemove', handleMousemove)
// 解绑
window.removeEventListener('mousemove', handleMousemove)
// 立即执行版
function debounce(func, wait) {
var timer;
return function() {
var context = this; // 注意 this 指向
var args = arguments; // arguments中存着event
if (timer) clearTimeout(timer);
var callNow = !timer;
timer = setTimeout(function() {
// 设置为null即下次callNow为true,即可实现下次执行。
timer = null;
}, wait)
if (callNow) func.apply(context, args);
}
}
// 使用方式如下:
content.onmousemove = debounce(count,1000);
立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。用法同上,效果如下:
// 合成版
/**
* @desc 函数防抖
* @param func 目标函数
* @param wait 延迟执行毫秒数
* @param immediate true - 立即执行, false - 延迟执行
*/
function debounce(func, wait, immediate) {
var timer;
return function() {
var context = this; // 注意 this 指向
var args = arguments; // arguments中存着event
if (timer) clearTimeout(timer);
if (immediate) {
var callNow = !timer;
timer = setTimeout(function() {
// 设置为null即下次callNow为true,即可实现下次执行。
timer = null;
}, wait);
if (callNow) func.apply(context, args);
} else {
timer = setTimeout(function() {
func.apply(context, args);
}, wait)
}
}
}
连续触发事件但是在n秒中只执行一次函数。即 2n 秒内执行 2 次… 。节流如字面意思,会稀释函数的执行频率。
【概念】规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。
【生活中的实例】一种说法是当 1 秒内连续播放 24 张以上的图片时,在人眼的视觉中就会形成一个连贯的动画,所以在电影的播放(以前是,现在不知道)中基本是以每秒 24 张的速度播放的,为什么不 100 张或更多是因为 24 张就可以满足人类视觉需求的时候,100 张就会显得很浪费资源。
同样有两个版本,立即执行版(时间戳版)和延迟执行版(定时器版)。
demo入口
// 立即执行版(时间戳版)
function throttle(func, wait) {
var previous = 0;
return function() {
var now = Date.now();
var context = this; // 注意 this 指向
var args = arguments; // arguments中存着event
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
// 使用方式如下:
content.onmousemove = throttle(count,1000);
效果如下:
demo入口
可以看到,在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。
经过“函数防抖”处理后,控制台输出的count()函数执行间隔时间如下:
可以看出执行频率约等于1000ms,但又都大于1000ms。从而实现指定频率并节流的效果。
// 延迟执行版(定时器版)
function throttle(func, wait) {
var timer;
return function() {
var context = this; // 注意 this 指向
var args = arguments; // arguments中存着event
if (!timer) {
timer = setTimeout(() => {
timer = null;
func.apply(context, args)
}, wait)
}
}
}
// 使用方式如下:
content.onmousemove = throttle(count,1000);
用法同上,效果如下:
demo入口
可以看到,在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次。
我们应该可以很容易的发现,其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内“开始”的时候,而定时器版的函数触发是在时间段内“结束”的时候。
同样地,我们也可以将时间戳版和定时器版的节流函数结合起来,实现合成版的节流函数。
// 合成版
/**
* @desc 函数节流
* @param func 函数
* @param wait 延迟执行毫秒数
* @param immediate true - 立即执行(时间戳版), false - 延迟执行(定时器版)
*/
function throttle(func, wait, immediate) {
if (immediate) {
var previous = 0;
} else {
var timer;
}
return function() {
var context = this; // 注意 this 指向
var args = arguments; // arguments中存着event
if (immediate) {
var now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
} else {
if (!timer) {
timer = setTimeout(() => {
timer = null;
func.apply(context, args)
}, wait)
}
}
}
}
总的来说,适合多次事件一次响应的情况
总的来说,适合大量事件按时间做平均分配触发。
function throttle(func, wait) {
var timer;
return function() {
var context = this; // 注意 this 指向
var args = arguments; // arguments中存着event
if (!timer) {
timer = setTimeout(() => {
timer = null;
func.apply(context, args)
}, wait)
}
}
}
// 使用方式如下:
content.onmousemove = throttle(count,1000);
首先,在执行throttle(count, 1000)
这行代码的时候,会有一个返回值,这个返回值是一个新的匿名函数,因此content.onmousemove = throttle(count,1000)
这句话最终可以这样理解:
content.onmousemove = function() {
var context = this; // 注意 this 指向
var args = arguments; // arguments中存着event
// var args = Array.prototype.slice.call(arguments, 1); // 这样可以去掉arguments中的event
console.log('this', this); // 输出contentDOM元素
console.log('arguments', arguments); // 输出带有event的数组
if (!timer) {
timer = setTimeout(() => {
timer = null;
func.apply(context, args)
}, wait)
}
}
到这边为止,只是绑定了事件函数,还没有真正执行,而this
的具体指向需要到真正运行时才能够确定下来。
其次,当我们触发onmousemove
事件的时候,才真正执行了上述的匿名函数,即content.onmousemove()
。此时,上述的匿名函数的执行是通过对象.函数名()
来完成的,那么函数内部的this
自然指向对象(content)
。
最后,匿名函数内部的func的调用方式如果是最普通的直接执行func()
,那么func
内部的this
必然指向window
,这将会是一个隐藏bug!所以,我们通过匿名函数捕获this
,然后通过func.apply()
的方式修改this
指向。
以上的例子我们的时间间隔都是使用的1000ms(1秒),但是实际在DOM元素拖拽或页面滚动时,1秒就太慢了,很影响用户体验。但是如果时间间隔太短又失去了节流的意义。那时间间隔我们应该设置多少更合适呢?
下面我们再看一个页面滚动事件的例子。
<script>
var num = 1;
var content = document.getElementById('content');
var lastDate = new Date().getTime();
function count() {
content.innerHTML = num++;
var nowDate = new Date().getTime();
// 输出两次相隔执行时间差
console.log(nowDate-lastDate)
lastDate = nowDate
};
window.onscroll = throttle(count, 10);
我们把节流时间间隔设置为10ms,按道理输出的时间间隔最小的应该有10的。但是实现输出基本上都是不小于16。
demo入口
而鼠标移动事件的触发时间间隔却很小。(下图为上面例子中的鼠标移动事件未做防抖和节流的输出结果)
为什么会这样子呢?
在说原因之前,我们先说一下另一个东西——显示器刷新频率。一般显示器的刷新频率是60Hz,我的显示器的刷新频率也是60Hz。这个60Hz的意思是1秒内显示器画面刷新的频率。也就可以计算出60Hz的显示器每次刷新的间隔是1000/60(约等于16)。
如果我们把显示器的刷新频率改掉,如改成48Hz。每次刷新间隔则是1000/48(约等于20)。
输出结果如下图:
所以滚动事件时,函数执行的间隔最小值取绝于显示器的刷新频率。大部分显示器的刷新频率都是60Hz,所以一般最小时间间隔为16ms。有一些玩游戏的朋友显示器可能会要求高一些,刷新频率也就高一些。
所以如果是鼠标移动事件,触发时间间隔即使小于16ms,但是用户看到的最快也只会是16ms。这样子就会造成部分计算的浪费。所以函数节流的最值时间间隔应该设置为16ms。如果是执行的计算量过大,明显影响性能,也可以适当调大。具体以不影响用户体验为标准,建议不要大于30ms。