在前端开发的过程中,我们经常会遇到连续触发的事件,比如resize、scroll、keydown 等等,有些时候,比如触发事件会有Ajax请求,耗时的运算,页面渲染等,我们不希望在事件持续触发的过程中那么频繁地去执行函数。
一般来说,防抖和节流是比较好的解决方案。
tips: 防抖和节流都是利用了闭包来保持对函数内变量的持续引用
另外还需要注意的是 this 和 参数的传递
防抖就是在事件触发n毫秒后执行函数,如果在这期间再次触发则重新计算函数执行事件,即:连续的触发中只有最后一次才会执行函数
function debounce (func, wait) {
let timeout;
return function () {
/*
注意: setTimeout 使用了箭头函数,不需要
const context = this;
this指向当前上下文
*/
const args = arguments;
// 每次触发事件,都清除上次的计时器,生成新的计时器重新计算函数执行时间
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
/*
注意:apply 手动绑定this
如果不这样做,func中的this会指向函数声明位置的上下文
*/
func.apply(this, args);
}, wait);
};
}
立即执行版:
// leading = true 立即执行
function debounce (func, wait, leading) {
let timeout;
return function () {
const args = arguments;
if (timeout) clearTimeout(timeout);
if (leading) {
if (!timeout) func.apply(this, args);
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null;
}, wait);
} else {
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
}
};
}
节流是指连续触发的事件n毫秒内只执行1次,节流会稀释函数的执行频率
区别于防抖,防抖是将多次执行变为最后一次执行,节流是将多次执行变为每隔一段时间执行
节流一般有两种方式可以实现,分别是时间戳版和定时器版
时间戳版:
function throttle (func, wait) {
let prev = 0;
return function () {
const now = new Date();
const args = arguments;
if (now - prev > wait) {
func.apply(this, args);
prev = now;
}
};
}
定时器版:
function throttle (func, wait) {
let timeout;
return function () {
const args = arguments;
if (timeout) return;
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null;
}, wait);
};
}
我们可以看到时间戳版和定时器版的差别是:定时器第一次触发不会立刻执行,在一段时间的最后执行一次,有一个延迟的效果。时间戳版的是第一次立即执行,结尾没有执行。
单独用时间戳版和定时器版都有缺陷,我们更希望第一次触发马上执行函数,结尾也执行一次。且连续触发时,是每隔n毫秒触发一次的均匀的执行。
我们将间戳版和定时器结合一下,得到下面的试验版
function throttle(func, wait) {
let timeout;
let prev = 0;
return function () {
const args = arguments;
const now = new Date();
if ( now - prev > wait) {
func.apply(this, args);
prev = now
} else {
if(!timeout) {
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null;
prev = new Date() // 注意这里不能用prev = now 这是个闭包
}, wait)
}
}
}
}
很容易看出来,这样没有办法保证连续触发的情况下函数执行时间是连续的间隔n,应该给定制器设置一个剩余执行时间 remaining
function throttle (func, wait) {
let timeout;
let prev = 0;
return function () {
const args = arguments;
const now = new Date();
const remaining = wait - (now - prev); //剩余时间
/*
注意:不能写成 remaining <= 0
因为当remaining=0时恰好触发,会触发立即执行,
此时上次设置的定时器也刚好到了执行时间(假设setTimeout没有延迟的情况下),就会有2次连续执行
*/
if (remaining < 0) { // 第一次触发立即执行
func.apply(this, args);
prev = now;
} else {
if (!timeout) {
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null;
prev = new Date();
}, remaining);
}
}
};
}
这样就完美实现了,如果想用开关开控制
/*
func (Function): 要节流的函数。
[wait=0] (number): 需要节流的毫秒。
[options={}] (Object): 选项对象。
[options.leading=true] (boolean): 指定调用在节流开始前。
[options.trailing=true] (boolean): 指定调用在节流结束后。
*/
function throttle (func, wait, { leading = true, trailing = true } = {}) {
let timeout;
let prev = 0;
return function () {
const now = new Date();
const remaining = wait - (now - prev);
const args = arguments;
if (remaining < 0 && leading) {
func.apply(this, args);
prev = now;
} else if (!timeout && trailing) {
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null;
prev = new Date();
}, trailing ? (leading ? remaining : wait) : remaining);
}
};
}
有错误欢迎指出~