防抖和节流都是对多次频繁触发的处理,但是二者又有很大不同。

防抖是将多次触发情况变为一次触发,如下图蓝色的圆,准备从左边走到右边,而每次点击按钮都会让其重新回到左边。我们把左到右这段距离比作防抖限制的时间wait,那么每次触发,只要wait时间没有走完,则wait会重新开始计时。

debounce-ball.gif

节流则是将多次触发变为每隔一段时间触发一次。如下图红色的球,每次触发,只要还没走完wait时间,就不会从头开始走。

throttle-ball.gif

对于防抖函数而言,我们要设置一个timer,作为每次触发时的标杆。

如下面的流程图所示,当timer存在,说明上一次的wait时间没有走完,需要将timer清空,重新计时。如果timer不存在,就赋值一个timer开始计时,再判断函数是否要立即执行,是的话则执行代码,清空timer,不是的话即开始等待wait时间的结束。如果wait时间还未结束,函数又被触发了,则回到顶部的函数调用,再进行一轮判断。

防抖函数流程图

节流函数则在函数调用时判断是否走完了wait,是则执行,不是则继续等待。

节流函数流程图

本篇小文主要是帮自己梳理防抖和节流的不同,以及其中的实现思路。学习过程里一直参看 JS | 前端进阶之道 。为了方便查阅,特将其中的防抖函数实现代码贴在下方。

// 这个是用来获取当前时间戳的
function now() {
  return +new Date()
}
/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

  // 延迟执行函数
  const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的情况下,函数会在延迟函数中执行
    // 使用到之前缓存的参数和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
      timer = later()
      // 如果是立即执行,调用函数
      // 否则缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    // 这样做延迟函数会重新计时
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

节流函数代码也是借鉴 JS | 前端进阶之道,不过只取了部分以实现功能。这里对wait是否走完的判断依据为当前时间和上一次调用时间的差值:

function throttle (func, wait) {
  var context, args
  // 设置前一个函数被触发的时间戳
  var previous = 0
  // 返回给用户调用的回调
  return function () {
    var now = new Date().getTime()
    // 首次进入
    if (!previous) previous = now
    // 准备context和args
    context = this
    args = arguments
    // 计算剩余的时间时长
    var remaining = wait - (now - previous)
    // 如果 now 超过了previous + wait,可以执行函数
    if (remaining <= 0) {
      previous = now
      timeout = null
      result = func.apply(context, args)
      context = args = null}
  }
}

之前学习节流时,做过一个小练习,也贴在此,若有兴趣,可以看看效果,持续点击+按钮即可。