JS进阶 - 逐步实现 throttle
手写 throttle 函数
用途
throttle 作为节流阀,能在高频次反复调用某个方法时对调用的速率进行控制,比如:监听页面滚动时减少方法的频繁调用、提交按钮防止短时间内的多次点击。
原理
每次调用 throttle 方法时,都会更新 previous 为当前的时间戳,再次调用时比较 previous 和当前时间戳(now)的差值是否大于设定的值(wait),如果差值大于 wait,则再次调用,如果差值小于 wait,则忽略,这样就实现了在一段时间内只调用一次方法
实现
首先来解析一下:下面是一段常见的调用 throttle 方法的代码
document.querySelector('#btn').addEventListener(
"click",
_throttle(function(){
console.count();
}, 2000)
);
在触发点击事件的回调函数时,可以看做是:_throttle(callback).call(event.target, event)
,很明显,_throttle(callback)
需要返回一个函数,那么就可以形成闭包,有了闭包上一次的触发时间戳就能够被记录下来,以下代码实现了 throttle 的基本功能
<button id="btn">click me</button>
<script>
function _throttle(func, wait){
// 初始化时间戳
let previous = 0;
// 返回 function,且引用了父函数的变量,形成了闭包
return function() {
const now = Date.now();
// 如果本次和上次的时间戳差值大于 wait 的值,则执行方法
if (now - previous > wait) {
// 记录当前时间戳
previous = now;
// 将回调函数指向调用 throttle 方法的 this,并传入参数
// 如果这里不绑定 this,那么 this 不会指向调用他的函数,而会指向全局对象 Window
return func.apply(this, arguments);
}
};
};
// 对按钮设置监听
document.querySelector('#btn').addEventListener(
"click",
_throttle(function(){
console.count();
}, 2000)
);
</script>
进阶
underscorejs 的 throttle 方法提供了更多的选项:启用/禁用前边界调用或后边界调用。
可禁用前边界调用
默认情况下,前边界调用是开启的(leading:true),首次调用时会立即执行函数,此时 previous 的值为 0,所。如果禁用了前边界调用,首次调用将不会执行,实现也很简单,只需要在初始化时,将 0 改为当前时间戳即可。(有备注的地方为新增的代码)
function _throttle(func, wait, options = {}){
let context, args, result;
let previous = 0;
return function() {
const now = Date.now();
// leading 默认是 undefined ,这里使用全等,即只有在手动传入 false 时才会禁用前边界调用
if (!previous && options.leading === false) {
previous = now;
}
args = arguments;
context = this;
if (now - previous > wait) {
previous = now;
result = func.apply(context, args);
context = args = null;
}
return result;
};
};
const btnElement = document.querySelector("#btn");
btnElement.addEventListener(
"click",
_throttle(
function(){
console.count();
},
2000,
{ leading: false }
)
);
可启用后边界调用
默认情况下,后边界调用是被禁用的(trailing:false),wait 的值为 1 时,一秒内点击三次按钮只会触发一次调用,但如果想让最后一次点击也在下一次可执行时生效,就可以使用后边界调用。
其原理就是设置定时器,在下一次可执行时再次执行函数,代码和 function 中的类似,只不过 previous 就不一定是当前时间戳了,因为要考虑到下一次调用时,如果是启用了前边界调用,则需要立即触发,如果这时候 previous 是当前时间戳,两数相减有可能小于 wait ,那就不能立即执行了,所以需要对 leading 的值进行判断
function _throttle(func, wait, options = {}){
let context, args, result;
let timeout = null;
let previous = 0;
return function() {
const now = Date.now();
if (!previous && options.leading === false) {
previous = now;
}
args = arguments;
context = this;
// 计算当前到下一次可触发时的时间间隔
// 因为时间可能被调整,所以这里取差值的绝对值
const remaining = wait - Math.abs(now - previous);
if (remaining <= 0) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
result = func.apply(context, args);
previous = now;
context = args = null;
} else if (!timeout && options.trailing === true) {
timeout = setTimeout(() => {
// 增加对 leading 的判断
previous = options.leading === false ? now : 0;
timeout = null;
result = func.apply(context, args);
context = args = null;
}, remaining);
}
return result;
};
};
const btnElement = document.querySelector("#btn");
btnElement.addEventListener(
"click",
_throttle(
function(){
console.count();
},
2000,
{ trailing: true }
)
);
与 underscore 比较
来看 underscore 的 throttle 代码:
_.throttle = function(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = _.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};
有几点不同的是:
- underscore 没有取
now - previous
的绝对值,而是在remaining > wait
时一律允许触发操作。 - 在控制后边界执行的函数中(underscore 将它写在 later 方法中),将 timeout 设置为 null,但又在后面判断了一次 timeout 是否存在,多数的说法是为了防止 func 在执行期间有新的 timeout 被设置,如果 timeout 被清空了,代表不再有等待执行的 func,也清空 context 和 args,不过没有看到具体的例子。
- 增加了 cancel 方法,可以途中取消节流阀
完整代码
function _throttle(func, wait, options = {}){
let context, args, result;
let timeout = null;
let previous = 0;
const throttled = function() {
const now = Date.now();
if (!previous && options.leading === false) {
previous = now;
}
args = arguments;
context = this;
const remaining = wait - Math.abs(now - previous);
if (remaining <= 0) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
result = func.apply(context, args);
previous = now;
context = args = null;
} else if (!timeout && options.trailing === true) {
timeout = setTimeout(() => {
previous = options.leading === false ? now : 0;
timeout = null;
result = func.apply(context, args);
if(!timeout){
context = args = null;
}
}, remaining);
}
return result;
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled
};
JS进阶 - 逐步实现 throttle