_.debounce(func, [wait=0], [options={}])

101

_.debounce(func, [wait=0], [options={}])
_.debounce创建一个去抖函数来推迟调用func,自从上一次去抖函数被调用之后等待wait毫秒时间过后再调用,或者等待直到下一次浏览器帧被重新绘制。创建去抖函数的同时也会创建一个cancel方法去取消延迟func调用,还有一个flush方法来立即调用。也提供了option参数来表明func函数是否应该在等待wait时间开始之前调用还是wait时间过后调用。func函数调用会带着提供给去抖函数的最后一个配置参数。之后对于去抖函数的调用返回最后一次func调用的结果。
注意:如果leading和trailing配置项是true,func只有在去抖函数等待wait时间度过的时候被调用超过一次时才会在wait时延过后再次调用。
如果wait等待时间是0,并且leading是false,func的调用会推迟至下一次事件循环,与setTimeout设置延迟时间为0一样
如果wait参数在使用requestAnimationFrame的环境中被省略,func会延迟至下一次页面重绘的时候被调用,通常延迟16ms
 
使用场景
当window大小不断变化的时候避免昂贵的计算,例如
jQuery(window).on('resize', debounce(calculateLayout, 150))
 
当点击的时候调用`sendMail`,将随后的调用去抖
jQuery(element).on('click', debounce(sendMail, 300, {
    'leading': true,
    'trailing': false
}))
确保`batchLog`在去抖化调用后1秒钟被调用1次
const debounced = debounce(batchLog, 250, { 'maxWait': 1000 })
const source = new EventSource('/stream')
jQuery(source).on('message', debounced)
取消wait时延过后的去抖调用
jQuery(window).on('popstate', debounced.cancel)

检查是否调用处于等待状态

const status = debounced.pending() ? "Pending..." : "Ready"

参数

func (Function): 需要被去抖的函数
[wait=0] (number): 推迟执行的毫秒数,如果省略这个参数,requestAnimationFrame会被使用
[options={}] (Object): 配置对象
[options.leading=false] (boolean): 是否在wait时延之前调用
[options.maxWait] (number): 在func被调用之前的最大延迟时间
[options.trailing=true] (boolean): 是否在wait时延过后调用

返回值

(Function): 返回新的被去抖的函数

例子

复制代码
// Avoid costly calculations while the window size is in flux.
jQuery(window).on('resize', _.debounce(calculateLayout, 150));
 
// Invoke `sendMail` when clicked, debouncing subsequent calls.
jQuery(element).on('click', _.debounce(sendMail, 300, {
  'leading': true,
  'trailing': false
}));
 
// Ensure `batchLog` is invoked once after 1 second of debounced calls.
var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
var source = new EventSource('/stream');
jQuery(source).on('message', debounced);
 
// Cancel the trailing debounced invocation.
jQuery(window).on('popstate', debounced.cancel);
复制代码

去抖流程

首次进入函数时因为 lastCallTime === undefined 并且 timerId === undefined,所以会执行 leadingEdge,如果此时 leading 为 true 的话,就会执行 func。同时,这里会设置一个定时器,在等待 wait(s) 后会执行 timerExpired,timerExpired 的主要作用就是触发 trailing。

如果在还未到 wait 的时候就再次调用了函数的话,会更新 lastCallTime,并且因为此时 isInvoking 不满足条件,所以这次什么也不会执行。

时间到达 wait 时,就会执行我们一开始设定的定时器timerExpired,此时因为time-lastCallTime < wait,所以不会执行 trailingEdge。

这时又会新增一个定时器,下一次执行的时间是 remainingWait,这里会根据是否有 maxwait 来作区分:

如果没有 maxwait,定时器的时间是 wait - timeSinceLastCall,保证下一次 trailing 的执行。

如果有 maxing,会比较出下一次 maxing 和下一次 trailing 的最小值,作为下一次函数要执行的时间。

最后,如果不再有函数调用,就会在定时器结束时执行 trailingEdge。

下面是流程图片描述:

节流与去抖的区别

throttle节流函数与debounce去抖函数之间的区别
节流函数默认leading和trailing都是true,所以在等待时间内,如果触发超过一次,那么wait时延过后就会主动触发
去抖函数默认leading为false,trailing为true,如果一直不停的触发,那么会不停地重置定时器,所以直到停止不停触发事件结束后等待最后一个wait时间过后才会触发事件处理程序

源代码:

复制代码
import isObject from './isObject.js'
import root from './.internal/root.js'

/**
 * Creates a debounced function that delays invoking `func` until after `wait`
 * milliseconds have elapsed since the last time the debounced function was
 * invoked, or until the next browser frame is drawn. The debounced function
 * comes with a `cancel` method to cancel delayed `func` invocations and a
 * `flush` method to immediately invoke them. Provide `options` to indicate
 * whether `func` should be invoked on the leading and/or trailing edge of the
 * `wait` timeout. The `func` is invoked with the last arguments provided to the
 * debounced function. Subsequent calls to the debounced function return the
 * result of the last `func` invocation.
 *
 * **Note:** If `leading` and `trailing` options are `true`, `func` is
 * invoked on the trailing edge of the timeout only if the debounced function
 * is invoked more than once during the `wait` timeout.
 *
 * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
 * until the next tick, similar to `setTimeout` with a timeout of `0`.
 *
 * If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
 * invocation will be deferred until the next frame is drawn (typically about
 * 16ms).
 *
 * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
 * for details over the differences between `debounce` and `throttle`.
 *
 * @since 0.1.0
 * @category Function
 * @param {Function} func The function to debounce.
 * @param {number} [wait=0]
 *  The number of milliseconds to delay; if omitted, `requestAnimationFrame` is
 *  used (if available).
 * @param {Object} [options={}] The options object.
 * @param {boolean} [options.leading=false]
 *  Specify invoking on the leading edge of the timeout.
 * @param {number} [options.maxWait]
 *  The maximum time `func` is allowed to be delayed before it's invoked.
 * @param {boolean} [options.trailing=true]
 *  Specify invoking on the trailing edge of the timeout.
 * @returns {Function} Returns the new debounced function.
 * @example
 *
 * // Avoid costly calculations while the window size is in flux.
 * jQuery(window).on('resize', debounce(calculateLayout, 150))
 *
 * // Invoke `sendMail` when clicked, debouncing subsequent calls.
 * jQuery(element).on('click', debounce(sendMail, 300, {
 *   'leading': true,
 *   'trailing': false
 * }))
 *
 * // Ensure `batchLog` is invoked once after 1 second of debounced calls.
 * const debounced = debounce(batchLog, 250, { 'maxWait': 1000 })
 * const source = new EventSource('/stream')
 * jQuery(source).on('message', debounced)
 *
 * // Cancel the trailing debounced invocation.
 * jQuery(window).on('popstate', debounced.cancel)
 *
 * // Check for pending invocations.
 * const status = debounced.pending() ? "Pending..." : "Ready"
 */

//创建一个去抖函数来推迟调用func,自从上一次去抖函数被调用之后等待wait毫秒时间过后再调用,或者等待直到下一次浏览器帧被重新绘制。创建去抖函数的同时也会创建一个cancel方法去取消延迟func调用,还有一个flush方法来立即调用。也提供了option参数来表明func函数是否应该在等待wait时间开始之前调用还是wait时间过后调用。func函数调用会带着提供给去抖函数的最后一个配置参数。之后对于去抖函数的调用返回最后一次func调用的结果。
//注意:如果leading和trailing配置项是true,func只有在去抖函数等待wait时间度过的时候被调用超过一次时才会在wait时延过后再次调用。
//如果wait等待时间是0,并且leading是false,func的调用会推迟至下一次事件循环,与setTimeout设置延迟时间为0一样
//如果wait参数在使用requestAnimationFrame的环境中被省略,func会延迟至下一次页面重绘的时候被调用,通常延迟16ms

//使用场景
//当window大小不断变化的时候避免昂贵的计算,例如jQuery(window).on('resize', debounce(calculateLayout, 150))

//当点击的时候调用`sendMail`,将随后的调用去抖
// jQuery(element).on('click', debounce(sendMail, 300, {
//    'leading': true,
//    'trailing': false
// }))

//确保`batchLog`在去抖化调用后1秒钟被调用1次
// const debounced = debounce(batchLog, 250, { 'maxWait': 1000 })
// const source = new EventSource('/stream')
// jQuery(source).on('message', debounced)

//取消wait时延过后的去抖调用
//jQuery(window).on('popstate', debounced.cancel)

//检查是否调用处于等待状态
//const status = debounced.pending() ? "Pending..." : "Ready"

//func是需要去抖的方法
//wait推迟执行的毫秒数,如果省略这个参数,requestAnimationFrame会被使用
//requestAnimationFrame浏览器在下一次重绘之前调用指定的函数
//options中的配置项
//options.leading在wait时延之前调用
//options.trailing在wait时延过后调用
//options.maxWait在func被调用之前的最大延迟时间
function debounce(func, wait, options) {
  let lastArgs,
    lastThis,
    maxWait,//func调用之前的最大延迟时间
    result,
    timerId,//定时器
    lastCallTime

  let lastInvokeTime = 0//上一次调用func的时间
  let leading = false//是否在wait时延之前调用
  let maxing = false//是否设置了func调用之前的最大延迟时间
  let trailing = true//是否在wait时延过后调用

  // Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
  //忽略requestAnimationFrame通过明确地设置wait=0
  const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')

  if (typeof func != 'function') {//如果func不是function类型,抛出错误
    throw new TypeError('Expected a function')
  }
  wait = +wait || 0//将wait转换成数字
  if (isObject(options)) {//如果options是对象
    leading = !!options.leading//是否在wait时延之前调用
    maxing = 'maxWait' in options//是否设置了func调用之前的最大延迟时间
    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait//func调用之前的最大延迟时间
    trailing = 'trailing' in options ? !!options.trailing : trailing//是否在wait时延过后调用
  }

  function invokeFunc(time) {//执行func
    const args = lastArgs
    const thisArg = lastThis

    lastArgs = lastThis = undefined//debounced接收参数和this置空
    lastInvokeTime = time//更新上一次执行func时间
    result = func.apply(thisArg, args)//调用func获取结果返回
    return result
  }

  function startTimer(pendingFunc, wait) {//启动定时器
    if (useRAF) {
      return root.requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
  }

  function cancelTimer(id) {//取消定时器
    if (useRAF) {
      return root.cancelAnimationFrame(id)
    }
    clearTimeout(id)
  }

  function leadingEdge(time) {//leading时调用
    // Reset any `maxWait` timer.
    lastInvokeTime = time//重置上一次func执行时间
    // Start the timer for the trailing edge.
    timerId = startTimer(timerExpired, wait)//为wait时延过后的trailing调用开启定时器
    // Invoke the leading edge.
    return leading ? invokeFunc(time) : result//如果leading为true,wait时延之前调用,直接调用,否则返回result,undefined
  }

  function remainingWait(time) {//还剩下多少时间可以执行下一次
    const timeSinceLastCall = time - lastCallTime//距离上次去抖时间
    const timeSinceLastInvoke = time - lastInvokeTime//距离上次func执行时间
    const timeWaiting = wait - timeSinceLastCall//距离上次去抖时间还需要等多久,下一次trailing时间

    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting
      //如果有传递最大等待时间,就在下一次trailing时间和maxWait - timeSinceLastInvoke里找最小的
      //如果没有传递最大等待时间,返回下一次trailing时间
  }

  function shouldInvoke(time) {//判断在time时间点是否func应该被调用
    const timeSinceLastCall = time - lastCallTime//距离上次去抖时间
    const timeSinceLastInvoke = time - lastInvokeTime//距离上次func执行时间

    // Either this is the first call, activity has stopped and we're at the
    // trailing edge, the system time has gone backwards and we're treating
    // it as the trailing edge, or we've hit the `maxWait` limit.
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
      //lastCallTime === undefined首次调用
      //timeSinceLastCall >= wait距离上次被去抖已经超过 wait
      //timeSinceLastCall < 0//系统时间倒退
      //maxing && timeSinceLastInvoke >= maxWait//距离上次func执行时间已经超过最大等待时间
  }

  function timerExpired() {//等待wait时间后触发trailing
    const time = Date.now()//当前时间
    if (shouldInvoke(time)) {//如果当前时间应该调用,就调用trailingEdge判断是否已经去抖过一次
      return trailingEdge(time)
    }
    // Restart the timer.
    timerId = startTimer(timerExpired, remainingWait(time))//如果此刻不能执行func,就重新启动定时器,时间为剩余等待时间
  }

  function trailingEdge(time) {//trailing时调用func
    timerId = undefined//定时器置空

    // Only invoke if we have `lastArgs` which means `func` has been
    // debounced at least once.
    if (trailing && lastArgs) {//只有在拥有lastArgs参数的时候才执行func,lastArgs说明func被去抖至少一次
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined//lastArgs和lastThis置空
    return result//返回结果undefined
  }

  function cancel() {//取消定时器,并将上一次调用时间清0,上一次debounced接收参数,this,上一次去抖时间都置空
    if (timerId !== undefined) {
      cancelTimer(timerId)
    }
    lastInvokeTime = 0
    lastArgs = lastCallTime = lastThis = timerId = undefined
  }

  function flush() {//如果没有定时器,返回当前结果,如果有定时器,调用trailingEdge
    return timerId === undefined ? result : trailingEdge(Date.now())
  }

  function pending() {//查看当前是否处于等待状态
    return timerId !== undefined
  }

  function debounced(...args) {
    const time = Date.now()//当前时间毫秒数
    const isInvoking = shouldInvoke(time)//判断当前时间是否func应该被调用

    lastArgs = args//参数数组
    lastThis = this//本次this
    lastCallTime = time//去抖时间

    if (isInvoking) {//如果此刻需要被调用,1说明是首次调用且是leading调用 2距离上次去抖wait时间了 3距离上次执行时间过去了最大等待时间
      if (timerId === undefined) {//如果没有定时器说明1首次调用2刚执行过取消操作或者trailing调用
        return leadingEdge(lastCallTime)//调用leadingEdge判断是否leading时需要调用
      }
      if (maxing) {//如果不是第一次调用且设置了最大延迟时间,说明已经超过了最大延迟时间,直接调用返回结果
        // Handle invocations in a tight loop.
        timerId = startTimer(timerExpired, wait)//开启一个定时器,等待时间wait,判断是否wait时间后需要出发trailing调用
        return invokeFunc(lastCallTime)//调用func返回结果
      }
    }
    if (timerId === undefined) {//如果此刻不需要被调用,且定时器没有开启,就开启一个定时器,等待时间wait
      timerId = startTimer(timerExpired, wait)
    }
    return result
  }
  debounced.cancel = cancel
  debounced.flush = flush
  debounced.pending = pending
  return debounced
}

export default debounce
复制代码

 

posted @   hahazexia  阅读(1941)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示