认识节流throttle函数
-
我们来理解一下节流的过程
-
当事件触发时,会执行这个事件的响应函数
-
如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数
-
不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的
-
-
节流的应用场景:
- 监听页面的滚动事件
- 鼠标移动事件
- 用户频繁点击按钮操作
- 游戏中的一些设计
节流函数的应用场景
-
很多人都玩过类似于
王者荣耀或者LOL
- 当玩
ADC
时,哪怕你疯狂点平A,射手的射速与你点击的频率没有太大关系 - 因为游戏中它们是由自己的
攻速阈值
的,那么这种攻速阈值带来的效果就是节流
- 当玩
-
很多人也都玩过类似于飞机大战的游戏
-
在飞机大战的游戏中,我们按下空格会发射一个子弹:
- 很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射
- 比如1秒钟只能发射一次,即使用户在这1秒钟按下了10次,子弹会保持发射一颗的频率来发射
- 但是事件是触发了10次的,响应的函数只触发了一次
Underscore 库的介绍
- 事实上我们可以通过一些第三方库来实现防抖操作:
- lodash
- underscore
- 这里使用underscore
- 我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多;
- 但是目前我看到underscore还在维护,lodash已经很久没有更新了;
- Underscore的官网: underscorejs.org/
- Underscore的安装有很多种方式:
- 下载Underscore,本地引入;
- 通过CDN直接引入;
- 通过包管理工具(npm)管理安装;
- 这里我们直接通过CDN:
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
HTML
<input type="text">
js
代码测试
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function (event) {
console.log(`发送了第${++counter}次网络请求`, this, event)
}
inputEl.oninput = _.throttle(inputChange, 1000)
throttle v1 基本实现
- 这里实现节流函数思路是采用
时间戳
的方式- 采用
lastTime
来记录每次执行的上一次函数触发的开始时间,默认为0
- 然后通过
传入的时间间隔与当前触发的时间以及上一次触发的开始的时间, 计算出还剩余多长事件需要去触发函数
- 最后触发函数时, 会将
当前触发的时间赋值给用来保存上次触发事件的变量
,实现节流
效果
- 采用
function throttle(fn, interval) {
// 1.记录上一次的开始时间
let lastTime = 0
// 2.事件触发时, 执行的函数
const _throttle = function () {
// 2.1 获取当前事件触发时的时间
const nowTime = +new Date()
// 2.2 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 2.3 真正触发的函数
fn()
// 2.4 保留上次触发的事件
lastTime = nowTime
}
}
return _throttle
}
- 那么这就是
节流函数
的基本实现,接下来还会增加一些功能
throttle v2 leading实现
- 实现思路就是传入一个对象,对象控制两个变量
leading
与trailing
leading
控制第一次是否执行trailing
控制最后一次是否执行
- 这里先实现
leading
功能,那么leading
这里的默认值我设置了true
,也就是默认第一次就执行 - 就下了就涉及到一个小算法,就是当
leading
为true
时,那么将nowTime
赋值给lastTime
- 就比如当前触发开始时间(nowTime)是
1000000
,那么将上一次触发开始时间(lastTime)等于1000000
- 在执行
nowTime - lastTime
时,结果必然为0
,那么就满足下面的if
判断了,就会执行函数了!
- 就比如当前触发开始时间(nowTime)是
- 但还要一点要注意,立即执行的前提必然是每次节流函数的第一次生效,所以还需判断
lastTime
是否为0
,当lastTime === 0
代表是第一次执行,才会进行上面leading
的判断
function throttle(fn, interval, options = { leading: true, trailing: false}) {
// 1.1 控制第一次与最后一次是否执行
const { leading, trailing } = options
// 1.2 记录上一次的开始时间
let lastTime = 0
// 2.事件触发时, 执行的函数
const _throttle = function () {
// 2.1 获取当前事件触发时的时间
const nowTime = +new Date()
// 2.2 如果 lastTime为0 并且 leading为false 将nowTime赋值给lastTime
if(!lastTime && !leading) lastTime = nowTime
// 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
// 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 2.4 真正触发的函数
fn()
// 2.5 保留上次触发事件的时间戳
lastTime = nowTime
}
}
return _throttle
}
- 这里先进行代码测试
HTML
<input type="text">
JS代码
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function (event) {
console.log(`发送了第${++counter}次网络请求`, this, event);
}
// leading这里默认就是true,所以我们测试 false,那么第一次不会即立即触发执
inputEl.oninput = throttle(inputChange, 1000, { leading: false })
throttle v3 traling实现
-
这里
traling
默认为false
,也就是默认最后一次不会执行 -
接下来判断如果
trailing
为true
并且没有执行过定时器时:- 开启定时器并将定时器编号给timer,方便取消定时器
- 执行定时器时,再将timer初始化
- 判断
leading
为true
时,lastTime
就需要重新获取时间
-
比如
interval
是为10s时- 触发事件也是第
10s
时,就会执行if (remainTime <= 0)
里面的代码,然后会终止执行
- 触发事件也是第
-
触发事件的时间大概率不会刚好与
interval
相等,后面很有可能会有ms
的,就比如触发事件是10.1s(10100ms)
时- 首先
10 - (10.1 - 0)
是小于0
的,那么if (remainTime <= 0)
里面的代码依旧会执行 - 但此时
remainTime
是为-0.1s
的,所以还会根据trailing :true
进入函数,再进行最后一次的回调 - 那么也就是说,会重复执行两次函数
- 首先
-
那怎么解决呢?
- 就是进行一个判断,当
trailing
为true
时 lastTime
就通过new Date()
重新获取下当前的时间戳并赋值,意味着用interval - (nowTime - lastTime)
重新求出remain
值作为setTimeout
延迟时间- 否则的话就初始化变量为0
- 就是进行一个判断,当
function throttle(fn, interval, options = { leading: true, trailing: false}) {
// 1.1 控制第一次与最好一次是否执行
let { leading, trailing } = options
// 1.2 记录上一次的开始时间
let lastTime = 0
// 1.3 用于判断是否存在定时器
let timer = null
// 2.事件触发时, 执行的函数
const _throttle = function () {
// 2.1 获取当前事件触发时的时间
const nowTime = +new Date()
// 2.2 如果 lastTime为0 并且 leading为false 将nowTime赋值给lastTime
if (!lastTime && !leading) lastTime = nowTime
// 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
// 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 3.1 如果有定时器就取消定时器并初始化timer
if (timer) {
clearTimeout(timer)
timer = null
}
// 2.4 真正触发的函数
fn()
// 2.5 保留上次触发的事件 并终止代码不执行定时器
return lastTime = nowTime
}
// 3.如果trailing为true 并且 没有定时器执行下面代码
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null
// 3.2 需要重新获取时间
lastTime = +new Date()
fn()
}, remainTime)
}
}
return _throttle
}
- 这里先进行代码测试
HTML
<input type="text">
JS代码
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function (event) {
console.log(`发送了第${++counter}次网络请求`, this, event);
}
// 这里将trailing设为true 最后一次会调用
inputEl.oninput = throttle(inputChange, 1000, { leading: false, trailing: true })
throttle v4 this参数
this
的写法与防抖
写法一样,可以用apply 或 call
都可以bind不行吗?可以的,在后面加括号调用也可
,这里我依旧采用apply
function throttle(fn, interval, options = { leading: true, trailing: false}) {
// 1.1 控制第一次与最好一次是否执行
let { leading, trailing } = options
// 1.2 记录上一次的开始时间
let lastTime = 0
// 1.3 用于判断是否存在定时器
let timer = null
// 2.事件触发时, 执行的函数
const _throttle = function (...args) {
// 2.1 获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 2.2 如果 lastTime为0 并且 leading为false 将nowTime赋值给lastTime
if (!lastTime && !leading) lastTime = nowTime
// 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
// 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 3.1 如果有定时器就取消定时器并初始化timer
if (timer) {
clearTimeout(timer)
timer = null
}
// 2.4 真正触发的函数 并传入this 与 参数
fn.apply(this, args)
// 2.5 保留上次触发的事件 并终止代码不执行定时器
return lastTime = nowTime
}
// 3.如果trailing为true 并且 没有定时器执行下面代码
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null
// 3.2 需要重新获取时间
lastTime = +new Date()
// 3.3 传入this 与 参数
fn.apply(this, args)
}, remainTime)
}
}
return _throttle
}
- 测试代码用上面的就行
throttle v5 取消功能
- 这也是比较简单的,取消定时器就可以了,那么可能会有疑问了,如果
traling
为false
怎么要取消呢? - 其实这里针对
traling
功能就好了,因为如果在traling
为false
的情况下,你输入的时间小于节流函数的interval
本质上也不会执行 - 所以针对
traling
为true
时,进行取消是没有问题的
function throttle(fn, interval, options = { leading: true, trailing: false}) {
// 1.1 控制第一次与最好一次是否执行
let { leading, trailing } = options
// 1.2 记录上一次的开始时间
let lastTime = 0
// 1.3 用于判断是否存在定时器
let timer = null
// 2.事件触发时, 执行的函数
const _throttle = function (...args) {
// 2.1 获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 2.2 如果 lastTime为0 并且 leading为false 将nowTime赋值给lastTime
if (!lastTime && !leading) lastTime = nowTime
// 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
// 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 3.1 如果有定时器就取消定时器并初始化timer
if (timer) {
clearTimeout(timer)
timer = null
}
// 2.4 真正触发的函数 并传入this 与 参数
fn.apply(this, args)
// 2.5 保留上次触发的事件 并终止代码不执行定时器
return lastTime = nowTime
}
// 3.如果trailing为true 并且 没有定时器执行下面代码
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null
// 3.2 需要重新获取时间
lastTime = +new Date()
// 3.3 传入this 与 参数
fn.apply(this, args)
}, remainTime)
}
}
// 4.取消功能
_throttle.cancel = function () {
if (timer) clearTimeout(timer)
// 4.1 取消代表整个函数终结了, 那么建议初始化变量
timer = null
lastTime = 0
}
return _throttle
}
- 这里也可以进行代码测试
HTML
<input type="text">
<button id="cancel">取消</button>
JS代码
const inputEl = document.querySelector("input")
const btnEl = document.querySelector("button")
let counter = 0
const inputChange = function (event) {
console.log(`发送了第${++counter}次网络请求`, this, event);
}
// 拿到throttle返回值
const _throttle = throttle(inputChange, 1000, { leading: false, trailing: true })
// 这种写法与之前的写法没有区别 都是调用 _throttle
inputEl.oninput = _throttle
// 点击按钮取消
btnEl.onclick = () => _throttle.cancel()
throttle v6 函数返回值
思路一:回调函数
- 通过外界传入函数回调形式返回返回值
function throttle(fn, interval, options = { leading: true, trailing: false}) {
// 1.1 控制第一次与最好一次是否执行
let { leading, trailing, resultCallback } = options
// 1.2 记录上一次的开始时间
let lastTime = 0
// 1.3 用于判断是否存在定时器
let timer = null
// 2.事件触发时, 执行的函数
const _throttle = function (...args) {
// 2.1 获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 2.2 如果 lastTime为0 并且 leading为false 将nowTime赋值给lastTime
if (!lastTime && !leading) lastTime = nowTime
// 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
// 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 3.1 如果有定时器就取消定时器并初始化timer
if (timer) {
clearTimeout(timer)
timer = null
}
// 2.4 真正触发的函数 并传入this 与 参数 再拿到其返回值
const result = fn.apply(this, args)
// 2.5 判断resultCallback是否传入并是否为函数 再将返回值传入回调函数
if(resultCallback && typeof resultCallback === 'function') resultCallback(result)
// 2.6 保留上次触发的事件 并终止代码不执行定时器
return lastTime = nowTime
}
// 3.如果trailing为true 并且 没有定时器执行下面代码
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null
// 3.2 需要重新获取时间
lastTime = +new Date()
// 3.3 传入this 与 参数 并拿到其返回值
const result = fn.apply(this, args)
// 3.4 判断resultCallback是否传入并是否为函数 再将返回值传入回调函数
if(resultCallback && typeof resultCallback === 'function') resultCallback(result)
}, remainTime)
}
}
// 4.取消功能
_throttle.cancel = function () {
if (timer) clearTimeout(timer)
// 4.1 取消代表整个函数终结了, 那么建议初始化变量
timer = null
lastTime = 0
}
return _throttle
}
思路二:Promise
- 通过返回
Promise
外部进行then
方法调用来获取返回值
function throttle(fn, interval, options = { leading: true, trailing: false}) {
// 1.1 控制第一次与最好一次是否执行
let { leading, trailing, resultCallback } = options
// 1.2 记录上一次的开始时间
let lastTime = 0
// 1.3 用于判断是否存在定时器
let timer = null
// 2.事件触发时, 执行的函数
const _throttle = function (...args) {
return new Promise((resolve, reject) => {
// 2.1 获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 2.2 如果 lastTime为0 并且 leading为false 将nowTime赋值给lastTime
if (!lastTime && !leading) lastTime = nowTime
// 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
// 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 3.1 如果有定时器就取消定时器并初始化timer
if (timer) {
clearTimeout(timer)
timer = null
}
// 2.4 真正触发的函数 并传入this 与 参数 再拿到其返回值
const result = fn.apply(this, args)
// 2.5 判断resultCallback是否传入并是否为函数 再将返回值传入回调函数
try {
if(resultCallback && typeof resultCallback === 'function') resolve(result)
} catch (err) {
reject(err)
}
// 2.6 保留上次触发的事件 并终止代码不执行定时器
return lastTime = nowTime
}
// 3.如果trailing为true 并且 没有定时器执行下面代码
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null
// 3.2 需要重新获取时间
lastTime = +new Date()
// 3.3 传入this 与 参数 并拿到其返回值
const result = fn.apply(this, args)
// 3.4 判断resultCallback是否传入并是否为函数 再将返回值传入回调函数
try {
if(resultCallback && typeof resultCallback === 'function') resolve(result)
} catch (err) {
reject(err)
}
}, remainTime)
}
})
}
// 4.取消功能
_throttle.cancel = function () {
if (timer) clearTimeout(timer)
// 4.1 取消代表整个函数终结了, 那么建议初始化变量
timer = null
lastTime = 0
}
return _throttle
}
- 这里我依旧使用代码对
Promise实现的返回值功能
进行简单测试: HTML测试代码
<input type="text">
<button id="cancel">取消</button>
js测试代码
const inputEl = document.querySelector("input")
const btnEl = document.querySelector("button")
let counter = 0
const inputChange = function (event) {
console.log(`发送了第${++counter}次网络请求`, this, event)
// 返回值:返回 0-99 随机一个s
return ~~(Math.random() * 100)
}
// 拿到throttle返回值
const _throttle = throttle(inputChange, 1000, {
leading: false,
trailing: false,
resultCallback() { }
})
// 通过临时函数获得_throttle返回值 -> Promsie 通过then方法拿到返回值
const tempCallback = function () {
const res = _throttle().then(res => {
console.log("Promise的返回值结果:", res)
})
}
// 调用tempCallback函数这种写法与之前的写法没有区别 也都是调用 _throttle
inputEl.oninput = tempCallback
// 点击按钮取消
btnEl.onclick = () => _throttle.cancel()