throttle(func, wait, options)
节流函数,返回一个函数的节流版本;所谓节流版本,就是给需要执行的函数一个执行间隔:每隔wait
ms才执行一次func
。
写个很简单的节流函数还是很简单的
var throttle = function (func, wait) {
var context, args;
var flag = true;
var later = function () {
flag = true;
}
return function () {
var result;
var context = this, args = arguments;
if (flag) {
result = func.apply(context, arguments);
flag = false;
setTimeout(later, wait);
}
return result;
}
}
我们用一个flag
变量来控制目标函数的执行,通过定时器来改变flag
的值;看上去节流函数应该是实现了。但是我们发现,这个节流函数默认不会执行最后一次执行,除非时间卡得好;而且会默认执行第一次函数执行,当然这可以通过改变flag
初始值来解决。
underscore
里对节流函数还有个要求,就是options
参数:如果你想禁用第一次首先执行的话,传递{leading: false}
,还有如果你想禁用最后一次执行的话,传递{trailing: false}
。
我们可以通过判断options.leading
来决定flag
的初始值;那么如果执行最后一次执行(这里的最后一次执行当然是指函数执行时间发生在wait
期间),该怎么做呢?
通过阅读underscore
源码,我发现作者思路是这样的:被节流的函数虽然不会执行,却会生成一个唯一的定时器;这个定时器只会被正确执行的函数销毁,如果没有被销毁,定时器就会执行所谓的“最后一次执行”。同时后续的每一二个没有被正确执行的函数,都会更新这个定时器要执行的函数的this
和args
那么思路清晰了,我们来写这样一个版本的节流函数
var throttle = function (func, wait, options) {
// 返回函数的this指针,参数,以及返回结果
var result, context, args;
// 维护定时器
var timeout = null;
// 维护上一次函数执行的时间
var previous = 0;
options = options || {};
var later = function () {
// 这个函数在某一连续执行阶段的最后执行的
// 因此previous需要像一开始那样初始化
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
}
return function () {
var now = _.now();
// 这里是关键,决定了函数的执行与最后一次相关
context = this, args = arguments;
if (!previous && options.leading === false) {
previous = now;
}
// 计算下一次触发的时间
var remaining = wait - (now - previous);
// 已经到了触发的时间 || 人为修改了时间
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;
}
}
debounce(func, wait, immediate)
防抖函数的场景差不多都类似于这两种:
- 当用户输入,导致搜索框的内容发生变化时,我们希望函数的触发是在内容发生变化
wait
ms并且没有变化后才发出请求 - 用户(更多是测试……)连续点击一个
button
,我们希望只有首次点击触发事件;后wait
ms内的几次触发都不再触发事件。
这样分析,我们发现防抖函数的要求有两种,一种是在wait
ms的开始触发,一种是在wait
ms之后触发。这就是debounce
中immediate
设计的意义。
var debounce = function (func, wait, immediate) {
var context, result, args;
// 计时器
var timeout = null;
// 记录上次代码执行时间戳
var timestamp;
var later = function () {
// 计算自上次执行代码过了多久
var last = _.now() - timestamp;
if (last < wait && last >= 0) {
// 如果此时处于代码执行后wait ms内
// 重新设置计时器
timeout = setTimeout(later, wait - last);
} else {
// 此时代码已经执行了wait ms
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
}
return function () {
// 记录执行状态
context = arguments, context = this;
// 记录执行时间戳
timestamp = _.now();
// 代码是否立即执行
var canNow = immediate && !timeout;
if (canNow) {
result = func.apply(context, args);
context = args = null;
}
// 设置定时器
if (!timeout) {
time = setTimeout(later, wait);
}
}
}
如果immediate
为true
,canNow
变量就会生效,第一次执行就会执行代码;并且在之后的计时器里,只要计时器存在,代码就不会被执行。
如果immediate
为false
,每次执行防抖函数都会更新时间戳;定时器自设置wait
ms后就会比较当前时间和时间戳,如果相差wait
ms,才会执行代码。
想到的额外的拓展
比如说触底刷新,