Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 中防抖和节流的应用 #143

Open
reng99 opened this issue May 21, 2022 · 0 comments
Open

JavaScript 中防抖和节流的应用 #143

reng99 opened this issue May 21, 2022 · 0 comments
Labels
blog a single blog javascript javascript tag

Comments

@reng99
Copy link
Owner

reng99 commented May 21, 2022

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第27天,点击查看活动详情

你可能会遇到这种的情况,一个站点使用自动填充的文本框,内容的拖拽,效果的滚动。那么,你遇到防抖和截流的概率还是很高的。为了使得这些操作,比如自动填充能够顺畅工作,你需要引入防抖和截流功能。

  1. 防抖 -> Debounce
  2. 节流 -> 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)
  }
}

debouncethrottle 函数都有一样的参数,但是它们主要的不同是,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 是理想的选择。

本文为译文,采用意译

嗯~

我们来总结下,读完了上面的内容,可以简单这么理解:

debouce_throttle.png

  • 防抖:你可以无限限次触发,但是在指定的 Delay 时间内监听到你没有新的触发事件了,就该我主角上场了。
  • 节流:不管你触发多少次,在指定的 Delay 时间到了以后,我必须上场一次

【完】✅

@reng99 reng99 added blog a single blog javascript javascript tag labels Jun 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blog a single blog javascript javascript tag
Projects
None yet
Development

No branches or pull requests

1 participant