本文共 2000 字,读完只需 8 分钟
上一篇文章讲了去抖函数,然后这一篇讲同样为了优化性能,降低事件处理频率的节流函数。
一、什么是节流?
节流函数(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 传递类数组对象 argumentsresult = 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 都是为了尽可能满足这类工具函数的不同使用场景。