Kwin Huang

不会搞艺术的程序员不是好设计师.

事件的节流和防抖

2019-09-29

有些浏览器事件可以在短时间内快速触发多次,比如调整窗口大小或向下滚动页面。例如,监听页面窗口滚动事件,并且用户持续快速地向下滚动页面,那么滚动事件可能在 3 秒内触发数千次,这可能会导致一些严重的性能问题。

如果在面试中讨论构建应用程序,出现滚动、窗口大小调整或按下键等事件请务必提及 防抖(Debouncing) 和 函数节流(Throttling)来提升页面速度和性能。这两兄弟的本质都是以闭包的形式存在。通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。

Throttle 第一个人说了算

throttle 的主要思想:在某段时间内,不管你触发了多少次回调,都只认第一次,并在计时结束时给予响应。

现在就来实现一个简单的 throttle

/**
 * generate a throttling function
 * 
 * @param {Function} fn The callback of event
 * @param {Number} interval time interval
 */
function throttle(fn, interval) {
  var last = 0

  return function() {

    var context = this

    var args = arguments

    var now = +new Date()

    if (now - last >= interval) {
      last = now
      fn.apply(context, args)
    }
  }
}
var betterScroll = throttle(function() {
  console.log('滚动事件触发')
}, 1000)

document.addEventListener('scroll', betterScroll)

Debounce 最后一个参赛者说了算

Debounce 的主要思想:我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次。

现在就来实现一个简单的 Debounce

/**
 * generate a debounce function
 * 
 * @param {Function} fn The callback of event
 * @param {Number} delay time interval
 */
function debounce(fn, delay) {
  var timer = null
  
  return function () {

    var context = this

    var args = arguments

    if (timer) {
      clearTimeout(timer)
    }
    // 设立新定时器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}

// 用debounce来包装scroll的回调
const betterScroll = debounce(function() {
  console.log('滚动事件触发')
}, 1000)

document.addEventListener('scroll', betterScroll)

用 Throttle 来优化 Debounce

debounce 的问题在于它“太有耐心了”。试想,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。

为了避免弄巧成拙,我们需要借力 throttle 的思想,打造一个“有底线”的 debounce——等你可以,但我有我的原则:delay 时间内,我可以为你重新生成定时器;但只要delay的时间到了,我必须要给用户一个响应。这个 throttle 与 debounce “合体”思路,已经被很多成熟的前端库应用到了它们的加强版 throttle 函数的实现中:

/**
 * generate a throttle function
 * 
 * @param {Function} fn The callback of event
 * @param {Number} delay time interval
 */
function throttle(fn, delay) {
  var last = 0, timer = null

  return function () {

    var context = this

    var args = arguments

    var now = +new Date()

    if (now - last < delay) {
      clearTimeout(timer)

      timer = setTimeout(function() {
        last = now
      }, delay)
    } else {
      last = now

      fn.apply(context, args)
    }
  }
}
var betterScroll = throttle(function() {
  console.log('滚动事件触发')
}, 1000)

document.addEventListener('scroll', betterScroll)