上一篇文章讲了去抖函数,然后这一篇讲同样为了优化性能,降低事件处理频率的节流函数。
一、什么是节流?
节流函数(throttle)就是让事件处理函数(handler)在大于等于执行周期时才能执行,周期之内不执行,即事件一直被触发,那么事件将会按每小段固定时间一次的频率执行。
打个比方:王者荣耀、英雄联盟、植物大战僵尸游戏中,技能的冷却时间,技能的冷却过程中,是无法使用技能的,只能等冷却时间到之后才能执行。
那什么样的场景能用到节流函数呢?
比如:
- 页面滚动和改变大小时需要进行业务处理,比如判断是否滑到底部,然后进行懒加载数据。
- 按钮被高频率地点击时,比如游戏和抢购网站。
我们通过一个简单的示意来理解:
节流函数可以用时间戳和定时器两种方式进行处理。
二、时间戳方式实现
<div class="container" id="container">
正在滑动:0
</div>
<script>
window.onload = function() {
var bodyEl = document.getElementsByTagName("body")[0]
}
var count = 0;
window.onmousemove = throttle(eventHandler, 1000);
function eventHandler(e) {
var containerEl = document.getElementById("container");
containerEl.innerHTML = "正在滑动: " + count;
count++;
}
function throttle(func, delay) {
var delay = delay || 1000;
var previousDate = new Date();
var previous = previousDate.getTime(); // 初始化一个时间,也作为高频率事件判断事件间隔的变量,通过闭包进行保存。
return function(args) {
var context = this;
var nowDate = new Date();
var now = nowDate.getTime();
if (now - previous >= delay) { // 如果本次触发和上次触发的时间间隔超过设定的时间
func.call(context, args); // 就执行事件处理函数 (eventHandler)
previous = now; // 然后将本次的触发时间,作为下次触发事件的参考时间。
}
}
}
</script>
复制代码
看时间戳实现版本的效果:
三、定时器方式实现
<div class="container" id="container">
正在滑动: 0
</div>
<script>
window.onload = function() {
var bodyEl = document.getElementsByTagName("body")[0]
}
var count = 0;
window.onmousemove = throttle(eventHandler, 1000);
function eventHandler(e) {
var containerEl = document.getElementById("container");
containerEl.innerHTML = "正在滑动: " + count;
count++;
}
function throttle(func, delay) {
var delay = delay || 1000;
var timer = null;
return function(args) {
var context = this;
var nowDate = new Date();
var now = nowDate.getTime();
if (!timer) {
timer = setTimeout(function() {
func.call(context, args);
timer = null;
}, delay)
}
}
}
</script>
复制代码
看看定时器版实现版本的效果:
三、时间戳和定时器的对比分析
对比时间戳和定时器两种方式,效果上的区别主要在于:
事件戳方式会立即执行,定时器会在事件触发后延迟执行,而且事件停止触发后还会再延迟执行一次。
具体选择哪种方式取决于使用场景。underscore 把这两类场景用 leading 和 trailing 进行了表示。
四、underscore 源码实现
underscore 的源码中就同时实现了时间戳和定时器实现方式,在调用时可以自由选择要不要在间隔时间开始时(leading)执行,或是间隔时间结束后(trailing)执行。
具体看伪代码和示意图:
<div class="container" id="container">
正在滑动: 0
</div>
<div class="height"></div>
<script>
window.onload = function() {
var bodyEl = document.getElementsByTagName("body")[0]
}
var count = 0;
// 事件处理函数
function eventHandler(e) {
var containerEl = document.getElementById("container");
containerEl.innerHTML = "正在滑动: " + count;
count++;
}
var _throttle = function(func, wait, options) {
var context, args, result;
// 定时器变量默认为 null, 是为了如果想要触发了一次后再延迟执行一次。
var timeout = null;
// 上一次触发事件回调的时间戳。 默认为 0 是为了方便第一次触发默认立即执行
var previous = 0;
// 如果没有传入 options 参数
// 则将 options 参数置为空对象
if (!options)
options = {};
var later = function() {
// 如果 options.leading === false
// 则每次触发回调后将 previous 置为 0, 表示下次事件触发会立即执行事件处理函数
// 否则置为当前时间戳
previous = options.leading === false ? 0 : +new Date();
// 剩余时间跑完,执行事件,并把定时器变量置为空,如果不为空,那么剩余时间内是不会执行事件处理函数的,见 else if 那。
timeout = null;
result = func.apply(context, args);
// 剩余时间结束,并执行完事件后,清理闭包中自由变量的内存垃圾,因为不再需要了。
if (!timeout)
context = args = null;
};
// 返回的事件回调函数
return function() {
// 记录当前时间戳
var now = +new Date();
// 第一次执行回调(此时 previous 为 0,之后 previous 值为上一次时间戳)
// 并且如果程序设定第一个回调不是立即执行的(options.leading === false)
// 则将 previous 值(表示上次执行的时间戳)设为 now 的时间戳(第一次触发时)
// 表示刚执行过,这次就不用执行了
if (!previous && options.leading === false)
previous = now;
// 间隔时间 和 上一次到本次事件触发回调的持续时间的时间差
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果间隔时间还没跑完,则不会执行任何事件处理函数。
// 如果超过间隔时间,就可以触发方法(remaining <= 0)
// remaining > wait,表示客户端系统时间被调整过
// 也会立即执行 func 函数
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
// 解除引用,防止内存泄露
timeout = null;
}
// 重置前一次触发的时间戳
previous = now;
// result 为事件处理函数(handler)的返回值
// 采用 apply 传递类数组对象 arguments
result = func.apply(context, args);
// 引用置为空,防止内存泄露
if (!timeout)
context = args = null;
} else if (!timeout && options.trailing !== false) {
// 如果 remaining > 0, 表示在间隔时间内,又触发了一次事件
// 如果 trailing 为真,则会在间隔时间结束时执行一次事件处理函数(handler)
// 在从触发到剩余时间跑完,会利用一个定时器执行事件处理函数,并在定时器结束时把 定时器变量置为空
// 如果剩余事件内已经存在一个定时器,则不会进入本 else if 分支, 表示剩余时间已经有一个定时器在运行,该定时器会在剩余时间跑完后执行。
// 如果 trailing = false,即不需要在剩余时间跑完执行事件处理函数。
// 间隔 remaining milliseconds 后触发 later 方法
timeout = setTimeout(later, remaining);
}
// 回调返回值
return result;
};
};
window.onmousemove = _throttle(eventHandler, 1000);
</script>
复制代码
下面是我画的示意图:
- 如果 leading 为真,那么绿色意味着间隔时间的开始会立即执行,第一次触发也会立即执行。
- 如果 trailing 为真,那么从蓝紫色的竖细线后的剩余事件,会跑一个定时器,定时器在时间间隔结束时再执行一次事件处理函数。
- 如果 leading 不为真,那么第一次事件触发不会立即执行。
- 如果 trailing 不为真,最后一次事件触发后,不然再执行一次事件处理函数。
节流和去抖的常见场景
- 输入框打字输入完后才开始异步请求数据校验内容:去抖
- 下拉滚动条判断是否到底部,进行懒加载数据:去抖和节流都可以,判断是否到底的方式不同
- 活动网站、游戏网站,按钮被疯狂点击:节流
五、总结
去抖和节流函数都是为了降低高频率事件触发的事件处理频率,从而优化网页中大量重绘重排带来的性能问题。
其区别在于去抖会在高频率事件触发时,只执行一次,节流会在满足间隔时间后执行一次。去抖的 immediate,节流中的 leading, trailing 都是为了尽可能满足这类工具函数的不同使用场景。
欢迎关注我的个人公众号“谢南波”,专注分享原创文章。