前言
节流防抖是前端老生常谈的一个话题,这里我们简单回顾下概念:
当某个事件被频繁的触发时(比如输入和滚动):
节流(throttle):会每隔一段时间执行一次。
防抖(debounce):会只执行最后一次。
这里我们以 input 事件为例:
const container = document.querySelector('#container')
function handleInput(event) {
const { value } = event.target
const div = document.createElement('div')
container.appendChild(div).innerHTML = value
}
可以看到在默认情况下,用户每输入一次就触发一次,很不环保。接下来我们实现节流与防抖函数,看看通过节流防抖处理后是什么样的。
实现节流
节流函数就就像红绿灯一样,当红灯时,不管有多少车辆排队,都需要等待红灯结束才能通行。
根据红绿灯的启发,我们得出思路:
当事件被触发时,开始计时,计时结束前不管触发了多少次,都不执行,直到计时结束后再去执行。
思路有了,方法就不难实现。
为了不影响原本的业务逻辑,原本的事件处理函数不变,只需要将其使用 throttle 函数包裹一下,再返回一个处理后的新的函数即可:
const throttledInput = throttle(handleInput)
接下来开始实现:
首先声明 throttle 函数,并通过参数 func 接收原本的函数:
function throttle(func) {
// 提前声明一个变量,用于保存定时器
let timer = null
// throttle 需要返回一个新的函数,这个函数内部其实还是 func,区别就是等待一段时间后才会执行
return function () {
// 如果定时器还在运行,说明已经在等待中了,稍后就会执行了,所以不用再往下执行了
if (timer) return
// 否则,设置定时器,等待 500 毫秒后执行 func 函数
timer = setTimeout(() => {
// 并将其参数原封不动的还给 func,并执行 func 函数,参数其实就是每个事件都有的 event,我们不能因为做了节流防抖就把 event 数据给丢了
func.apply(this, arguments)
// 执行后就不用等待了,可以把定时器清空了,用于后续继续处理新的事件
timer = null
}, 500)
}
}
在上面的代码中,我们主要创建了一个 timeout,当事件被触发时,开始计时,计时结束后执行事件处理函数。由于 timeout 可能已经在运行了,所以还要提前检查其是否在运行,如果在运行,就不用管他了,return 即可。
测试一下,为 input 事件添加节流函数:
input.addEventListener("input", throttledInput)
运行效果:
还没完,由于 throttle 是一个通用函数,每个业务的等待时间可能不一样,为了使 throttle 更加灵活,需要将等待时间可以设置为一个参数,但不是必传参数,这里默认设置为 500,大家可以自行调整:
/**
* @description 节流函数
* @param {Function} func 需要节流的函数
* @param {Number} delay 节流的时间
* @returns {Function} 返回节流后的函数
*/
function throttle(func, delay = 500) {
let timer = null
return function () {
if (timer) return
timer = setTimeout(() => {
func.apply(this, arguments)
timer = null
}, delay)
}
}
实现防抖
就像文章开头说的,「防抖」就是只执行最后一次,如何判断是否为最后一次,那就要定义「最后」的时间范围,不然直到世界毁灭前都不算最后一次。
那么就设定一个等待时间吧,在这个等待时间结束前,都不算最后一次。如果等待时间内被重新触发就重新等待,直到不被触发,才算最后一次,才去执行传入的函数。
根据这个思路,我们就可以很轻松的实现 debounce 方法。首先明确使用方法,同样为了不改变原本的事件处理方法,我们使用 debounce 包裹一层即可:
const debouncedInput = debounce(handleInput)
将事件处理函数设为第一个参数 func,和上面的 throttle 一样,为了灵活性,将等待时间设为第二个参数 delay。
function debounce(func, delay = 500) {
// 创建一个定时器
let timer
// 返回一个包裹了 func 的新的函数,作用就是在等待结束后才会执行 func
return function () {
// 如果定时器还在运行,则清除定时器,重新等待下一个,直到在 delay 毫秒内等不到为止
timer && clearTimeout(timer)
// 等待 delay 毫秒后如果没被清除,就不等了,func 就会被执行了
timer = setTimeout(() => {
// 执行 func
func.apply(this, arguments)
}, delay)
}
}
在上面的代码中,我们创建了一个 timeout,计时结束后会执行用户传入的函数。当事件被触发时,会首先检查timeout 是否在运行,如果在运行,则清除,重新等待,一直触发就会一直重新等待,不停的触发说明时间还没结束,所以不需要执行,直到 delay 毫秒(默认 500)后等不到才会执行。
为 input 事件添加防抖函数:
input.addEventListener("input", debouncedInput)
运行效果:
最终代码:
/**
* @description 防抖函数
* @param {Function} func 需要防抖的函数
* @param {Number} delay 防抖的时间
* @returns {Function} 返回防抖后的函数
*/
function debounce(func, delay = 500) {
let timer
return function () {
timer && clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, arguments)
}, delay)
}
}
apply
上面的代码中使用了 apply,它是 Function 原型上的一个方法,作用是将一个函数的作用域设置为某个对象,并将参数传入函数。
有关 apply 的详细介绍,可以点击 这里 查看。