节流与防抖的作用都是防止函数多次调用。 区别在于,假如用户一直触发这个函数,且每次触发函数的间隔小于阙值,防抖的情况下只会调用一次,而节流会每隔一定时间调用函数。 函数防抖 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
我理解的防抖是用户停止操作后在n秒后执行一次,而节流是不管函数调用几次都是按照n秒执行一次的规律执行,直到用户操作结束;我主观理解是,防抖像直接将水龙头关闭,节流将水龙头调小
一、防抖函数 : 用户停止操作n秒后函数执行一次
1、基础版本
function debounce(fn, delay) {
// 1.定义一个定时器, 保存上一次的定时器
let timer = null
// 2.执行真正的定时器
const _debounce = (...args) => {
// 取消上一次定时器
if(timer) clearTimeout(timer)
// 延迟执行
timer = setTimeout(() => {
// 外部传入真正要执行的函数
fn.apply(this, args)
}, delay)
}
return _deboune
}
在html中调用
<script src="./01_debounce-V1-基本实现.js"></script>
<script>
const inputEl = document.querySelector('input')
let count = 0
function demo(event){
count++;
console.log(`发送了${count}次`);
}
// 给输入框绑定input事件,发送请求
inputEl.addEventListener("input", debounce(demo, 2000))
</script>
这是一个基础版的防抖函数,利用定时器设置n秒后执行,根据timer是否有值判断用户是否停止输入,如果有值取消定时器,如果没值说明用户已经停止操作,执行定时器
考虑函数this指向和参数传递问题,所以我们用apply来对函数进行绑定this和传参,你当时学的时候这里疑惑了一下,现在做笔记特地提出一下,之所以_debounce能够传递参数是因为它是在input事件调用的,事件函数会默认传递一个event,而真正需要执行的函数又在其内部执行,所以能够进行传参,是_debounce传demo,不是demo传_debounce
function debounce(fn, delay, immediate = false, resultCallBack) {
// 1.定义一个定时器, 保存上一次的定时器
let timer = null
let isInvoke = false
const _debounce = function(...args){
return new Promise((resovle, reject) => {
// 取消上一次定时器
if(timer) clearTimeout(timer)
// 延迟执行
if(immediate && !isInvoke) {
const result = fn.apply(this, args)
if(resultCallBack && typeof resultCallBack === 'function') resultCallBack(result)
try {
resovle(result)
} catch (error) {
reject(error)
}
isInvoke = true
} else {
timer = setTimeout(() => {
// 外部传入真正要执行的函数
const result = fn.apply(this, args)
if(resultCallBack && typeof resultCallBack === 'function') resultCallBack(result)
try {
resovle(result)
} catch (error) {
reject(error)
}
isInvoke = false
timer = null
}, delay)
}
})
}
// 封装取消功能
_debounce.cancel = function() {
if (timer) {
clearTimeout(timer)
console.log("我执行了~",timer);
}
timer = null
isInvoke = false
}
return _debounce
}
在html中引用
<script src="./04_debounce-V4-函数返回值.js"></script>
<script>
const inputEl = document.querySelector('input')
const btnEL = document.querySelector('button')
let count = 0
function demo(event){
count++;
console.log(`发送了${count}次`,this,event);
return `我是返回值${count},你知道吗~`
}
// 第一种接收参数的方案:用回调函数接收参数
const debounceChang = debounce(demo,1000,false,(e) => {
console.log("拿到真正执行函数的返回值:",e);
})
// 接收返回值的第二种方案,用promise
const tempCallBack = (...args) => {
debounceChang.apply(inputEl, args).then((res) => {
console.log("Promise拿到的结果:",res);
})
}
// 给输入框绑定input事件,发送请求
inputEl.addEventListener("input", tempCallBack)
// 给按钮绑定点击事件,取消请求
btnEL.addEventListener("click", function (){
debounceChang.cancel()
})
</script>
那么第二个就是是重磅升级版本,考虑了更多情况,并且增加了第一次是否执行、接收返回值、取消等功能;
2、立即执行
通过接收第三个参数判断第一次是否执行
// 给输入框绑定input事件,发送请求
inputEl.addEventListener("input", debounce(demo, 2000, true))
inputEl.addEventListener("input", debounce(需要执行的函数, 间隔时间, 是否立即执行))
给第一次是否执行参数一个默认值,那么在调用时便可以选择不传,我们声明一个变量isInvoke用来和immediate参数一起判断是否需要立即执行,这样防止了直接修改函数的参数
function debounce(fn, delay, immediate = false) {
// 1.定义一个定时器, 保存上一次的定时器
let timer = null
let isInvoke = false
// 2.执行真正的定时器
return (...args) => {
// 取消上一次定时器
if(timer) clearTimeout(timer)
// 延迟执行
if(immediate && !isInvoke) {
fn.apply(this, args)
isInvoke = true
} else {
timer = setTimeout(() => {
// 外部传入真正要执行的函数
fn.apply(this, args)
isInvoke = false
timer = null
}, delay)
}
}
}
3、接收返回值
那么第四个参数是用来接收函数返回值的,通过回调函数传参的方式进行接收
const result = fn.apply(this, args)
if(resultCallBack && typeof resultCallBack === 'function') resultCallBack(result)
还有第二种接收返回值的方案,用Promise,只是使用该方法后,调用时会稍麻烦一点
const result = fn.apply(this, args)
try {
resovle(result)
} catch (error) {
reject(error)
}
需要在使用时额外声明一个回调函数tempCallBack,用来执行then方法
const debounceChang = debounce(demo,1000,false,(e) => {
console.log("拿到真正执行函数的返回值:",e);
})
// 接收返回值的第二种方案,用promise
const tempCallBack = (...args) => {
debounceChang.apply(inputEl, args).then((res) => {
console.log("Promise拿到的结果:",res);
})
}
// 给输入框绑定input事件,发送请求
inputEl.addEventListener("input", tempCallBack)
4、取消功能
防抖函数的最后一个功能取消功能的实现原理是在_debounce函数对象上添加一个cancel方法,cancel方法实现取消定时器和变量恢复默认值
_debounce.cancel = function() {
if (timer) {
clearTimeout(timer)
console.log("我执行了~",timer);
}
timer = null
isInvoke = false
}
btnEL.addEventListener("click", function (){
debounceChang.cancel()
})
给按钮绑定点击事件,取消函数调用
btnEL.addEventListener("click", function (){
debounceChang.cancel()
})
二、节流函数 以n秒的频率规律的调用函数
在实现这个函数时需要考虑好实际情况,需要用到开始的时间lastTime、现在的时间nowTime、和设定需要间隔的时间interval这三个时间来准确控制,当然也可以简单粗暴的用定时器做一个简单版本的
function throttle(fn, interval) {
let timer = null
return (...args) => {
if(!timer) {
timer = setTimeout(() => {
fn.apply(this,args)
timer = null
},interval)
}
}
}
上面版本因为定时器在事件队列中属于宏任务,并不是立即执行的,所以有了第二版本
function throttle(fn, interval){
let lastTime = 0
const _throttle = function() {
const nowTime = new Date().getTime()
// 等待时间相差大于interval,只有经过设定时间nowTime - lastTime 才会大于 interval(设定间隔时间)
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 需要执行的函数
fn()
// 保留上次触发时间
lastTime = nowTime
}
}
return _throttle
}
<input type="text">
<script src="./08_throttle.js"></script>
<script>
const inputEl = document.querySelector('input')
let count = 0
function demo(event){
count++;
console.log(`发送了${count}次`,this,event);
return `我是返回值${count},你知道吗~`
}
inputEl.addEventListener("input", throttle(demo, 3000))
</script>
| 来源:王红元手作 |
当remainTime等于0或小于0时,执行函数,由于nowTime为时间戳不断地在增加,而第一次lastTime为0,所以会在开始时立即执行一次,执行一次之后lastTime被赋值为上一次的nowTime,等待interval时间过后,新的nowTime减lastTime会大于interval,所以函数再次执行,此时lastTime再次被赋值为这一刻的nowTime以此实现在同一频率下执行函数
当然了,这也不是完整版,完整版加入了leading、traling、接收函数返回值、取消功能;先再次贴出完整版,并对各个功能进行描述
// 剩余功能:this-参数、leading、traling、函数返回值、取消功能
function throttle(fn, interval, options){
// 记录上一次的开始时间
let lastTime = 0
let timer = null
const {leading, traling, callBack} = options
// 事件触发时,真正执行的函数
const _throttle = function(...args) {
return new Promise((resolve, reject) => {
// 获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 判断第一次是否需要执行
if(!lastTime && !leading) lastTime = nowTime
// 等待时间相差大于interval,只有经过设定时间nowTime - lastTime 才会大于 interval(设定间隔时间)
// 使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还剩余多长时间去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
if(timer){
clearTimeout(timer)
timer = null
}
// 真正触发的函数
const result = fn.apply(this, args)
// 返回值
if(callBack && typeof callBack === "function") callBack(result)
resolve(result)
// 保留上次触发时间
lastTime = nowTime
// 判断最后一次是否需要执行,并且判断timer是否已经有值,防止产生多个定时器
}else if(traling && !timer){
timer = setTimeout(() => {
const result = fn.apply(this, args)
// 返回值
if(callBack && typeof callBack === "function") callBack(result)
resolve(result)
timer = null
// 根据第一次是否执行进行赋值
lastTime = !leading ? 0 : new Date().getTime()
},remainTime)
}
})
}
_throttle.cancel = function() {
if(timer) clearTimeout(timer)
timer = null
lastTime = 0
}
return _throttle
}
<script src="./08_throttle.js"></script>
<script>
const inputEl = document.querySelector('input')
const btnEL = document.querySelector('button')
let count = 0
function demo(event){
count++;
console.log(`发送了${count}次`,this,event);
return `我是返回值${count},你知道吗~`
}
const throttleChang = throttle(demo, 3000, {
leading: false,
traling: true,
callBack: function(res){
console.log("callBack:",res);
}
})
const tempCallBack = (...args) => {
throttleChang.apply(inputEl, args).then((res) => {
console.log("Promise拿到的结果:",res);
})
}
inputEl.addEventListener("input", tempCallBack)
</script>
1、 控制第一次是否执行
leading为函数第一次是否执行,用户传入一个布尔值判断即可;基础版本之所以会立即执行是因为nowTime - lastTime远远的大于了interval,那么我们只需要nowTime与lastTime相等,即可实现执行函数的条件,因为传入参数过多,为了代码可读性,我们第三个参数用对象的方式来传参,并在执行时用对象结构赋值的方法取出来
function throttle(fn, interval, options){
let lastTime = 0
const {leading} = options
const _throttle = function() {
const nowTime = new Date().getTime()
if(!lastTime && !leading) lastTime = nowTime
// 等待时间相差大于interval,只有经过设定时间nowTime - lastTime 才会大于 interval(设定间隔时间)
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
fn()
// 保留上次触发时间
lastTime = nowTime
}
}
return _throttle
}
2、控制最后一次是否执行
traling的实现逻辑有些复杂,我现阶段能力还有限,可能还描述不正确,未来的你再看这篇文章时记得改正 traling参数也是个布尔值用来控制最后一次函数是否执行,首先采用的方法是定时器,在用户停止操作时执行,那么第一个问题就是为了预防添加多个定时器。我们需要一个timer变量来接收定时器,好在timer为空时才添加定时器,为了预防用户操作一段时间后再次操作,我们需要对lastTime赋值,这是就需要根据用户第一次是否立即执行来设置值。最后用户再次操作时防止定时器和一开始的判定条件同时执行,所以我们需要将timer定时器关闭并将timer置空
const _throttle = function(...args) {
// 获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 判断第一次是否需要执行
if(!lastTime && !leading) lastTime = nowTime
// 等待时间相差大于interval,只有经过设定时间nowTime - lastTime 才会大于 interval(设定间隔时间)
// 使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还剩余多长时间去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
if(timer){
clearTimeout(timer)
timer = null
}
// 真正触发的函数
const result = fn.apply(this, args)
// 保留上次触发时间
lastTime = nowTime
// 判断最后一次是否需要执行,并且判断timer是否已经有值,防止产生多个定时器
// 此例为3秒执行一次,防止在1秒2秒时创建多个定时器
}else if(traling && !timer){
timer = setTimeout(() => {
const result = fn.apply(this, args)
timer = null
// 根据第一次是否执行进行赋值
lastTime = !leading ? 0 : new Date().getTime()
},remainTime)
}
3、函数返回值
返回值的实现与防抖函数一致,第一种方法同样是用传参数的方式传入一个CallBack来接收执行函数的返回值,第二种方式依旧是Promise的方式便不再描述
4、取消功能
取消功能也与防抖函数一致,在_throttle函数对象上添加一个cancel方法,直接调用即可
最后的话 此篇文章是学习王红元老师的课程后,为了帮助自己捋顺思路而做的记录,也为了方便此后自己复习,愿自己砥砺前行、如愿以偿!