# 节流和防抖
函数节流和函数防抖都是「闭包」、「高阶函数」的应用,他们的目的都是优化短时间内同一函数频繁被调用的场景。比如页面滚动时执行某个函数,可以使用节流函数来降低函数被触发等频次,input 输入可以使用防抖函数,只在用户输入结束时执行函数等。
# 节流
# 定义
函数节流指的是在一定时间间隔内(例如 3 秒)某个函数被重复调用时只执行一次,在这 3 秒内无视后来产生的函数调用请求。
# 实现思路
第一种:用时间戳来判断是否已到执行时间,记录上次执行的时间戳,然后每次触发事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经达到时间差,如果是则执行,并更新上次执行的时间戳,如此循环。
第二种:使用定时器,函数第一次被调用时设置一个定时器 timer=setTimeout()
,此后每次重复触发函数,判断如果已经存在定时器,则直接 return
;直到定时器触发,清除定时器 timer
。之后函数再次执行时此时 timer
为 null
,则可以重新设置定时器。
# 手写
# 第一版
- 使用时间戳,首次触发事件立即执行,之后触发如果计时小于等待时间则不执行,反之执行
- 特点是每次触发都是立即执行的,然后等待
const throttle = function(fn, wait = 50) {
var prev = 0; // 初始时间值为0,第一次始终立即执行
return function(...argus) {
var now = new Date().getTime();
if (now - prev > wait) {
prev = now;
// 在实际使用过程中,return的这个函数是先被执行的,他的this指向了执行环境上下文
// 而fn这个真正的回调函数需要把this重新指向执行环境上下文
// 尤其是作为DOM事件的回调时,fn中的this需要指向DOM对象本身
// 这一点也和HOC是一样的道理
fn.apply(this, argus);
}
};
};
# 第二版
- 使用定时器。首次触发时设置一个定时器延迟执行函数,之后触发,如果有定时器就不执行,没有就再设一个定时器延迟执行函数。
- 他的特点是每次触发都是先等待,在延时之后执行的。
const throttle = function(fn, wait = 50) {
var timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(function() {
timer = null;
fn.call(this, args);
}, wait);
}
};
};
# 第三版
- 节流开始时和结束时都执行一次
const throttle = function(fn, wait = 50) {
var prev = 0;
var timer = null;
function throttled(...args) {
var now = new Date().getTime();
var remaining = wait - (now - prev);
if (remaining <= 0 || remaining > wait) {
if (timer) {
clearTimeout(timer);
timer = null;
}
prev = now;
fn.call(this, args);
} else if (!timer) {
timer = setTimeout(function() {
prev = now;
timer = null;
fn.call(this, args);
}, remaining);
}
}
return throttled;
};
# 第四版
- 使用参数控制节流开始合结束时是否执行函数
function throttle(fn, wait, options) {
var prev = 0;
var timer = null;
if (!options) {
options = {};
}
if (options.leading === false && options.trailing === false) {
throw Error("Leading and Training can't be set to false at the same time");
}
function throttled(...args) {
var now = new Date().getTime();
if (!prev && options.leading === false) {
// options.leading === false 表示节流开始时不执行,那么就只能是延时后执行
// !prev === true 表示函数没有执行过,即将首次执行
// 如果不是首次执行,且节流开始不执行,结束时执行,需要设置prev=now,
// 即剩余等待时间remaining=wait,需要等待wait这么长时间,达到延时执行的目的
prev = now;
}
var remaining = wait - (now - prev);
// 在节流等待过程的开始时刻执行一次回调
// 如果剩余等待时间小于等于0,即等待结束;或者修改了系统时间导致剩余时间比wait还大,都认为是可以触发事件执行
if (remaining <= 0 || remaining > wait) {
// 检查有没有timer,清除延时执行中的timer
if (timer) {
clearTimeout(timer);
timer = null;
}
prev = now;
// arguments是「函数参数」对象,MouseEvent是其中一个「参数对象」,args 相当于 [...arguments],值为[MouseEvent]
// 由于作为DOM事件的回调函数,只包含了event事件对象
// arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,
// 必须使用Array.prototype.slice.call先将其转为数组。
// rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。
fn.call(this, args);
// fn.call(this, arguments)
// fn.call(this, [...arguments])
} else if (!timer && options.trailing !== false) {
// 如果还有剩余时间需要等待,且没有timer,设置timer延时执行,在节流的结束时刻也执行一次
// 注意这里的等待时间是规定的等待时长的剩余时间remaining
// 且延时执行需要参数options.training不能为false,即允许延时执行,即在节流结束时执行
timer = setTimeout(function() {
prev = now;
timer = null;
fn.call(this, args);
}, remaining);
}
}
// cancel只能针对延时执行进行取消
throttled.cacel = function() {
clearTimeout(timer);
prev = 0;
timer = null;
};
return throttled;
}
# 防抖
防抖函数 debounce
被设计为:连续多次调用函数时,相邻的两次函数调用需要间隔超过指定的时间才能执行,否则不执行。
# 思路
实现原理就是利用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行。
# 手写
function debounce(func, await, immediate) {
var timer, result;
function debounced() {
// DOM对象的事件回调函数中的this指向DOM对象本身,以及默认回给回调函数传入事件对象参数Event
// debounce返回该函数,并赋值给DOM对象的事件回调函数,此时this就指向DOM对象,Event也在arguments中
var context = this;
var arg = arguments;
// 清除上一个定时器
if (timer) {
clearTimeout(timer);
}
// 如果需要立即执行,即先执行后等待
if (immediate) {
// 先执行,且上一次执行结束了才能进行下一次执行
if (!timer) {
result = func.apply(context, arg);
}
// 后等待
timer = setTimeout(function() {
// 等待结束,可以继续下一次执行
// 这里赋值null不用担心定时器没有释放的问题,因为此时定时器已经执行了,不是未执行。
timer = null;
}, await);
} else {
timer = setTimeout(function() {
// 异步时,执行func(),调用时的执行上下文是window对象,func内部的this回指向window
// 修正func的this指向,和默认参数,保持和不使用debounce时一致
result = func.apply(context, arg);
}, await);
}
// 事件回调函数func有可能是有返回值的,我们也将它return
// 注意,当immediate为false时,result的赋值是异步的,所以异步结束前,这里return的result仍然是undefined。
// 当immediate为false时,return出去的result始终都是undeined
// 当immediate为true时,因为是立即执行,result已经被赋值了,所以这里return的就是事件回调函数func实际的返回值。
return result;
}
// cancel方法可以立即取消当前这一次的debounce,当等待时间很长想提前取消时很有用
// 如果是立即执行,那么效果就是func已经执行了
// 如果不是立即执行,那么在setTimeout结束前调用cancel方法,func都不会被执行
debounced.cancel = function() {
clearTimeout(timer);
timer = null;
};
return debounced;
}