一、概念
- 防抖:对于快速连续触发执行的函数,通过某种方式让函数在一定时间内只执行一次,每次触发时重新计时
- 节流:对于快速连续触发执行的函数,通过某种方式让函数减少执行频次,只有上一次执行了之后才开始计算下一次执行时机
二、应用场景
- 防抖:滚轮滚动事件、input输入事件、窗口resize事件等,需要做一些处理的时候、
- 节流:连续触发事件时需要连续更新,但是又不需要太高频率的时候,比如根据页面滚动位置,计算进度条位置
三、防抖函数实现
根据上面的描述可知:
- 防抖函数需要接收一个事件执行函数,返回一个新的函数作为真正的执行函数;
- 返回的函数内部存在一个定时器,每次触发函数执行,将会重新生成定时器;
根据上述条件,可实现一个丐版
function debounce(func, time) {
let timer = null
function core(...args) {
const context = this
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
func.apply(context, args)
}, time)
}
return core
}
上面实现的结果是在连续触发执行后的某一个时刻执行,例如给滚动事件添加,则是在滚动结束后的某一时机执行绑定的函数,那如果想要控制执行时机呢?比如想要在刚开始触发时也可以执行
function debounce(func, time, immediate) {
let timer = null
let context = null
function run(args) {
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
// 定时器执行,说明这一连续触发结束,不是立即执行的,需要在这里执行绑定的回调
if(!immediate) {
func.apply(context, args)
}
// 事件结束,恢复标记
timer = null
}, time)
}
function core(...args) {
context = this
/**
* 如果是立即执行,则绑定的执行函数需要在第一次就执行,定时器中的不需要执行,只需要通过定时器来标记是不是第一次执行,定时器触发,则说明这一连续的事件结束了,重置timer,等待下一次的事件
* 如果不是立即执行,则正常走定时器逻辑,在最后一次触发时,执行绑定逻辑
*/
if(immediate) {
if(!timer) {
func.apply(context, args)
}
run(args)
} else {
run(args)
}
}
return core
}
四、节流函数实现
实现要点:
- 事件触发回调时,先判断是否需要立即执行,如果需要且为初次触发,则执行事件函数
- 如果在间隔时间内,事件回调再次触发,则创建定时器,待间隔时间到期后执行事件函数
- 连续触发时,如果触发事件间隔达到条件,需要执行事件函数,并重置定时器状态
- 重置计时开始时间时,需要判断配置项是否配置了开始节流前触发,重置为不同的初始值,以便下次触发事件执行时判断是否开始节流前执行绑定函数
function throttle(func, wait, options = {}) {
let previous = 0
let timer = null
let context = null
let args = null
function throttled(...rest) {
const now = Date.now()
let result = null
context = this
args = rest
// previous为0,表示是初次触发
if(previous === 0 && options.leading === false) {
previous = now
}
const remaining = wait - (now - previous)
if(remaining <=0 ) { // 此次执行,距离上次执行时间间隔到达触发条件
// 这里面会执行事件函数,不需要定时器内的延时执行,清理掉定时器
if(timer) {
clearTimeout(timer)
// 标记为null,那么下次再触发事件时,进入else分支后会重新创建定时器
timer = null
}
// 更新间隔起始时间
previous = now
result = func.apply(context, args)
// 这两个变量在定时器中有引用,需要手动释放内存,防止出现内存泄漏
context = null
args = null
} else if(!timer && options.trailing !== false) {
// 当前没有定时器,则需要创建一个延时执行事件函数
timer = setTimeout(() => {
timer = null
// 更新间隔起始时间,如果设置了开始节流前不执行事件函数,则这里置为0,那么下次触发执行时,会直接去创建定时器
previous = options.leading === false ? 0 : Date.now()
result = func.apply(context, args);
context = null
args = null
}, wait)
}
return result
}
// 手动清理掉节流
throttled.cancel = function() {
if(timer) {
clearTimeout(timer);
}
previous = 0;
timer = context = args = null;
}
return throttled
}