Skip to content

防抖与节流

简单版实现

  • 去抖 让一个函数在一定间隔内没有被调用时,才开始执行被调用函数

延迟处理,如果在延时内又触发时间,则重新延迟

js
function debound(func, delay, immediate) {
  let timer = null
  return function() {
    let context = this
    let args = arguments

    if (timer) clearTimeout(timer)
    if (immediate) {
      // 根据距离上次触发操作的时间是否到达delay来决定是否要现在执行函数
      var doNow = !timer
      // 每次都重新设置timer 就是要保证每次执行至少 delay时间后才执行
      timer = setTimeout(function() {
        timer = null
      })
    }
  }
}
  • 节流 让一个函数无法在很段时间内连续调用,当上一次函数执行后过了规定时间间隔,才能进行下一次调用
js
/**
 * @param {Function} handler 要进行节流的函数
 * @param {Number} wait 需要等待的时间
 * @return {Function}
 */
function throttle(handler, wait) {
  let lastTime = 0
  return function() {
    let curTime = +new Date()
    if (curTime - lastTime > wait) {
      handler.apply(this, arguments)
      lastTime = curTime
    }
  }
}

防抖

在滚动事件中需要做复杂计算或者实现按钮防多次点击操作

这些需求都可用防抖来实现。若在频繁事件回调中做复杂计算,很可能导致页面卡顿,不如将多次计算合并为一次,只在一个精确点做操作。防抖轮子很多,这里使用underscore源码来解释防抖:

js
/**
 * 返回函数连续调用时,空闲时间必须大于或等于 wait ~ func才会执行
 * @param {function}  func      回调
 * @param {number}    wait      等待时间
 * @param {boolean}   immediate 设为true时,是否立即调用函数
 * @return {function}           返回用户调用函数
 */
_.debounce = function(func, wait, immediate) {
  var timeout, args, context, timestamp, result
  var later = function() {
    // 现在和上一次时间戳比较
    var last = _.now() - timestamp
    // 如果当前间隔时间少于设定时间 且 大于0就重新设置定时器
    if (last < wait && last >= 0) {
      timeout = setTimeout(later, wait - last)
    } else {
      // 否则就是时间到了 执行回调函数
      timeout = null
      if (!immediate) {
        result = func.apply(context, args)
        if (!timeout) context = args = null
      }
    }
  }

  return function() {
    context = this
    args = arguments
    // 获得时间戳
    timestamp = _.now()
    // 如果定时器不存在 且立即执行函数
    var callNow = immediate && !timeout
    // 如果定时器不存在就创建一个
    if (!timeout) timeout = setTimeout(later, wait)
    if (callNow) {
      // 如果需要立即执行函数的话,通过apply执行
      result = func.apply(context, apply)
      context = args = null
    }
    return result
  }
}

整体函数实现不难,总结一下

  • 对于按钮防点击的实现: 一旦开始一个定时器,只要定时器还在,不管怎么点击都不会执行回调。一旦定时器结束并设置为null,就可以再次点击了
  • 对于延迟执行函数来说的实现:每次调用防抖函数都会判断本次调用和之前的时间间隔,如果小于需要调用的时间间隔,就会重新创建一个定时器,并且定时器的延时为设定时间减去之前的时间间隔。一旦时间到,就会执行相应的回调

节流

防抖动和节流本质是不一样的。 防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每间隔一段时间执行

js
/**
 * 返回函数连续调用时, func执行频率限定为 次/wait
 * @param {function}  func      回调
 * @param {number}    wait      等待时间
 * @param {boolean}   options   若想忽略开始函数调用,传入{leading: false}
 *                              若想忽略结尾函数调用,传入{trailing: false}
 *                              两者不能共存,否则函数不执行
 * @return {function}           返回用户调用函数
 */
_.throttle = function(func, wait, options) {
  var context, args, result
  var timeout = null
  // 之前的时间戳
  var previous = 0
  // 如果options没传则设为空对象
  if (!options) options = {}
  // 定时器回调函数
  var later = function() {
    // 如果设置了leading 就将previous 设为0
    // 用于下面函数的第一个if判断
    previous = options.leading === false ? 0 : _.now()
    // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
    timeout = null
    result = func.apply(context, args)
    if (!timeout) context = args = null
  }

  return function() {
    // 获得当前时间戳
    var now = _.now()
    // 首次进入前者肯定为true 若需要第一个不执行函数,
    // 就将上次时间戳设为当前的,这样在计算remaining的值时会大于0
    if (!previous && options.leading === false) previous = now
    // 计算剩余时间
    context = this
    args = arguments
    // 若当前调用已大于上次调用时间 + wait 或者用户手动调了时间
    // 若设置了trailing 只会进入这条件
    // 若没有设置leading,那么第一次会进入该条件
    // (定时器的延迟不一定准,有可能进入该条件)
    if (remaining <= 0 || remaining > wait) {
      // 如果存在定时器就清理掉,否则调用二次回调
      if (timeout) {
        clearTimeout(timeout)
        timeout = null
      }
      previous = now
      result = func.apply(context, args)
      if (!timeout) context = args = null
    } else if (!timeout && options.trailing !== false) {
      // 判断是否设置了定时器和trailing
      // 没有就开启一个定时器,且不能同时设置leading和trailing
      timeout = setTimeout(later, remaining)
    }
    return result
  }
}

共 20 个模块,1301 篇 Markdown 文档。