建议先去看我这篇博客带你一步步用Javascript简单实现和优化防抖1、什么是防抖 防抖(debounce):每次触发定时器后,取消上一个 - 掘金,再来看节流实现。
1、什么是节流
节流(throttle):每次触发定时器后,直到这个定时器结束之前无法再次触发。一般用于可预知的用户行为的优化,比如为scroll事件的回调函数添加定时器。
打个比方,相信大家都玩过王者荣耀或者英雄联盟等MOBA游戏,当我们攻速是一定的,不管我们点击普攻键有多快,发射的弹幕也是根据你的攻速来决定的。
假设攻速是一秒射一次,我们一秒钟点击了一万次普攻键(我嘞个手速大王),这个点击事件触发了一万次,但是我给你的响应只有一次。
节流在间隔一段时间执行一次回调的场景有:
1.滚动加载,加载更多或滚到底部监听
2.搜索框,搜索联想功能
函数初版
需要接受参数
- 参数1:要执行的回调函数
- 参数2:要执行的间隔时间
function myThrottle(fn, interval){
}
返回值
返回值为一个新的函数
function myThrottle(fn, interval){
const _throttle = function(){
}
return _throttle
}
实现逻辑: 如果要实现节流函数,利用定时器不太方便管理,可以用时间戳获取当前时间nowTime 参数开始时间 StartTime 和 等待时间waitTime,间隔时间 interval waitTIme = interval - (nowTime - startTime) 得到等待时间waitTime,对其进行判断,如果小于等于0,则可以执行回调函数fn 开始时间可以初始化为0,第一次执行时,waitTime一定是负值(因为nowTime很大),所以第一次执行节流函数,一定会立即执行
具体实现
function MyThrottle(fn, interval) {
let startTime = 0;
const _throttle = function () {
const nowTime = new Data().getTime()
const waitTime = interval - (nowTime - startTime)
if (waitTime <= 0) {
fn()
startTime = nowTime
}
}
return _throttle
}
代码解释
function MyThrottle(fn, interval) {
const nowTime = new Date().getTime();
//...
}
-
这里定义了一个名为
MyThrottle的函数,它接收两个参数:fn:是需要被节流控制调用频率的目标函数,也就是期望在一定时间间隔内限制其执行次数的那个函数。interval:代表时间间隔,单位应该是毫秒,用于指定在多长时间内只允许fn执行一次。
-
在函数内部,首先通过
new Date().getTime()获取了当前的时间戳(从 1970 年 1 月 1 日 00:00:00 UTC 到当前时刻的毫秒数)并赋值给nowTime,这个时间戳将用于后续和其他时间进行比较,来判断是否达到了可以再次执行目标函数的时间条件。
return _throttle;
MyThrottle函数最终返回了内部定义的_throttle函数,这样外部调用MyThrottle并传入相应参数后,得到的返回值(也就是_throttle函数)可以被赋值给一个变量,然后通过这个变量去调用,在每次调用时都会执行内部的节流逻辑,来决定是否实际执行传入的目标函数fn。
测试
const inputEl = document.querySelector("input")
// 3.自己实现的节流函数
let counter = 1
inputEl.oninput = MyThrottle(function() {
console.log(`发送网络请求${counter++}:`, this.value)
}, 1000)
优化this指向和参数
相信大家知道, 打印undefined是因为this指向问题, 在手写防抖函数相信已经知道如何优化了
下面我们优化一下this指向
function MyThrottle(fn, interval) {
let startTime = 0
const _throttle = function (...args) {
const nowTime = new Data().getTime()
const waitTime = interval - (nowTime - startTime)
if (waitTime <= 0) {
fn.apply(this,args)
startTime = nowTime
}
}
return _throttle
}
测试
const inputEl = document.querySelector("input")
// 3.自己实现的节流函数
let counter = 1
inputEl.oninput = MyThrottle(function(event) {
console.log(`发送网络请求${counter++}:`, this.value, event)
}, 1000)
优化控制立即执行
function MyThrottle(fn, interval,leading = true) {
let startTime = 0
const _throttle = function (...args) {
// 1.获取当前时间
const nowTime = new Data().getTime()
// 对立即执行进行控制
if(!leading && startTime === 0){
startTime = nowTime
}
// 2.计算需要等待的时间执行函数
const waitTime = interval - (nowTime - startTime)
if (waitTime <= 0) {
fn.apply(this,args)
startTime = nowTime
}
}
return _throttle
}
测试
const inputEl = document.querySelector("input")
// 3.自己实现的节流函数
let counter = 1
inputEl.oninput = MyThrottle(function(event) {
console.log(`发送网络请求${counter++}:`, this.value, event)
}, 1000)
优化尾部控制(了解即可)
function MyThrottle(fn, interval,{ leading = true, trailing = false } = {}) {
let startTime = 0
let timer = null
const _throttle = function (...args) {
// 1.获取当前时间
const nowTime = new Data().getTime()
// 对立即执行进行控制
if(!leading && startTime === 0){
startTime = nowTime
}
// 2.计算需要等待的时间执行函数
const waitTime = interval - (nowTime - startTime)
if (waitTime <= 0) {
if(timer) clearTimeout(timer)
fn.apply(this,args)
startTime = nowTime
timer = null
return
}
}
// 判断是否进行尾部控制
if(trailing && !timer) {
timer = setTimeout(() => {
fn.apply(this,args)
startTime = new Date().getTime()
timer = null
},waitTime)
}
return _throttle
}
代码解释
trailing:布尔值,默认值为false,用于决定在一个连续触发周期的末尾,如果距离上一次执行fn已经过了一部分时间间隔但还未完整经过interval这么长的时间,是否在时间间隔结束时执行一次fn函数。例如在持续触发某个事件但最后停止触发时,若trailing为true,会根据剩余时间判断是否再执行一次fn。timer:用于存储定时器的标识(在 JavaScript 中,setTimeout函数返回的一个定时器 ID),初始值设为null,它主要在涉及trailing(尾部执行控制)逻辑时发挥作用,用于判断是否已经存在等待执行的定时器以及后续取消或重置定时器等操作。- 判断
timer是否存在(即是否已经设置了用于trailing逻辑的定时器),如果存在就通过clearTimeout(timer)取消这个定时器,避免重复执行或者出现定时器混乱的情况。
if (trailing &&!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
startTime = new Date().getTime();
timer = null;
}, waitTime);
}
-
这部分代码实现了对
trailing参数所控制的尾部执行逻辑。通过if (trailing &&!timer)进行判断,如果trailing参数为true(即配置为允许在时间间隔末尾执行一次)且当前没有正在等待执行的定时器(timer为null),则会执行以下操作:- 创建一个定时器,使用
setTimeout函数,它会在waitTime毫秒后执行传入的回调函数。这个waitTime就是前面计算出来的距离时间间隔结束还需要等待的时间。 - 在定时器的回调函数中,首先使用
fn.apply(this, args)调用目标函数fn,同样通过apply方法保证this指向和参数传递的正确性;然后将startTime更新为当前的时间戳,意味着重新开始计时,准备下一轮的节流判断;最后将timer重置为null,表示这个定时器已经执行完毕,清除相关标识。
- 创建一个定时器,使用
注意:在尾部控制中startTime = new Date().getTime();不能写成startTime = nowTime;
1. 时间准确性与逻辑连贯性考虑
- 不同执行阶段的时间区分:
nowTime是在_throttle函数一开始就获取的当前时间,它主要用于和startTime配合来计算距离上一次执行fn函数后经过了多长时间,以此判断是否达到了可以再次执行fn的条件(通过waitTime = interval - (nowTime - startTime)这个计算)。而在尾部控制逻辑这里,我们所处的时间点已经是等待了一段时间(具体是waitTime所代表的时长)后,定时器触发执行回调函数的时刻了。如果直接用之前获取的nowTime赋值给startTime,就忽略了从上次判断到定时器触发这中间经过的时间,导致时间记录不准确,无法正确开启下一轮的节流计时逻辑。 - 保持逻辑连贯准确:在定时器回调函数中,当执行完
fn函数后,我们希望重新设置一个准确的起始时间来开启下一轮基于时间间隔的节流判断。通过startTime = new Date().getTime();获取当下这个定时器触发时刻的准确时间戳作为新的起始时间,能保证后续的节流计算(比如下一次再进入_throttle函数时计算waitTime等)是基于最新的、符合逻辑的时间起点,使得整个节流过程在时间上连贯且符合预期,不会出现时间错乱导致的函数执行频率失控问题。
2. 避免重复执行与意外情况干扰
- 防止重复执行干扰:假设我们直接使用
nowTime(之前获取的那个时间值)来赋值给startTime,如果在定时器触发前,又有新的触发事件导致_throttle函数被多次调用,nowTime的值就一直在变化,而且它并不能反映出定时器触发这个特定时刻的状态。那么在定时器最终触发时,用这个可能已经过时且不符合当下定时器触发场景的nowTime来更新startTime,很可能会扰乱后续的节流逻辑,甚至可能导致在短时间内错误地又满足了执行条件,让fn函数被重复执行,违背了节流的本意,即限制函数执行频率的功能就无法准确实现了。 - 应对异步等复杂情况:在实际应用中,有可能存在异步操作或者其他复杂情况影响函数的执行顺序和时间。比如在定时器等待期间,外部可能有其他代码在运行,或者
fn函数本身内部有异步逻辑还未执行完等情况。通过获取定时器触发时的实时时间戳来更新startTime,能够让节流逻辑自适应这些复杂情况,确保不管之前经历了怎样的过程,每次进入新的一轮节流周期都是从一个准确、合适的时间起点开始计算,避免受到之前各种不确定因素的干扰。
测试
const inputEl = document.querySelector("input")
// 3.自己实现的节流函数
let counter = 1
inputEl.oninput = MyThrottle(function(event) {
console.log(`发送网络请求${counter++}:`, this.value, event)
}, 3000, { trailing: true })
优化取消操作
这里与防抖的操作是一样的。
function MyThrottle(fn, interval,{ leading = true, trailing = false } = {}) {
let startTime = 0
let timer = null
const _throttle = function (...args) {
// 1.获取当前时间
const nowTime = new Data().getTime()
// 对立即执行进行控制
if(!leading && startTime === 0){
startTime = nowTime
}
// 2.计算需要等待的时间执行函数
const waitTime = interval - (nowTime - startTime)
if (waitTime <= 0) {
if(timer) clearTimeout(timer)
fn.apply(this,args)
startTime = nowTime
timer = null
return
}
}
// 判断是否进行尾部控制
if(trailing && !timer) {
timer = setTimeout(() => {
fn.apply(this,args)
startTime = new Date().getTime()
timer = null
},waitTime)
}
_throttle.cancel = function () {
if(timer) clearTimeout(timer)
startTime = 0
timer = null
}
return _throttle
}
测试
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector(".cancel")
// 3.自己实现的节流函数
let counter = 1
const throttleFn = MyThrottle(function(event) {
console.log(`发送网络请求${counter++}:`, this.value, event)
}, 3000, { trailing: true })
inputEl.oninput = throttleFn
cancelBtn.onclick = function() {
throttleFn.cancel()
}
优化获取返回值
这里与防抖的操作是一样的
function MyThrottle(fn, interval, { leading = true, trailing = false } = {}) {
let startTime = 0
let timer = null
const _throttle = function(...args) {
return new Promise((resolve, reject) => {
try {
// 1.获取当前时间
const nowTime = new Date().getTime()
// 对立即执行进行控制
if (!leading && startTime === 0) {
startTime = nowTime
}
// 2.计算需要等待的时间执行函数
const waitTime = interval - (nowTime - startTime)
if (waitTime <= 0) {
// console.log("执行操作fn")
if (timer) clearTimeout(timer)
const res = fn.apply(this, args)
resolve(res)
startTime = nowTime
timer = null
return
}
// 3.判断是否需要执行尾部
if (trailing && !timer) {
timer = setTimeout(() => {
// console.log("执行timer")
const res = fn.apply(this, args)
resolve(res)
startTime = new Date().getTime()
timer = null
}, waitTime);
}
} catch (error) {
reject(error)
}
})
}
_throttle.cancel = function() {
if (timer) clearTimeout(timer)
startTime = 0
timer = null
}
return _throttle
}
测试
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector(".cancel")
// 3.自己实现的节流函数
let counter = 1
const throttleFn = MyThrottle(function(event) {
console.log(`发送网络请求${counter++}:`, this.value, event)
return "throttle return value"
}, 3000, { trailing: true })
throttleFn("aaaa").then(res => {
console.log("res:", res)
})