1. 防抖(debounce)
- 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
- 当事件密集触发时,函数的触发会被频繁的推迟;
-
只有等待了一段时间也没有事件触发,才会真正的执行响应函数;
1.1 应用场景
例如: 输入框中频繁的输入内容
频繁的点击按钮,触发某个事件
监听浏览器滚动事件,完成某些特定操作
用户缩放浏览器的resize事件
等等
1.2 简单实现
第一个参数为函数 fn, 第二个参数延迟时间 delay
function debounce(fn, delay) {
// 1.定义一个定时器, 保存上一次的定时器
let timer = null
// 2.真正执行的函数
const _debounce = function() {
// 取消上一次的定时器
if (timer) clearTimeout(timer)
// 延迟执行
timer = setTimeout(() => {
// 外部传入的真正要执行的函数
fn()
}, delay)
}
return _debounce
}
至此, 已经实现了一个简单的防抖函数
1.3 绑定this和参数args
当原始函数有参数和需要用到this时, 可以给_debounc
添加args接收参数并在调用函数fn时使用 fn.apply(this, args)
function debounce(fn, delay) {
let timer = null
// 原始函数的参数args
const _debounce = function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
// 绑定this
fn.apply(this, args)
}, delay)
}
return _debounce
}
1.4 第一次是否执行
首次触发时是否立即执行
接收一个参数 immediate, 定义一个默认值, 这里为false, 声明一个是否第一次激活的变量 isInvoke = false, 如果immediate && !isInvoke
就执行函数并将 isInvoke = true, 如下代码
function debounce(fn, delay, immediate = false) {
// 1.定义一个定时器, 保存上一次的定时器
let timer = null
let isInvoke = false // 是否激活了立即执行
// 2.真正执行的函数
const _debounce = function(...args) {
// 取消上一次的定时器
if (timer) clearTimeout(timer)
// 判断是否需要立即执行
if (immediate && !isInvoke) {
fn.apply(this, args)
isInvoke = true // 已经立即执行, 阻止下次触发的立即执行
} else {
// 延迟执行
timer = setTimeout(() => {
// 外部传入的真正要执行的函数
fn.apply(this, args)
isInvoke = false // 将isInvoke初始化
}, delay)
}
}
return _debounce
}
1.5 增加取消方法
如果触发了事件又不需要了, 比如直接离开此位置, 增加取消功能防止事件触发带来意外不当
function debounce(fn, delay, immediate = false) {
// 1.定义一个定时器, 保存上一次的定时器
...
// 2.真正执行的函数
const _debounce = function(...args) {
...
}
// 封装取消功能
_debounce.cancel = function() {
if (timer) clearTimeout(timer)
timer = null
isInvoke = false
}
return _debounce
}
- 使用时便可以直接使用cancel方法
function fn() {
...
}
const debounceAfterFn = debounce(fn, 1000)
// 使用取消方法
debounceAfterFn.cancel()
1.6 函数返回值
解决返回值问题, _debounce返回一个Promise, 调用原始函数fn时拿到返回值
const result = fn.apply(this, args)
, 再调用resolve(result)
, 使用时通过.then
获取返回值
// debounce.js
function debounce(fn, delay, immediate = false) {
// 1.定义一个定时器, 保存上一次的定时器
let timer = null
let isInvoke = false
// 2.真正执行的函数
const _debounce = function(...args) {
return new Promise((resolve, reject) => {
// 取消上一次的定时器
if (timer) clearTimeout(timer)
// 判断是否需要立即执行
if (immediate && !isInvoke) {
const result = fn.apply(this, args)
resolve(result)
isInvoke = true
} else {
// 延迟执行
timer = setTimeout(() => {
// 外部传入的真正要执行的函数, 拿到函数返回值并调用resolve
const result = fn.apply(this, args)
resolve(result)
isInvoke = false
timer = null
}, delay)
}
})
}
// 封装取消功能
_debounce.cancel = function() {
if (timer) clearTimeout(timer)
timer = null
isInvoke = false
}
return _debounce
}
防抖的使用
在html中引入写好的防抖函数的文件debounce.js
<input type="text">
<button id="cancel">取消</button>
<script src="debounce.js"></script>
<script>
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function(event) {
console.log(`触发第${++counter}次`, this, event)
// 返回值
return "aaaaaaaaaaaa"
}
// 防抖处理 将原本函数放入 debounce 作为参数, 之后直接使用 debounceChange 即可
const debounceChange = debounce(inputChange, 3000, false)
//文本框事件触发
inputEl.oninput = (...args) => {
// 这种调用方法需要重新绑定this, 所写的debounce函数是没有问题的, 在实际开发中使用相对这里会简单一点
debounceChange.apply(inputEl, args).then(res => {
console.log("Promise的返回值结果:", res)
})
}
// 取消功能
const cancelBtn = document.querySelector("#cancel")
cancelBtn.onclick = function() {
debounceChange.cancel()
}
</script>
2. 节流(throttle)
- 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
-
不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的;
2.1 应用场景
例如: 鼠标移动事件
王者荣耀攻击键, 点击再快也是以一定攻速(频率)进行攻击
等等
2.2 简单实现
时间间隔: interval
; 当前时间: nowTime
; 上次执行时间: lastTime
- 主要算出每次点击后剩余的时间
- 时间差 = 当前时间 - 上次执行时间
- 剩余时间 = 时间间隔 - 时间差
const remainTime = interval - (nowTime - lastTime)
判断当剩余时间 <= 0 时执行函数
每次执行完之后将 lastTime = nowTime
function throttle(fn, interval, options) {
// 1.记录上一次的开始时间
let lastTime = 0
// 2.事件触发时, 真正执行的函数
const _throttle = function() {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 2.3.真正触发函数
fn()
// 2.4.保留上次触发的时间
lastTime = nowTime
}
}
return _throttle
}
2.3 首次不触发leading和最后结束触发trailing
2.3.1 leading
初始值lastTime = 0, 所以第一次的nowTime - lastTime
也会很大, interval
减去很大的正数, 就会成为负数 < 0 所以默认第一次是执行的
如果 interval 不是特别大, 第一次就是执行的, 除非你设置的 interval 大于当前时间. 也就是从计算机元年(1970)到如今, 五十多年的频率, 这个节流可不简单 :)
如果想要第一次不执行, 拿到remainTime之前将 lastTime = nowTime
即可
function throttle(fn, interval, options = { leading: true, trailing: false }) {
// 1.记录上一次的开始时间
const { leading, trailing } = options
let lastTime = 0
// 2.事件触发时, 真正执行的函数
const _throttle = function() {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
if (!lastTime && !leading) lastTime = nowTime
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 2.3.真正触发函数
fn()
// 2.4.保留上次触发的时间
lastTime = nowTime
}
}
return _throttle
}
2.3.2 trailing
最后是否执行, 需要使用到定时器 setTimeout
- 首先定义一个定时器 timer = null
- 如果
remainTime <= 0
判断 timer 不为空是就清除定时器并置空, 执行完函数后直接 return - 否则判断 trailing 为 true 且 timer 为 null 时, 设置一个定时器, 延迟时间就是剩余时间 remainTime
- 通过 timer 执行函数后关于 lastTime 最后应该设置为什么, 取决于 leading 的值
- 当 leading 为 true 时, 相当于2.2简单实现, 将lastTime赋值当前时间, 这里重新获取当前时间
lastTime = new Date().getTime()
- 当 leading 为 false 时, 直接将 lastTime 初始化为0, 相当于2.3.1 leading, 为0时下次执行将会进入
if (!lastTime && !leading) lastTime = nowTime
- 因此
lastTime = !leading ? 0 : new Date().getTime()
- 当 leading 为 true 时, 相当于2.2简单实现, 将lastTime赋值当前时间, 这里重新获取当前时间
function throttle(fn, interval, options = { leading: true, trailing: false }) {
// 1.记录上一次的开始时间
const { leading, trailing } = options
let lastTime = 0
let timer = null
// 2.事件触发时, 真正执行的函数
const _throttle = function() {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
if (!lastTime && !leading) lastTime = nowTime
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
if (timer) {
clearTimeout(timer)
timer = null
}
// 2.3.真正触发函数
fn()
// 2.4.保留上次触发的时间
lastTime = nowTime
return
}
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null
lastTime = !leading ? 0 : new Date().getTime()
fn()
}, remainTime)
}
}
return _throttle
}
2.4 绑定原本函数的this和参数args && 增加取消方法和函数返回值
与防抖实现相同, 使用apply绑定this和传参数, 返回Promise来解决返回值问题 最终代码如下:
function throttle(fn, interval, options = { leading: true, trailing: false }) {
// 1.记录上一次的开始时间
const { leading, trailing, resultCallback } = options
let lastTime = 0
let timer = null
// 2.事件触发时, 真正执行的函数
const _throttle = function(...args) {
return new Promise((resolve, reject) => {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
if (!lastTime && !leading) lastTime = nowTime
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
if (timer) {
clearTimeout(timer)
timer = null
}
// 2.3.真正触发函数
const result = fn.apply(this, args)
resolve(result)
// 2.4.保留上次触发的时间
lastTime = nowTime
return
}
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null
lastTime = !leading ? 0: new Date().getTime()
const result = fn.apply(this, args)
resolve(result)
}, remainTime)
}
})
}
_throttle.cancel = function() {
if(timer) clearTimeout(timer)
timer = null
lastTime = 0
}
return _throttle
}
也可使使用回调函数callback来处理返回值
节流的使用
<input type="text">
<button id="cancel">取消</button>
<script src="throttle.js"></script>
<script>
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function(event) {
console.log(`触发第${++counter}次`, this, event)
return 11111111111
}
// 节流处理
const _throttle = throttle(inputChange, 3000, {
leading: false,
trailing: true,
})
inputEl.oninput = (...args) => {
_throttle.apply(inputEl, args).then(res => {
console.log("Promise的返回值结果:", res)
})
}
// 取消功能
const cancelBtn = document.querySelector("#cancel")
cancelBtn.onclick = function() {
_throttle.cancel()
}
</script>
结语
学习自coderwhy(王红元)
老师, 感谢老师