lodash - (debounce.js和throttle.js)

1,509 阅读7分钟

debounce.js(防抖)

root  全局对象
isObject 判断变量是否是广义的对象(对象、数组、函数), 不包括null

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"
 */
function debounce(func, wait, options) {
  let lastArgs, // 上次调用参数
    lastThis,  // 上次调用this
    maxWait, // 允许被延迟的最大值
    result, // 返回结果
    timerId, // 定时器
    lastCallTime // 最近调用时间
  let lastInvokeTime = 0 // 上次调用func时间,即成功执行时间
  let leading = false // 指定在超时前调用
  let maxing = false  // 是否传入等待时间
  let trailing = true // 指定在超时后调用
  // Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
  const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function') // 通过requestAnimationFrame 设置wait=0;
  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) { //调用func,参数为当前时间
    const args = lastArgs // 调用参数
    const thisArg = lastThis //调用的this
    lastArgs = lastThis = undefined // 清除lastArgs和lastThis
    lastInvokeTime = time //上次调用时间为当前时间 
   result = func.apply(thisArg, args) //调用func,并将结果返回
    return result
  }
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) {  //超时之前调用    // Reset any `maxWait` timer.
    lastInvokeTime = time    // Start the timer for the trailing edge.
    timerId = startTimer(timerExpired, wait) // 开始timer 
   // Invoke the leading edge.
    return leading ? invokeFunc(time) : result
 // 如果leading为true,调用func,否则返回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  // 得到最后一次调用到现在的时间差
    // 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))  }
  function timerExpired() { // 刷新timer
    const time = Date.now() 
   if (shouldInvoke(time)) { //如果可以调用,调用trailingEdge 
     return trailingEdge(time)
    }
    // Restart the timer.
    timerId = startTimer(timerExpired, remainingWait(time)) // 不调用则重置timerId
  } 
  function trailingEdge(time) { //超时之后调用 
   timerId = undefined    // Only invoke if we have `lastArgs` which means `func` has been
    // debounced at least once.
    if (trailing && lastArgs) { //如果设置trailing为true,并且有lastArgs,调用func 
     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 // 调用的this 
   lastCallTime = time
    if (isInvoking) {
      if (timerId === undefined) { //首次触发,调用leadingEdge
        return leadingEdge(lastCallTime)
      }
     if (maxing) { // 处理多次频繁的调用 
       // Handle invocations in a tight loop.
        timerId = startTimer(timerExpired, wait) 
       return invokeFunc(lastCallTime)
      }
    }
    if (timerId === undefined) { //如果没有timer,设置定时器
      timerId = startTimer(timerExpired, wait)
    }
    return result 
 }
  debounced.cancel = cancel
  debounced.flush = flush
  debounced.pending = pending
  return debounced
}
export default debounce

throttle.js(断流)

import debounce from './debounce.js'

import isObject from './isObject.js'

/** * Creates a throttled function that only invokes `func` at most once per
 * every `wait` milliseconds (or once per browser frame). The throttled 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
 * throttled function. Subsequent calls to the throttled 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 throttled 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 `throttle` and `debounce`. 
 * 
 * @since 0.1.0 * @category Function
 * @param {Function} func The function to throttle.
 * @param {number} [wait=0] 
 *  The number of milliseconds to throttle invocations to; if omitted,
 *  `requestAnimationFrame` is used (if available).
 * @param {Object} [options={}] The options object.
 * @param {boolean} [options.leading=true] 
 *  Specify invoking on the leading edge of the timeout. 
 * @param {boolean} [options.trailing=true]
 *  Specify invoking on the trailing edge of the timeout. 
 * @returns {Function} Returns the new throttled function.
 * @example
 *
 * // Avoid excessively updating the position while scrolling. 
 * jQuery(window).on('scroll', throttle(updatePosition, 100)) 
 * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
 * const throttled = throttle(renewToken, 300000, { 'trailing': false })
 * jQuery(element).on('click', throttled)
 *  // Cancel the trailing throttled invocation.
 * jQuery(window).on('popstate', throttled.cancel)
 */
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

从上面看得出  throttle 是通过debounce来实现,所以也就没有注解了


debounce源码分析

debounce.js这个文件的核心和入口是debounced函数,我们先看看它:

function debounced(...args) {     
const time = Date.now()    
const isInvoking = shouldInvoke(time) // 是否能调用
    lastArgs = args  // 记录最后一次调用传入的参数
    lastThis = this // 记录最后一次调用的this 
   lastCallTime = time // 记录最后一次时间
    // isInvoking 字面可以表示是否台调用
     if (isInvoking) {
      if (timerId === undefined) { //首次触发,调用leadingEdge
       return leadingEdge(lastCallTime)
      }
      if (maxing) { // 处理多次频繁的调用 其实这个是和throttle有关的参数
        // Handle invocations in a tight loop.
        timerId = startTimer(timerExpired, wait)
        return invokeFunc(lastCallTime)
      } 
   }
    if (timerId === undefined) { //如果没有timer,设置定时器
      timerId = startTimer(timerExpired, wait)
    }
    return result
  }

这里面很多变量,用闭包存下的一些值  lastArgs 、lastThis 、lastCallTime 

trailingEdge函数其实就是执行一下invokeFunc然后清空一下定时器还有一些上下文,这样下次再执行debounce过的函数的时候就能够继续下一轮了

  function trailingEdge(time) { //超时之后调用
    timerId = undefined 
   // Only invoke if we have `lastArgs` which means `func` has been 
   // debounced at least once. 
   if (trailing && lastArgs) { //如果设置trailing为true,并且有lastArgs,调用func
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined 
   return result
  }

接下来timerExpired的内容

function timerExpired() { // 刷新timer

const time = Date.now()
if (shouldInvoke(time)) { //如果可以调用,调用trailingEdge
return trailingEdge(time)}// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time)) // 不调用则重置timerId
} 

以上要是在没有maxWait 参数情况下要简单实现如下:

// debounce简单实现
const debounce = function(wait, func){  
  let timerId
  return function(...args){
    var thisArg = this
    clearTimeout(last)
    timerId = setTimeout(function(){
        func.apply(thisArg, args)
    }, wait)
  }
}

throttle源码分析

其实基本用的都是debounce.js里面的内容,其实就是debounced函数中的如下代码作用:

if (maxing) { // 处理多次频繁的调用
     // Handle invocations in a tight loop.
     timerId = startTimer(timerExpired, wait)
     return invokeFunc(lastCallTime)
}

可以看到remainingWait和shouldInvoke中也都对maxing进行了判断

总结一下其实就是下面这样,忽略了细节和边界问题

throttle简单实现
var throttle = function(wait, func){
   var last = 0  return function(){
    var time = +new Date()
    if (time - last > wait){
      func.apply(this, arguments)
      last = curr
     }
  }
}
一开始看的时候有点不理解,因为和我想的这两个的简单实现有很多不同,看一遍时有点不知所云,后通过其现有参数的意图后才慢慢拆分理解,lodash考虑了很多细节和现实场景,如 是否要立即执行、最一次要执行、上一次执行和下一次执行重叠等等