【译】JavaScript 中的节流(throttle)和防抖(debounce)

目录

    • JavaScript中的节流和防抖
    • 节流和防抖的区别
    • 使用场景
    • 实现节流和防抖
      • 防抖
      • 节流

JavaScript中的节流和防抖

你是否因过度调用函数而影响性能呢?

解决性能问题是在 JavaScript 应用中经常要面对的事情。

节流和防抖使我们能够控制函数调用的速度,这是任何 Web 开发人员都需要了解的技术。当我们为事件绑定事件处理程序时,它们非常有用。在某些场景中,我们没有必要调用函数。 想一想我们要在 window 的 resize 事件触发时执行一个回调,每当浏览器的窗口尺寸变化时就要执行回调真的有意义吗?很可能不是,我们希望等待用户完成交互之后,再执行回调。

这儿有一个 demo(在窗口中对 mousemove 和 touchmove 事件处理程序做节流和防抖处理。节流函数调用 用红色菱形表示,防抖函数调用 用绿色圆圈表示):https://codepen.io/jh3y/pen/mGVGvm

节流和防抖的区别

节流 throttle:

throttle 是英文风门、油门、节流阀的意思。我们可以想到,通过脚踩油门可以控制进入发动机的油量,像这种操作一样,某些情况我们希望控制函数调用的次数。

我们找一个更好的类比,节流就像是乐透(彩票)一样,每五秒钟只出一个球。

或许更好的类比是在酒吧喝酒,你去了一个酒吧,但是在这儿有个规矩,每隔45分钟才能续杯一次。你在第一分钟点了一杯酒然后服务员递给你一杯,然后你想每隔一分钟就喝一杯,但是老板不让你这么做,直到45分钟后才给你下一杯,所以你必须等到45分钟后你才能喝下一杯。

使用节流时,我们可能希望节流结束时进行最后一次调用。想象一下,你在第15分钟点了一杯酒然后被拒绝,在第45分钟时你没有点,但是因为有第15分钟的一个订单给你上了一杯,老板对你感到抱歉?

节流是一个高阶函数,接收一个函数和一个超时参数。节流能控制函数在指定时间间隔内只被调用一次。它的作用是可以减慢函数调用

防抖 debounce:
**
防抖解释起来可能有点困难,就像在餐厅点餐,你向服务员说出要吃的东西,最后她会问你还需要其他的吗,如果你确定这些已经足够了不需要其他的了她们才会离开去给你准备,如果你还需要的话就继续向订单中添加东西,最后她们还问你还需不需要其他的,知道你确定已经足够了。

防抖和节流不同,使用防抖时,直到用户停止操作才会触发函数调用。它常被用在用户完成键入或滚动时要执行计算或调用接口的场景

一句话总结:

节流是限制流量,点击次数再多,短时间内有效的也只有一次;防抖是防止手抖,连续多点了几次,也只是最后一次有效。

微信群中有人这么说:节流就跟怀孕一样,一段时间内只能怀一胎;防抖就跟下班一样,只要有新活儿来就不能走。

上边 Codepen 中节流和防抖的可视化演示,可能有助于理解,移动鼠标或滑动手指(移动端)开始执行节流和防抖

使用场景

  • 用户可能连续点击时使用节流

  • API 调用时使用节流

  • mousemove 或 touchmove 事件回调时使用节流

  • resize 事件回调时使用防抖

  • scroll 事件回调时使用防抖

  • 一个自动保存功能中,保存函数使用防抖

让我们思考每一个场景,节流可能比防抖用的少。通常情况下,当你考虑使用节流时,可能使用防抖更好

对于节流,让我们看第一个例子,用户可能连续点击时使用节流。我们的应用中有一个按钮,当点击按钮时,就会调用某个 API,通过节流就可以限制 API 的调用次数。用户可能每秒钟点击20次,但我们每秒钟只触发一次回调

对于防抖,可以看自动保存功能的例子。每次用户进行更新或交互操作时,自动保存函数都尝试保存应用状态。这时我们可以使用防抖,直到用户在一段时间内没有进行任何更新或交互操作,才进行保存。这样就能避免用户连续操作时进行过多不必要的保存,有利于提高性能

