你可能会遇到这种的情况,一个站点使用自动填充的文本框,内容的拖拽,效果的滚动。那么,你遇到防抖和截流的概率还是很高的。为了使得这些操作,比如自动填充能够顺畅工作,你需要引入防抖和截流功能。
- 防抖 -> Debounce
- 节流 -> Throttle
为什么我们需要防抖/节流
开篇已经简单提了,debounce/throttle
能让你的站点表现更优异。它们工作原理是通过减少动作发起的次数。我们简单举个例子,自动填充文本框触发接口请求,如下:
input.addEventListener("input", e => {
fetch(`/api/getOptions?query=${e.target.value}`)
.then(res => res.json())
.then(data => setOptions(data))
})
👆上面的事件监测器,监听到输入文本框发生更改,就基于文本框的内容触发一个查询接口。这看起来还不错,但是用户输入 Samantha
文字会发生什么?
当用户输入 S
,事件监测器触发请求,并带上选项 S
。当此请求正在调用的时候,Sa
输入内容会再次被监听,我们将重新以 Sa
为选项内容发起新的请求。以此类推,这种请求会持续到我们输完 Samantha
的内容。
这会在短时间内发起 8
次请求,但是我们只关心最后一次请求。这意味着前 7
的接口请求都是不必要的,纯属浪费时间和金钱。
为了避免不必要的请求发生,我们就需要防抖和截流。
防抖
我们先来谈下防抖,因为它是解决自动文本框类问题的理想解决方案。防抖的原理是延迟一段时间吊起我们的函数。如果在这个时间段没有发生什么,函数正常进行,但是有内容发生变更后的一段时间触发函数。这就意味着,防抖函数只会在特定的时间之后被触发。
在我们的例子中,我们假设延迟 1
秒触发。也就是当用户停止输入内容后 1
秒,接口强求被吊起。如果我们在 1
秒内输完 Samantha
内容,请求查询内容就是 Samantha
。下面我们看看怎么应用防抖。
function debounce(cb, delay = 250) {
let timeout
return (...args) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
cb(...args)
}, delay)
}
}
上面的防抖函数带有 cb
回调函数参数和一个延时的参数。我们在 debound
函数后返回回调函数,这种包装的方式,保证过了 delay
秒之后,回调函数才会被调用。最后,我们在每次调用 debounce
函数时清楚现有的定时器,以确保我们在延迟完成之前调用 debouce
函数,并重新计时。
这意味着如果用户在 1
秒内,每隔 300
毫秒触发一次输入,上面 debouce
函数如下方式工作。
// Type S - Start timer
// Type a - Restart timer
// Type m - Restart timer
// Type a - Restart timer
// Type n - Restart timer
// Wait 1 second
// Call debounced function with Saman
// Type t - Start timer
// No more typing
// Call debounced function with Samant
不知你注意到没有,即使我们已经输入了 Saman
文案动作超过了一秒中,回调函数也不会调起,知道再过 1
秒钟才被调用。现在,看我们怎么引用防抖函数。
const updateOptions = debounce(query => {
fetch(`/api/getOptions?query=${query}`)
.then(res => res.json())
.then(data => setOptions(data))
}, 1000)
input.addEventListener("input", e => {
updateOptions(e.target.value)
)}
我们把请求函数放到回调函数中。
防抖函数在自动填充的情形非常好用,你也可以使用在其他地方,你想将多个触发请求变成一个触发,以缓解服务器的压力。
节流
像防抖一样,节流也是限制请求的多次发送;但是,不同的是,防抖是每隔指定的时间发起请求。举个例子,如果你在 throttle
函数中设置延迟时间是 1
秒,函数被调用执行,用户输入每隔 1
秒发起请求。看下下面的应用,你就明白了。
function throttle(cb, delay = 250) {
let shouldWait = false
return (...args) => {
if (shouldWait) return
cb(...args)
shouldWait = true
setTimeout(() => {
shouldWait = false
}, delay)
}
}
debounce
和 throttle
函数都有一样的参数,但是它们主要的不同是,throttle
中的回调函数在函数执行后立马被调用,并且回调函数不在定时器函数内。回调函数要做的唯一事情就是将 shouldWait
标识设置为 false
。当我们第一次调用 throttle
函数,会将 shouldWait
标识设置为 true
。这延时的时间内再次调用 throttle
函数,那就什么都不做。当时间超出了延时的时间,shouldWait
标识才会设置为 false
。
假设我们每隔 300
毫秒输入一个字符,然后我们的延时是 1
秒。那么 throttle
函数会像下面这样工作:
// Type S - Call throttled function with S
// Type a - Do nothing: 700ms left to wait
// Type m - Do nothing: 400ms left to wait
// Type a - Do nothing: 100ms left to wait
// Delay is over - Nothing happens
// Type n - Call throttled function with Saman
// No more typing
// Delay is over - Nothing happens
如果你留意看,你会发现第 1200
毫秒的时候,第二次 throttle
函数才会被触发。已经延迟了我们预设时间 200
毫秒。对于节流的需求来说,目前的 throttle
函数已经满足了需求。但是我们做些优化,一旦 throttle
函数中的延时结束,我们就调用函数的前一个迭代。我们像下面这样子应用。
function throttle(cb, delay = 1000) {
let shouldWait = false
let waitingArgs
const timeoutFunc = () => {
if (waitingArgs == null) {
shouldWait = false
} else {
cb(...waitingArgs)
waitingArgs = null
setTimeout(timeoutFunc, delay)
}
}
return (...args) => {
if (shouldWait) {
waitingArgs = args
return
}
cb(...args)
shouldWait = true
setTimeout(timeoutFunc, delay)
}
}
上面的代码有点吓人,但是原理都一样。不同的是,在 throttle
函数延时时,后者存储了前一个 args
参数值作为变量 waitingArgs
。当延迟完成后,我们会检查 waitingArgs
是否有内容。如果没有内容,我们会将 shouldWait
设置为 false
,然后进入下一次触发。如果 waitingArgs
有内容,这就意味着延时到了之后,我们将会带上 waitingArgs
参数触发我们的回调函数,然后重置我们的定时器。
这个版本的 throttle
函数也是延时时间为 1
秒,每隔 300
毫秒输入值,效果如下:
// Type S - Call throttled function with S
// Type a - Save Sa to waiting args: 700ms left to wait
// Type m - Save Sam to waiting args: 400ms left to wait
// Type a - Save Sama to waiting args: 100ms left to wait
// Delay is over - Call throttled function with Sama
// Type n - Save Saman to waiting args: 700ms left to wait
// No more typing
// Delay is over - Call throttled function with Saman
正如你所看到的,每次我们触发 throttle
函数时,如果延时时间结束,我们要么调用回调函数,要么保存要在延时结束时使用的参数。如果这个参数有值的话,当延时结束时,我们将使用它。这就保证了 throttle
函数在延时结束时获取到最新的参数值。
我们看下怎么应用到我们的例子中。
const updateOptions = throttle(query => {
fetch(`/api/getOptions?query=${query}`)
.then(res => res.json())
.then(data => setOptions(data))
}, 500)
input.addEventListener("input", e => {
updateOptions(e.target.value)
)}
你会发现,我们的应用跟 debounce
函数很相似,除了将 debounce
名改为 throttle
。
当然,自动填充文本内容例子,对 throttle
函数并不适用,但是,如果你处理类如更改元素大小,元素拖拉拽,或者其他多次发生的事件,那么 throttle
函数是理想的选择。因为 throttle
每次延时结束时,你都会获得有关事件的更新信息,而 debounce
需要等待输入后延时后才能触发。总的来说,当你想定期将多个事件组合成一个事件时, throttle
是理想的选择。
本文为译文,采用意译
嗯~
我们来总结下,读完了上面的内容,可以简单这么理解:
- 防抖:你可以无限限次触发,但是在指定的
Delay
时间内监听到你没有新的触发事件了,就该我主角上场了。 - 节流:不管你触发多少次,在指定的
Delay
时间到了以后,我必须上场一次
【完】✅