这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战
防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中
而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理
而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件被频繁的触发
防抖(debounce)
- 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间(也就是进行
事件的延迟触发) - 当事件密集触发时,函数的触发会被频繁的推迟
- 只有等待了一段时间也没有事件触发,才会真正的执行响应函数
使用场景
- 输入框中频繁的输入内容,搜索或 者提交信息
- 频繁的点击按钮,触发某个事件
- 监听浏览器滚动事件,完成某些特 定操作
- 用户缩放浏览器的resize事件
节流(throttle)
- 当事件触发时,会执行这个事件的响应函数
- 如果这个事件会被频繁触发,那么节流函数会
按照一定的频率来执行函数 - 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的
使用场景
- 监听页面的滚动事件
- 鼠标移动事件
- 用户频繁点击按钮操作
- 游戏中的一些设计(如在一段时间内,用户无论点击了多少次空格,都只会发射一个子弹)
underscore
事实上我们可以通过一些第三方库来实现防抖操作
lodash是underscore的升级版,它更重量级,功能也更多
但是目前underscore还在维护,lodash已经很久没有更新了
防抖
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
</head>
<body>
<input type="text" id="input" />
<script>
const inputEl = document.getElementById('input')
let count = 0
const inputHandler = () => {
console.log(`第${++count}次请求被触发`)
}
// 防抖
// 下划线 --- underscore提供的全局对象
// 参数1:需要进行防抖的函数
// 参数2:函数需要被推迟的时间
// 返回值: 一个实现了防抖功能的新函数
inputEl.oninput = _.debounce(inputHandler, 500)
</script>
</body>
</html>
节流
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
</head>
<body>
<input type="text" id="input" />
<script>
const inputEl = document.getElementById('input')
let count = 0
const inputHandler = () => {
console.log(`第${++count}次请求被触发`)
}
// 节流
// 下划线 --- underscore提供的全局对象
// 参数1:需要进行节流的函数
// 参数2:函数被触发的间隔时间
// 返回值: 一个实现了节流功能的新函数
inputEl.oninput = _.throttle(inputHandler, 500)
</script>
</body>
</html>
自定义防抖和节流函数
防抖
// 防抖函数定义
function debounce(fn, delay) {
// 闭包所使用的自由变量
let timer = null
return function() {
clearTimeout(timer)
timer = setTimeout(() => {
fn()
// 初始化自由变量
timer = null
}, delay)
}
}
我们在使用函数的时候,可能需要传递对应的参数或者在函数内部使用this关键字
但是使用上面的方式实现防抖功能的时候,是无法获取到准确的this和参数的
所以对其进行如下的调整
function debounce(fn, delay) {
// 闭包所使用的自由变量
let timer = null
// 返回的函数是实际调用的函数
// 所以该函数可以获取到正确的this以及参数
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
有的时候,我们希望在每一个阶段的第一次操作的时候,并不进行防抖操作(便于先向用户展示对应的效果),而对于每一个阶段中的其它执行周期都进行防抖操作
function debounce(fn, delay, isimmediate=false) {
let timer = null
// 虽然可以直接使用isimmediate完成对应功能
// 但是isimmediate其实是一个参数,不可以随便修改 --- 为了是一个纯函数,不产生副作用
// 所以新开了变量isinvoke用来辅助判断
let isinvoke = false
return function(...args) {
clearTimeout(timer)
if (isimmediate && !isinvoke) {
fn.apply(this, args)
isinvoke = true
} else {
timer = setTimeout(() => {
fn.apply(this, args)
isinvoke = false
timer = null
}, delay)
}
}
}
在实际使用中,我们可能遇到如下场景,用户在进行操作后,防抖函数执行前,用户执行了取消操作或者用户离开了当前页面
而防抖函数中存放的是一些比较耗时的异步操作,此时这些异步操作的函数其实是没有执行的必要的
所以需要为自定义的防抖函数添加取消功能
// 防抖函数
function debounce(fn, delay, isimmediate=false) {
let timer = null
let isinvoke = false
const _debounce =function(...args) {
clearTimeout(timer)
if (isimmediate && !isinvoke) {
fn.apply(this, args)
isinvoke = true
} else {
timer = setTimeout(() => {
fn.apply(this, args)
isinvoke = false
timer = null
}, delay)
}
}
_debounce.cancel = function() {
clearTimeout(timer)
isinvoke = false
timer = null
}
return _debounce
}
<!-- 使用 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./index.js"></script>
</head>
<body>
<input type="text" id="input" />
<button id="btn">cancel</button>
<script>
const inputEl = document.getElementById('input')
const btnEl = document.getElementById('btn')
let count = 0
const inputHandler = function(e) {
console.log(`第${++count}次请求被触发`)
}
const debounceHandler = debounce(inputHandler, 2000)
inputEl.oninput = debounceHandler
btnEl.onclick = function() {
debounceHandler.cancel()
}
</script>
</body>
</html>
虽然我们很少直接去获取防抖函数的返回值,但是不排除存在这样的可能性,所以我们需要将对于函数的返回值返回
方法1 --- 回调函数
function debounce(fn, delay, isimmediate=false, callback) {
let timer = null
let isinvoke = false
const _debounce =function(...args) {
clearTimeout(timer)
if (isimmediate && !isinvoke) {
const res = fn.apply(this, args)
callback && callback(res)
isinvoke = true
} else {
timer = setTimeout(() => {
const res = fn.apply(this, args)
callback && callback(res)
isinvoke = false
timer = null
}, delay)
}
}
_debounce.cancel = function() {
clearTimeout(timer)
isinvoke = false
timer = null
}
return _debounce
}
方法2 --- Promise
function debounce(fn, delay, isimmediate=false) {
let timer = null
let isinvoke = false
const _debounce =function(...args) {
return new Promise((resolve, reject)=> {
clearTimeout(timer)
if (isimmediate && !isinvoke) {
try {
const res = fn.apply(this, args)
resolve(res)
isinvoke = true
} catch(e) {
console.error(e.message)
}
} else {
timer = setTimeout(() => {
try {
const res = fn.apply(this, args)
resolve(res)
isinvoke = false
timer = null
} catch(e) {
console.error(e.message)
}
}, delay)
}
})
}
_debounce.cancel = function() {
clearTimeout(timer)
isinvoke = false
timer = null
}
return _debounce
}
节流
节流函数的本质就是限制在一定的时间间隔之间触发对应的事件
所以基本思路是获取上一次响应函数的触发时间(lastTime)和当前触发事件的触发时间(currentTime)
如果currentTime - lastTime的值大于或等于delay那么就需要触发对应的响应函数
function throttle(fn, interval) {
let lastTime = 0
return function() {
const currentTime = Date.now()
if (currentTime - lastTime > interval) {
fn()
lastTime = currentTime
}
}
}
但是此时的节流函数在执行的时候,会发现第一次刚刚触发的时候,对应的函数就会执行,这是因为默认currentTime是第一次执行的时间戳,lastTime是0,所以currentTime - lastTime一定是一个大于0的数值,所以第一次节流函数的回调一定会被触发
function throttle(fn, interval, options = { leading: true }) {
const {
leading // heading 用于控制第一次输入的时候,函数是否会被触发
} = options // options --- 传入的配置对象
let lastTime = 0
return function() {
const currentTime = Date.now()
// 第一次执行且heading的值为false的时候
if (!lastTime && !leading) {
// 此时确保delay的值是0
// 以保证下次throttle函数被触发的时候,正好是delay毫秒后
lastTime = currentTime
}
if (currentTime - lastTime > interval) {
fn()
lastTime = currentTime
}
}
}
此时存在一个问题,即如果间隔为10s,此时我在10.3s执行了一次事件,按照之前的逻辑函数,到20s的时候,对应的事件并不会被触发,如果此时我需要到20s的时候,在10s到20s之间执行的时间会被触发,就需要进行如下修改
function throttle(fn, interval, options) {
const {
leading = true,
trailing = false
} = options
let lastTime = 0
let timer = null
return function() {
const currentTime = Date.now()
if (!leading && !lastTime) {
lastTime = Date.now()
}
const remainTime = interval - (currentTime - lastTime)
if (remainTime <= 0) {
// 清除定时器,并重置对应的定时器变量
// 避免throttle函数被重复触发多次
clearTimeout(timer)
timer = null
fn()
lastTime = currentTime
return
}
// 最后一次回调函数被触发
if (trailing && !timer) {
timer =setTimeout(() => {
fn()
// 最后一次throttle函数执行完毕
// 相关变量初始化
timer = null
// 确保定时器开启以后,所有的throttle函数都在定时器中执行(★★★)
lastTime = !leading ? 0: Date.now()
}, remainTime)
}
}
}
和防抖函数一样,我们可以修正throttle函数的this并添加取消功能
function throttle(fn, interval, options) {
const {
leading = true,
trailing = false
} = options
let lastTime = 0
let timer = null
const _throttle = function(...args) {
const currentTime = Date.now()
if (!leading && !lastTime) {
lastTime = Date.now()
}
const remainTime = interval - (currentTime - lastTime)
if (remainTime <= 0) {
clearTimeout(timer)
timer = null
fn.apply(this, args)
lastTime = currentTime
return
}
if (trailing && !timer) {
timer =setTimeout(() => {
// 这里的setTimeout使用了回调函数
// 所以可以获取到正确的this
fn.apply(this, args)
timer = null
lastTime = !leading ? 0: Date.now()
}, remainTime)
}
}
// 取消函数
_throttle.cancel = () => {
clearTimeout(timer)
timer = 0
lastTime = 0
}
return _throttle
}
同样,我们也可以通过Promise或对应的callback获取到throttle函数的返回值
function throttle(fn, interval, options) {
const {
leading = true,
trailing = false
} = options
let lastTime = 0
let timer = null
const _throttle = function(...args) {
return new Promise((resolve, reject) => {
const currentTime = Date.now()
if (!leading && !lastTime) {
lastTime = Date.now()
}
const remainTime = interval - (currentTime - lastTime)
if (remainTime <= 0) {
clearTimeout(timer)
timer = null
const res = fn.apply(this, args)
resolve(res)
lastTime = currentTime
return
}
if (trailing && !timer) {
timer =setTimeout(() => {
// 这里的setTimeout使用了回调函数
// 所以可以获取到正确的this
const res = fn.apply(this, args)
resolve(res)
timer = null
lastTime = !leading ? 0: Date.now()
}, remainTime)
}
})
}
// 取消函数
_throttle.cancel = () => {
clearTimeout(timer)
timer = 0
lastTime = 0
}
return _throttle
}