# 认识节流 throttle 函数
-
我们来理解一下节流的过程
-
节流的应用场景:
- 监听页面的滚动事件
- 鼠标移动事件
- 用户频繁点击按钮操作
- 游戏中的一些设计
# 节流函数的应用场景
-
很多人都玩过类似于 王者荣耀或者LOL
- 当玩
ADC
时,哪怕你疯狂点平 A,射手的射速与你点击的频率没有太大关系
- 因为游戏中它们是由自己的
攻速阈值
的,那么这种攻速阈值带来的效果就是 节流
-
很多人也都玩过类似于飞机大战的游戏
-
在飞机大战的游戏中,我们按下空格会发射一个子弹:
- 很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射
- 比如 1 秒钟只能发射一次,即使用户在这 1 秒钟按下了 10 次,子弹会保持发射一颗的频率来发射
- 但是事件是触发了 10 次的,响应的函数只触发了一次
# Underscore 库的介绍
- 事实上我们可以通过一些第三方库来实现防抖操作:
- 这里使用 underscore
- 我们可以理解成 lodash 是 underscore 的升级版,它更重量级,功能也更多;
- 但是目前我看到 underscore 还在维护,lodash 已经很久没有更新了;
- Underscore 的官网: https://underscorejs.org/
- Underscore 的安装有很多种方式:
- 下载 Underscore,本地引入;
- 通过 CDN 直接引入;
- 通过包管理工具(npm)管理安装;
- 这里我们直接通过 CDN:
| <script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script> |
| 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) { |
| |
| let lastTime = 0 |
| |
| |
| const _throttle = function () { |
| |
| const nowTime = +new Date() |
| |
| |
| const remainTime = interval - (nowTime - lastTime) |
| |
| if (remainTime <= 0) { |
| |
| fn() |
| |
| |
| 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
判断了,就会执行函数了!
- 但还要一点要注意,立即执行的前提必然是每次节流函数的第一次生效,所以还需判断
lastTime
是否为 0
,当 lastTime === 0
代表是第一次执行,才会进行上面 leading
的判断
| function throttle(fn, interval, options = { leading: true, trailing: false}) { |
| |
| const { leading, trailing } = options |
| |
| let lastTime = 0 |
| |
| |
| const _throttle = function () { |
| |
| const nowTime = +new Date() |
| |
| |
| if(!lastTime && !leading) lastTime = nowTime |
| |
| |
| |
| const remainTime = interval - (nowTime - lastTime) |
| |
| if (remainTime <= 0) { |
| |
| fn() |
| |
| |
| lastTime = nowTime |
| } |
| } |
| return _throttle |
| } |
| const inputEl = document.querySelector("input") |
| |
| let counter = 0 |
| const inputChange = function (event) { |
| console.log(`发送了第${++counter}次网络请求`, this, event); |
| } |
| |
| |
| 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}) { |
| |
| let { leading, trailing } = options |
| |
| |
| let lastTime = 0 |
| |
| |
| let timer = null |
| |
| |
| const _throttle = function () { |
| |
| const nowTime = +new Date() |
| |
| |
| if (!lastTime && !leading) lastTime = nowTime |
| |
| |
| |
| const remainTime = interval - (nowTime - lastTime) |
| |
| if (remainTime <= 0) { |
| |
| if (timer) { |
| clearTimeout(timer) |
| timer = null |
| } |
| |
| |
| fn() |
| |
| |
| return lastTime = nowTime |
| } |
| |
| |
| if (trailing && !timer) { |
| timer = setTimeout(() => { |
| timer = null |
| |
| |
| lastTime = +new Date() |
| |
| fn() |
| }, remainTime) |
| } |
| } |
| |
| return _throttle |
| } |
| const inputEl = document.querySelector("input") |
| |
| let counter = 0 |
| const inputChange = function (event) { |
| console.log(`发送了第${++counter}次网络请求`, this, event); |
| } |
| |
| 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}) { |
| |
| let { leading, trailing } = options |
| |
| |
| let lastTime = 0 |
| |
| |
| let timer = null |
| |
| |
| const _throttle = function (...args) { |
| |
| const nowTime = new Date().getTime() |
| |
| |
| if (!lastTime && !leading) lastTime = nowTime |
| |
| |
| |
| const remainTime = interval - (nowTime - lastTime) |
| |
| if (remainTime <= 0) { |
| |
| if (timer) { |
| clearTimeout(timer) |
| timer = null |
| } |
| |
| |
| fn.apply(this, args) |
| |
| |
| return lastTime = nowTime |
| } |
| |
| |
| if (trailing && !timer) { |
| timer = setTimeout(() => { |
| timer = null |
| |
| |
| lastTime = +new Date() |
| |
| |
| 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}) { |
| |
| let { leading, trailing } = options |
| |
| |
| let lastTime = 0 |
| |
| |
| let timer = null |
| |
| |
| const _throttle = function (...args) { |
| |
| const nowTime = new Date().getTime() |
| |
| |
| if (!lastTime && !leading) lastTime = nowTime |
| |
| |
| |
| const remainTime = interval - (nowTime - lastTime) |
| |
| if (remainTime <= 0) { |
| |
| if (timer) { |
| clearTimeout(timer) |
| timer = null |
| } |
| |
| |
| fn.apply(this, args) |
| |
| |
| return lastTime = nowTime |
| } |
| |
| |
| if (trailing && !timer) { |
| timer = setTimeout(() => { |
| timer = null |
| |
| |
| lastTime = +new Date() |
| |
| |
| fn.apply(this, args) |
| }, remainTime) |
| } |
| } |
| |
| |
| _throttle.cancel = function () { |
| if (timer) clearTimeout(timer) |
| |
| |
| timer = null |
| lastTime = 0 |
| } |
| |
| return _throttle |
| } |
| <input type="text"> |
| <button id="cancel">取消</button> |
| const inputEl = document.querySelector("input") |
| const btnEl = document.querySelector("button") |
| let counter = 0 |
| const inputChange = function (event) { |
| console.log(`发送了第${++counter}次网络请求`, this, event); |
| } |
| |
| const _throttle = throttle(inputChange, 1000, { leading: false, trailing: true }) |
| |
| inputEl.oninput = _throttle |
| |
| btnEl.onclick = () => _throttle.cancel() |
# throttle v6 函数返回值
# 思路一:回调函数
| function throttle(fn, interval, options = { leading: true, trailing: false}) { |
| |
| let { leading, trailing, resultCallback } = options |
| |
| |
| let lastTime = 0 |
| |
| |
| let timer = null |
| |
| |
| const _throttle = function (...args) { |
| |
| const nowTime = new Date().getTime() |
| |
| |
| if (!lastTime && !leading) lastTime = nowTime |
| |
| |
| |
| const remainTime = interval - (nowTime - lastTime) |
| |
| if (remainTime <= 0) { |
| |
| if (timer) { |
| clearTimeout(timer) |
| timer = null |
| } |
| |
| |
| const result = fn.apply(this, args) |
| |
| |
| if(resultCallback && typeof resultCallback === 'function') resultCallback(result) |
| |
| |
| return lastTime = nowTime |
| } |
| |
| |
| if (trailing && !timer) { |
| timer = setTimeout(() => { |
| timer = null |
| |
| |
| lastTime = +new Date() |
| |
| |
| const result = fn.apply(this, args) |
| |
| |
| if(resultCallback && typeof resultCallback === 'function') resultCallback(result) |
| }, remainTime) |
| } |
| } |
| |
| |
| _throttle.cancel = function () { |
| if (timer) clearTimeout(timer) |
| |
| |
| timer = null |
| lastTime = 0 |
| } |
| |
| return _throttle |
| } |
# 思路二:Promise
- 通过返回
Promise
外部进行 then
方法调用来获取返回值
| function throttle(fn, interval, options = { leading: true, trailing: false}) { |
| |
| let { leading, trailing, resultCallback } = options |
| |
| |
| let lastTime = 0 |
| |
| |
| let timer = null |
| |
| |
| const _throttle = function (...args) { |
| return new Promise((resolve, reject) => { |
| |
| const nowTime = new Date().getTime() |
| |
| |
| if (!lastTime && !leading) lastTime = nowTime |
| |
| |
| |
| const remainTime = interval - (nowTime - lastTime) |
| |
| if (remainTime <= 0) { |
| |
| if (timer) { |
| clearTimeout(timer) |
| timer = null |
| } |
| |
| |
| const result = fn.apply(this, args) |
| |
| |
| try { |
| if(resultCallback && typeof resultCallback === 'function') resolve(result) |
| } catch (err) { |
| reject(err) |
| } |
| |
| |
| return lastTime = nowTime |
| } |
| |
| |
| if (trailing && !timer) { |
| timer = setTimeout(() => { |
| timer = null |
| |
| |
| lastTime = +new Date() |
| |
| |
| const result = fn.apply(this, args) |
| |
| |
| try { |
| if(resultCallback && typeof resultCallback === 'function') resolve(result) |
| } catch (err) { |
| reject(err) |
| } |
| |
| }, remainTime) |
| } |
| }) |
| } |
| |
| |
| _throttle.cancel = function () { |
| if (timer) clearTimeout(timer) |
| |
| |
| timer = null |
| lastTime = 0 |
| } |
| |
| return _throttle |
| } |
- 这里我依旧使用代码对
Promise实现的返回值功能
进行简单测试:
HTML测试代码
| <input type="text"> |
| <button id="cancel">取消</button> |
| const inputEl = document.querySelector("input") |
| const btnEl = document.querySelector("button") |
| |
| let counter = 0 |
| const inputChange = function (event) { |
| console.log(`发送了第${++counter}次网络请求`, this, event) |
| |
| |
| return ~~(Math.random() * 100) |
| } |
| |
| |
| const _throttle = throttle(inputChange, 1000, { |
| leading: false, |
| trailing: false, |
| resultCallback() { } |
| }) |
| |
| |
| const tempCallback = function () { |
| const res = _throttle().then(res => { |
| console.log("Promise的返回值结果:", res) |
| }) |
| } |
| |
| |
| inputEl.oninput = tempCallback |
| |
| |
| btnEl.onclick = () => _throttle.cancel() |