实现节流和防抖

节流和防抖有各种不同的实现,但最终的目标都是一样的,大部分实现中都使用了 setTimeout

https://codepen.io/jh3y/pen/opNYWy 在这个例子中,函数以2秒时间进行防抖限制、以3秒时间进行节流限制

防抖

在两者中,防抖实现起来较为简单

const debounce = (func, delay) => {
  let inDebounce
  return function() {
    const context = this // 保存传给新函数的 this
    const args = arguments // 保存传给新函数的参数
    clearTimeout(inDebounce) // 清空上次调用
    inDebounce = setTimeout(() => func.apply(context, args), delay) // 重新计时,delay 时间后调用原函数,传入了已保存的 this 和参数
  }
}

将函数(func)和延迟时间(delay)传递给防抖函数,inDebounce 是执行函数的定时器的引用,这里是一个闭包

如果我们是第一调用,函数将在延迟结束时(也就是 delay 毫秒)执行。如果我们我们在延迟结束之前(也就是 delay 毫秒内)再次调用,则会重新计时,在重新计时后的 delay 毫秒执行

下面是使用防抖的例子:

debounceBtn.addEventListener('click', debounce(function() {
  console.info('Hey! It is', new Date().toUTCString());
}, 3000));

在这个例子中,停止点击按钮3秒后,会打印时间

节流

节流的实现有点复杂,因为节流的操作有不同的解释。让我们从限制执行函数的速度开始

const throttle = (func, limit) => {
  let inThrottle
  return function() {
    const args = arguments
    const context = this
    if (!inThrottle) {
      func.apply(context, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

第一次调用我们的函数将立刻执行并设置 inThrottle 为 true,表示正在节流中。如果在 limit 时间内再次调用节流函数,因为此时 inThrottle 为 true,原函数不会执行。当 limit 时间后,将 inThrottle 置为 false。

因为 limit 时间后 inThrottle 为 false,所以再调用时就像第一次调用一样,然后不断重复这个过程。

使用节流的例子:

throttleBtn.addEventListener('click', throttle(function() {
  return console.log('Hey! It is', new Date().toUTCString());
}, 1000));

但是最后一次调用怎么办呢?在这样的场景中:通过点击 div 的四个角然后绑定鼠标的 mousemove 事件来控制 div 的大小,为了避免过多计算提高性能,我们使用了上边 throttle 的实现,并且将限制时间 limit 设为500ms,如果我们拖动鼠标很快,比如从开始拖动到结束拖动只用了400ms,但是在上边的实现中,只会在刚开始拖动的时候触发一次函数通过鼠标位置计算 div 大小,在结束的 400ms 时却没有触发,所以这个实现并不能100%得到预期的结果。因此我们需要在限制时间 limit 内获取到最后一次函数调用并执行它。

下面是修改后的节流函数:

const throttle = (func, limit) => {
    let Timer
    let lastRanTime
    return function() {
        const context = this
        const args = arguments
        if (!lastRanTime) { // 第一次调用
            func.apply(context, args) // 直接传参执行原函数
            lastRanTime = Date.now() // 并记录执行时间
        } else {
            clearTimeout(Timer) // 清除上次调用
            Timer = setTimeout(function() { // 保存这次调用的引用
                if ((Date.now() - lastRanTime) >= limit) {
                    func.apply(context, args)
                    lastRanTime = Date.now()
                }
            }, limit - (Date.now() - lastRanTime)) // 保证每一个 limit 时间段内的最后一次调用都会执行
        }
    }
}

这个实现确保我们获取并执行最后一次调用,并且调用的时间(每个 limit 时间段的结尾)也是正确的。我们通过一个变量 lastRanTime 来实现,该变量保存最后一次调用的时间戳,然后根据它确定最后一次调用是否发生在 limit 范围内。我们也使用了 lastRanTime 来确定是否运行了该节流函数,所以之前实现中的 inThrottle 变量就不需要了

总之,防抖和节流都是我们需要了解的技术,它们可以显著的提高应用的性能。

你可能感兴趣的:(WEB前端)