JavaScript中防抖和节流的实战应用记录
前言
你可能会遇到这种的情况,一个站点使用自动填充的文本框,内容的拖拽,效果的滚动。那么,你遇到防抖和截流的概率还是很高的。为了使得这些操作,比如自动填充能够顺畅工作,你需要引入防抖和截流功能。
- 防抖 -> 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 时间到了以后,我必须上场一次
总结
上一篇:uniapp实现微信小程序的电子签名效果(附demo)
栏 目:JavaScript代码
下一篇:解析js中获得父窗口链接getParent方法以及各种打开窗口的方法
本文地址:http://www.codeinn.net/misctech/211757.html