上次介绍了前端性能优化之防抖-debounce,此次来聊聊它的兄弟-节流。javascript
再拿乘电梯的例子来讲:坐过电梯的都知道,在电梯关门但未上升或降低的一小段时间内,若是有人从外面按开门按钮,电梯是会再开门的。要是电梯空间没有限制的话,那里面的人就一直在等。。。后来电梯工程师收到了好多投诉,因而他们就改变了方案,设定每隔必定时间,好比30秒,电梯就会关门,下一节电梯会继续等待30秒。前端
专业术语归纳就是:每隔必定时间,执行一次函数。java
最简易版的代码实现:性能优化
function throttle(fn, delay) {
let timer = null;
return function() {
const context = this;
const args = arguments;
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
}
};
}
复制代码
很好理解,返回一个匿名函数造成闭包,并维护了一个局部变量timer。只有在timer不为null才开启定时器,而timer为null的时机则是定时器执行完毕。闭包
除了定时器,还能够用时间戳实现:app
function throttle(fn, delay) {
let last = 0;
return function() {
const context = this;
const args = arguments;
const now = +new Date();
const offset = now - last;
if (offset > delay) {
last = now;
fn.apply(context, args);
}
};
}
复制代码
last表明上次执行fn的时刻,每次执行匿名函数都会计算当前时刻与last的间隔,是否比咱们设定的时间间隔大,若大于,则执行fn,并更新last的值。前端性能
比较上述两种实现方式,实际上是有区别的: 定时器方式,第一次触发并不会执行fn,但中止触发以后,还会再次执行一次fn 时间戳方式,第一次触发会执行fn,中止触发后,不会再次执行一次fn函数
两种方式是能够互补的,能够将其结合起来,即能第一次触发会执行fn,又能在中止触发后,再次执行一次fn:post
function throttle(fn, delay) {
let last = 0;
let timer = null;
return function() {
const context = this;
const args = arguments;
const now = +new Date();
const offset = now - last;
if (offset > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
last = now;
fn.apply(context, args);
}
else if (!timer) {
timer = setTimeout(() => {
last = +new Date();
timer = null;
fn.apply(context, args);
}, delay - offset);
}
};
}
复制代码
匿名函数内有个if...else,第一个是判断时间戳,第二个是判判定时器,对比下前面两种实现方式。 首先是时间戳方式的简易版:性能
if (offset > delay) {
last = now;
fn.apply(context, args);
}
复制代码
混合版:
if (offset > delay) {
if (timer) { // 注意这里
clearTimeout(timer);
timer = null;
}
last = now;
fn.apply(context, args);
}
复制代码
能够发现,混合版比简易版多了对timer不为null的判断,并清除了定时器、将timer置为null。 再是定时器实现方式的简易版:
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
}
复制代码
混合版:
else if (!timer) {
timer = setTimeout(() => {
last = +new Date(); // 注意这里
timer = null;
fn.apply(context, args);
}, delay - offset);
}
复制代码
能够看到,混合版比简易版多了对last变量的重置,而last变量是时间戳实现方式中判断的重要因素。这里要注意下,由于是在定时器的回调中,因此last的重置值要从新获取当前时间戳,而不能使用变量now。
经过以上对比,咱们能够发现,混合版是综合了两种不一样实现方式的做用,但除去开始和结束阶段的不一样,二者的共同做用是一致的--执行fn函数。因此,同一个时刻,执行fn函数的语句只能存在一个!在混合版的实现中,时间戳判断里,去除了定时器的影响,定时器判断里,去除了时间戳的影响。
对于当即执行和中止触发后的再次执行,咱们能够经过参数来控制,适应需求的变化。 假设规定{ immediate: false } 阻止当即执行,{ trailing: false } 阻止中止触发后的再次触发:
function throttle(fn, delay, options = {}) {
let timer = null;
let last = 0;
return function() {
const context = this;
const args = arguments;
const now = +new Date();
if (last === 0 && options.immediate === false) { // 这个条件语句是新增的
last = now;
}
const offset = now - last;
if (offset > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
last = now;
fn.apply(context, args);
}
else if (!timer && options.trailing !== false) { // options.trailing !== false 是新增的
timer = setTimeout(() => {
last = options.immediate === false ? 0 : +new Date();;
timer = null;
fn.apply(context, args);
}, delay - offset);
}
};
}
复制代码
相对于混合版,除了新增了一个参数options,其它不一样之处已在代码中标明。 思考下,当即执行是时间戳方式实现的,那么想要阻止当即执行的话,只要阻止第一次触发时,offset > delay 条件的成立就好了!如何判断是第一次触发?last变量只有初始化时,值才会是0,再加上咱们手动传入的参数,阻止当即执行的条件就知足了:
if (last === 0 && options.immediate === false) {
last = now;
}
复制代码
条件知足后,咱们重置last变量的初始值为当前时间戳,那么第一次 offset > delay 就不会成立了! 而后想阻止中止触发后的再次执行,仔细一想,要是不须要这个功能的话,时间戳的实现不就能够知足了?对!咱们只要变相地去除定时器就行了:
!timer && options.trailing !== false
复制代码
若是咱们不手动传入{ trailing: false } ,这个条件是永远不会成立的,即定时器永远不会开启。
不过有个问题在于,immediate和trailing不能同时设置为false,缘由在于,{ trailing: false } 的话,中止触发后不会再次执行,而后关键的last变量也就不会被重置为0,下一次再次触发又会当即执行,这样就有冲突了。