防抖与节流

95 阅读6分钟

本来应该是主要分析防抖的,但是平时使用时防抖和节流基本都是一个比较容易混淆的概念,而且经常拿出来进行比较,所以就一起进行分析了。

什么是防抖和节流

防抖和节流都是用来减少函数的执行次数,减轻客户端和服务端压力,提高执行效率与性能并且少浪费资源的。我们为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用throttle(防抖)和debounce(节流)的方式来减少调用频率。

定义:

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

看了上面两个的定义之后,其实还是有很大的区别的,所以也有不同的适用范围。

  • 防抖适用于输入框远程查询事件,在线文档自动保存,浏览器视口大小改变
  • 节流适用于按钮提交事件,页面滚动事件的触发,搜索框联想功能

其实有时候看了上面的两个适用场景,我自己都会混淆,比如输入框这个情况,我自己有时候如果和业务相结合就会出现混淆。

上面已经对防抖和节流做了一定的解释了,那么让我们来针对这两种情况实现一下源码并且比对一下lodash的源码实现吧

防抖

其实实现防抖还是比较简单的,通过上面的定义我们可以知道,在一定时间之后执行该函数所以需要使用setTimeout在经过设定时间之后执行该函数,如果在未执行之前反复触发就要重新计时,那么就需要在未执行之前将之前的执行清除掉,那么就需要使用clearTimeout进行清除的操作,经过上面的分析之后其实代码就很简单了,如下:

function debounce (func, time) {
    // 闭包存储计时的id,方便后面删除
    var timeout = null;
    var debounced = function () {
        var _this = this;
        var params = arguments;
        // 删除之前的定时任务,或进行判断是否有定时器
        clearTimeout(timeout);
        // 设置计时器
        timeout = setTimeout(function () {
            func.apply(_this, params)
        }, time)
    }
    return debounced
}

当然我自己实现的是一个很简单的防抖,开源的代码例如lodash会有很多的考虑,比我的复杂的多,但是我们之间的原理是一样的。 看一下lodash是如何实现的吧:

// 判断是否是一个对象
import isObject from './isObject.js'
// 一个全局的变量
import root from './.internal/root.js'
function debounce(func, wait, options) {
  let lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime

  let lastInvokeTime = 0
  let leading = false
  let maxing = false
  let trailing = true
  const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
  // 源码严谨的判断了传入的参数是否是一个函数。
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  // 对时间的处理
  wait = +wait || 0
  // 判断用户传入的特殊要求是否是一个对象,然后对属性进行处理,这里通过属性就可以实现节流的功能
  if (isObject(options)) {
    leading = !!options.leading
    maxing = 'maxWait' in options
    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
    trailing = 'trailing' in options ? !!options.trailing : trailing
  }
  // 触发函数,就是直接执行用户真正的函数
  function invokeFunc(time) {
    const args = lastArgs
    const thisArg = lastThis

    lastArgs = lastThis = undefined
    lastInvokeTime = time
    result = func.apply(thisArg, args)
    return result
  }

  // 此处是使用setTimeout创建一个定时器,返回一个定时器标志位
  function startTimer(pendingFunc, wait) {
    if (useRAF) {
      root.cancelAnimationFrame(timerId)
      return root.requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
  }
  // 取消定时器
  function cancelTimer(id) {
    if (useRAF) {
      return root.cancelAnimationFrame(id)
    }
    clearTimeout(id)
  }

  function leadingEdge(time) {
    
    lastInvokeTime = time
   
    timerId = startTimer(timerExpired, wait)
    
    return leading ? invokeFunc(time) : result
  }
  // 用来计算剩余时间的(针对节流使用)
  function remainingWait(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    const timeWaiting = wait - timeSinceLastCall

    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting
  }
  // 判断是否应该调用
  function shouldInvoke(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
  }

  function timerExpired() {
    const time = Date.now()
    if (shouldInvoke(time)) {
      return trailingEdge(time)
    }
   
    timerId = startTimer(timerExpired, remainingWait(time))
  }

  function trailingEdge(time) {
    timerId = undefined

    if (trailing && lastArgs) {
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined
    return result
  }
  // 取消定时器功能
  function cancel() {
    if (timerId !== undefined) {
      cancelTimer(timerId)
    }
    lastInvokeTime = 0
    lastArgs = lastCallTime = lastThis = timerId = undefined
  }

  function flush() {
    return timerId === undefined ? result : trailingEdge(Date.now())
  }

  function pending() {
    return timerId !== undefined
  }
  // 这个是实现防抖的主要函数
  function debounced(...args) {
    const time = Date.now()
    const isInvoking = shouldInvoke(time)

    lastArgs = args
    lastThis = this
    lastCallTime = time

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime)
      }
      if (maxing) {
        timerId = startTimer(timerExpired, wait)
        return invokeFunc(lastCallTime)
      }
    }
    if (timerId === undefined) {
      timerId = startTimer(timerExpired, wait)
    }
    return result
  }
  debounced.cancel = cancel
  debounced.flush = flush
  debounced.pending = pending
  return debounced
}
export default debounce

lodash的函数是比较复杂的,因为里面有很多的配置,通过这些配置来实现更加丰富的功能,节流函数也相当于实现在了这个函数中,但是这里面的原理是和上面一样的。但是如果化繁为简的话就没有那么复杂了。

节流

节流是在第一次执行之后,一段时间之内都不会再次执行此函数,那就直接执行此函数,然后设置一个标志位(timeout)为一个任意值,在此值没有再次恢复到null时是不可以再次执行此函数的,需要立刻停止,那对于何时将timeout变为null,那就需要使用setTimeout了,在指定时间之后将值复位,这样就可以实现节流函数了,所以节流函数的简版如下:

function throttle(func, time) {
    var timeout = null;
     return function () {
         // timeout没有变为null之前不可以执行
         if (timeout) {
               return;
         }
         var _this = this;
         var params = arguments;
         // 立刻执行函数
         func.apply(_this, params);
         // 在一段时间后将timeout复位
         timeout = setTimeout(() => {
                timeout = null;
         }, time)
     }
}

lodash源码如下:

// 引入了防抖函数
import debounce from './debounce.js'
import isObject from './isObject.js'

function throttle(func, wait, options) {
  let leading = true
  let trailing = true

  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  if (isObject(options)) {
    leading = 'leading' in options ? !!options.leading : leading
    trailing = 'trailing' in options ? !!options.trailing : trailing
  }
  return debounce(func, wait, {
    leading,
    trailing,
    'maxWait': wait
  })
}

export default throttle

lodash的节流函数的核心就是防抖函数,因为传入的参数导致防抖函数变成了节流函数,所以我们要是设置的参数和源码中一致,也可以将防抖函数变为节流函数。

总结

上面两个函数其实都是「闭包」、「高阶函数」的应用,在实现之前我又再次去看了一下闭包这个概念,还把js的红皮书里面的相关章节看了一下,又有了一些感触吧,其实经常看一些源码,然后根据源码中遇到的问题在回头去看一下书,真的对自己原来不理解的地方又有了一些新的感悟吧。