Javascript 中的函数大多数状况下都是用户调用执行的,可是在某些场景下不是用户直接控制的,在这些场景下,函数会被频繁调用,容易形成性能问题。html
好比在 window.onresize
事件和 window.onScroll
事件中,因为用户能够不断地触发,这会致使函数短期内频繁调用,若是函数中有复杂的计算,很容易就形成性能的问题。前端
这些场景下最主要的问题是触发频率过高,1s内能够触发数次,可是大多数状况下咱们并不须要那么高的触发频率,可能只要在500ms内触发一次,这样其实咱们能够用 setTimeout
来解决,在这期间的触发都忽略掉。git
咱们能够先尝试着本身实现一个节流函数:github
// 本身实现的简单节流函数
function throttle (func, time) {
var timeout = null,
context = null,
args = null
return function() {
context = this
args = arguments
// 只要timeout函数存在,全部调用都无视
if(timeout) return;
timeout = setTimeout(function() {
func.apply(context, args)
clearTimeout(timeout)
timeout = null
}, time||500)
}
}
复制代码
咱们实现了一个简单的节流函数,可是还不够完整,若是我想在第一次触发的时候当即执行怎么办?若是我想禁用掉最后一次执行怎么办?underscore 中实现了一个比较完整的节流函数。segmentfault
// options是一个对象,若是options.leading为false,就是禁用第一次触发当即调用
// 若是options.trailing为false,则是禁用第一次执行
_.throttle = function (func, wait, options) {
// 一些初始化操做
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function () {
// 若是禁用第一次首先执行,返回0不然就用previous保存当前时间戳
previous = options.leading === false ? 0 : _.now();
// 解除引用
timeout = null;
result = func.apply(context, args);
// 看到一种说法是在func函数里面从新给timeout赋值,会致使timeout依然存在,因此这里会判断!timeout
if (!timeout) context = args = null;
};
return function () {
// 获取当前调用时的时间(ms)
var now = _.now();
// 若是previous为0而且禁用了第一次执行,那么将previous设置为当前时间
// 这里用全等来避免undefined的状况
if (!previous && options.leading === false) previous = now;
// 还要wait时间才会触发下一次func
var remaining = wait - (now - previous);
context = this;
args = arguments;
// remaining小于0有两种状况,一种是上次调用后到如今已经到了wait时间
// 一种状况是第一次触发的时候而且options.leading不为false,previous为0,由于now记录的是unix时间戳,因此会远远大于wait
// remaining大于wait的状况我本身不清楚,但看到一种说法是客户端系统时间被调整过,可能会出现now小于previous的状况
// 这两种情形下会当即执行func函数,并把previous设置为now
if (remaining <= 0 || remaining > wait) {
if (timeout) {
// 清除定时器
clearTimeout(timeout);
timeout = null;
}
// previous保存当前触发的时间戳
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
// 若是timeout不存在(当前定时器还存在)
// 而且options.trailing不为false,这个时候会从新设置定时器,remaining时间后执行later函数
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
复制代码
这段代码看着很少,可是让我纠结了好久,运行的时候主要会有如下几种状况。bash
now - previous
几乎为0,这个时候知足 else if
里面的判断,会设置一个定时器,这个定时器在 remaining 时间后执行,因此只要在 remaining 时间内无论咱们再怎么频繁触发,因为不会知足两个 if 里面的条件,因此都不会执行 func,一直到 remaining 后才会执行func这种状况和上面状况相似,不过区别在于第一次触发的时候。微信
因为知足 !previous && options.leading === false
这个条件,因此 previous 会被设置为 now,这个时候 remaining 等于 wait,因此会走 else if
的分支,这样就会重复前一种状况下步骤2的流程app
remaining <= wait
,可是又由于 options.trailing
为 false,这样就不会走 if 的任何一个分支,一直到 now-previous
大于 wait 的时候(也就是过了 wait 时间后),这样会知足 if 第一个分支的条件,func 会当即被执行一次最好不要这么写,由于会致使一个 bug 的出现,若是咱们在一段时间内频繁触发,这个是没什么问题,但若是咱们最后一次触发后中止等待 wait 时间后再从新开始触发,这时候的第一次触发就会当即执行 func,leading 为 false 并无生效。函数
不知道有没有人和我同样有这两个疑问,leading 为 false 的时候,真的只是在第一次调用的时候有区别吗?trailing 是怎么作到禁用最后一次执行的?源码分析
这两个问题让我昨晚睡觉前都还在纠结,还好今天在 segmentfault 上面有热心的用户帮我解答了。
请直接看第一个回答以及下面的评论区:关于 underscore 源码 中throttle 函数的疑惑?
GDUTxxZ 大神给了一段代码,执行后不一样的表现让我印象深入。
var _now = new Date().getTime()
var throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
console.log(`函数${++i}在${new Date().getTime() - _now}调用`)
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 若是超过了wait时间,那么就当即执行
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;
};
};
var i = 0
var test = throttle(() => {
console.log(`函数${i}在${new Date().getTime() - _now}执行`)
}, 1000, {leading: false})
setInterval(test, 3000)
复制代码
我将传入 leading 和没传入 leading 的状况做了如下比较。 leading 为 false 时:
callback => wait => callback
通常状况下固然不会有这种极端状况存在,可是可能出现这种状况。若是在 scroll 事件中,咱们滚动一段距离后中止了,等 wait ms 后再开始滚动,这个时候若是 leading 为 false,依然会延迟 wait 时间后执行,而不是当即执行,这也是为何同时设置 leading 和 trailing 为 false 的时候会出现问题。
trailing 为 false 时究竟是怎么禁用了最后一次调用?这个也一直让我很纠结。一样的,我也写了一段代码,比较了一下两次运行后的不一样结果。
var _now = new Date().getTime()
var throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
console.log(`函数${++i}在${new Date().getTime() - _now}调用`)
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 若是超过了wait时间,那么就当即执行
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;
};
};
var i = 0
var test = throttle(() => {
console.log(函数${i}在${new Date().getTime() - _now}执行)
}, 1000, {trailing: false})
window.addEventListener("scroll", test)
复制代码
trailing 为 false 时:
没有设置 trailing 时:
这两张图很明显的不一样就是设置了 trailing 的时候,最后一次老是"执行",而未设置 trailing 最后一次老是"调用",少了一次执行。
咱们能够假设在一种临界的场景下,好比在倒数第二次执行 func 后的 (wait-1) 的时间内。
若是设置了 trailing,由于没法走 setTimeout,因此只能等待 wait 时间后才能当即调用 func,因此在(wait-1)的时间内不管咱们触发了多少次都不会执行 func 函数。
若是没有设置 trailing,那么确定会走 setTimeout,在这个期间触发的第一次就会设置一个定时器,等到 wait 时间后自动执行 func 函数,到(wait-1)的这段时间内无论咱们触发了多少次,反正第一次触发的时候就已经设置了定时器,因此到最后必定会执行一次 func 函数。
好久之前就使用过 throttle 函数,本身也实现过简单的,可是看到 underscore 源码后才发现原来还会有这么多使人充满想象的场景,本身所学的这点知识真的是皮毛。
若是你以为这篇内容对你挺有启发,我想邀请你帮我三个小忙: