吐槽一下函数防抖 debounce 与函数节流 throttle,说说更好的函数节流 betterThrottle

rrdawlx

技术圈内总有人喜欢故弄玄虚,把简单的事情说复杂。
首先表明一下个人看法:现在网上常见的“函数节流”和“函数防抖”,本质上都是“函数节流”,是“函数节流”的不同实现,它们目的都是降低被连续调用的函数的实际执行频率。“函数节流”是一种方法,“防抖”是“函数节流”后能达到的一种效果。

网上的“函数防抖”实现大致如下:

function debounce (fn, delay = 100) {
  let timeout
  
  return function (...args) {
    clearTimeout(timeout)

    timeout = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

网上的“函数节流”实现大致如下:

function throttle (fn, interval = 100) {
  let lastTime = 0
  
  return function (...args) {
    let now = Date.now()
    
    if (now - lastTime >= interval) {
      lastTime = now
      fn.apply(this, args)
    }
  }
}

“函数防抖”和“函数节流”这两个名词是谁最先提出的无从考究,但应该是老外先提出来的。underscore 和 loadsh 这两个库里面都有 debounce 和 throttle 两个方法的实现,估计很多人都是以这些库中的定义为标准来区分两个函数。
我们来看一下《JavaScript高级程序设计(第3版)》615页中对“函数节流”的实现:
image.png
image.png
看到没,就是类似上面 debounce 函数的基于 setTimeout 的实现。到此,你有没有觉得业界对“函数防抖”和“函数节流”的定义有些混乱?
咱们来推敲一下“函数防抖”的本质,看看是否应该是这样的:
为什么要“防抖”? → 因为元素“抖”起来影响用户体验 → 为什么会“抖”? → 因为元素变化太过频繁 → 怎么“防抖”? → 降低元素变化频率 → 降低触发元素变化的函数的执行频率 → “函数节流”
再看一下《JavaScript高级程序设计(第3版)》614页中对“函数节流”的定义:
image.png
所以说,“防抖”的本质是“函数节流”,“防抖”是目的,“函数节流”是达到目的的方法。“函数防抖”这个词本身就有问题,“抖”的是页面中的元素,函数不会“抖”,说成“元素防抖”更贴切。
“函数防抖”与“函数节流”的关系,就好比是“凤梨”和“菠萝”的关系,它们确实有些许区别,但本质上是一样东西。对于那些使用了 debounce 函数能够达到“防抖”效果的场景,我相信使用 throttle 函数也能达到“防抖”效果。

debounce 函数存在的问题

  1. 如果函数只被调用了一次,也必须等 delay 时间后才执行。
  2. 函数被连续调用时,每两次调用的时间间隔 Δt 是随机的。如果 Δt 长期小于 delay,则函数会长时间未执行,甚至只执行一次。(有人会觉得要“防抖”就得只执行最后一次调用的函数,我觉得这是错的。元素看起来“抖”了是因为其变化太过频繁,假设只要元素变化间隔大于 delay 就不会抖,那我们要“防抖”,就是要让函数的执行时间间隔大于 delay,而不是让函数只在最后执行一次。)
  3. setTimeout 的作用是告诉浏览器在指定时间后将指定的函数 fn 放到 JS 任务队列中。极端情况下可能出现 JS 任务队列中存在两个 fn 的待执行任务的情况,也就可能出现两次 fn 的执行时间间隔小于 delay 的情况。

throttle 函数存在的问题

  1. 最后一次调用的函数可能不会被执行,很多时候这可能会导致问题。

更好的函数节流 betterThrottle

更好的“函数节流”实现,主要有以下优点:

  1. 第一次调用的函数会被立即执行。
  2. 最后一次调用的函数肯定会被执行。
  3. 降低函数执行频率,但不会降到意想不到地低,实际执行频率会尽量接近指定的频率。
  4. 保证两次函数执行时间间隔不小于指定的时间间隔。
function betterThrottle (fn, interval = 100) {
  let timeout = null
  let lastTime = 0

  return function (...args) {
    clearTimeout(timeout)
    let now = Date.now()
    let _interval = now - lastTime

    if (_interval >= interval) {
      lastTime = now
      fn.apply(this, args)
    } else {
      let _lastTime = lastTime
      timeout = setTimeout(() => {
        if (_lastTime === lastTime) {
          lastTime = Date.now()
          fn.apply(this, args)
        }
      }, interval - _interval)
    }
  }
}
阅读 1.4k

rrdait
rrdait
192 声望
0 粉丝
0 条评论
192 声望
0 粉丝
宣传栏