每日源码分析 - lodash(debounce.js和throttle.js)

本系列使用 lodash 4.17.4css

前言

本文件引用了isObject函数前端

import isObject from './isObject.js' 判断变量是不是广义的对象(对象、数组、函数), 不包括null跨域

正文

import isObject from './isObject.js'

/** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked. 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`. * * 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. * @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,
    maxWait,
    result,
    timerId,
    lastCallTime

  let lastInvokeTime = 0
  let leading = false
  let maxing = false
  let trailing = true

  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
  }

  function leadingEdge(time) {
    // Reset any `maxWait` timer.
    lastInvokeTime = time
    // Start the timer for the trailing edge.
    timerId = setTimeout(timerExpired, wait)
    // Invoke the leading edge.
    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

    // 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() {
    const time = Date.now()
    if (shouldInvoke(time)) {
      return trailingEdge(time)
    }
    // Restart the timer.
    timerId = setTimeout(timerExpired, remainingWait(time))
  }

  function trailingEdge(time) {
    timerId = undefined

    // Only invoke if we have `lastArgs` which means `func` has been
    // debounced at least once.
    if (trailing && lastArgs) {
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined
    return result
  }

  function cancel() {
    if (timerId !== undefined) {
      clearTimeout(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) {
        // Handle invocations in a tight loop.
        timerId = setTimeout(timerExpired, wait)
        return invokeFunc(lastCallTime)
      }
    }
    if (timerId === undefined) {
      timerId = setTimeout(timerExpired, wait)
    }
    return result
  }
  debounced.cancel = cancel
  debounced.flush = flush
  debounced.pending = pending
  return debounced
}

export default debounce

复制代码

使用方式

函数防抖(debounce)

函数防抖(debounce)和函数节流(throttle)相信有必定前端基础的应该都知道,不过仍是简单说一下数组

防抖(debounce)就是把多个顺序的调用合并到一块儿(只执行一次),这在某些状况下对性能会有极大的优化(后面使用场景会说几个)。浏览器

图片来自css-tricks 性能优化

debounce

在lodash的options中提供了一个leading属性,这个属性让其在开始的时候触发。服务器

图片来自css-tricks 闭包

leading

// debounce函数的简单使用
var log = function() {
    console.log("log after stop moving");
}
document.addEventListener('mousemove', debounce(log, 500))
复制代码

函数节流(throttle)

使用throttle时,只容许一个函数在 X 毫秒内执行一次。app

好比你设置了400ms,那么即便你在这400ms里面调用了100次,也只有一次执行。跟 debounce 主要的不一样在于,throttle 保证 X 毫秒内至少执行一次。异步

在lodash的实现中,throttle主要借助了debounce来实现。

// throttle函数的简单使用
var log = function() {
    console.log("log every 500ms");
}
document.addEventListener('mousemove', throttle(log, 500))
复制代码

使用场景

我尽可能总结一下debounce和throttle函数实际的应用场景

防抖(debounce)

1. 自动补全(autocomplete)性能优化

自动补全不少地方都有,基本无一例外都是经过发出异步请求将当前内容做为参数传给服务器,而后服务器回传备选项。

那么问题来了,若是我每输入一个字符都要发出个异步请求,那么异步请求的个数会不会太多了呢?由于实际上用户可能只须要输入完后给出的备选项

这时候就可使用防抖,好比当输入框input事件触发隔了1000ms的时候我再发起异步请求。

2. 原生事件性能优化

想象一下,我有个使用js进行自适应的元素,那么很天然,我须要考虑我浏览器窗口发生resize事件的时候我要去从新计算它的位置。如今问题来了,咱们看看resize一次触发多少次。

window.addEventListener('resize', function() {
  console.log('resize')
})
复制代码

至少在我电脑上,稍微改变一下就会触发几回resize事件,而用js去自适应的话会有较多的DOM操做,咱们都知道DOM操做很浪费时间,因此对于resize事件咱们是否是能够用debounce让它最后再计算位置?固然若是你以为最后才去计算位置或者一些属性会不太即时,你能够继续往下看看函数节流(throttle)

节流(throttle)

和防抖同样,节流也能够用于原生事件的优化。咱们看下面几个例子

图片懒加载

图片懒加载(lazyload)可能不少人都知道,若是咱们浏览一个图片不少的网站的话,咱们不但愿全部的图片在一开始就加载了,一是浪费流量,可能用户不关心下面的图片呢。二是性能,那么多图片一块儿下载,性能爆炸。

那么通常咱们都会让图片懒加载,让一个图片一开始在页面中的标签为

<img src="#" data-src="我是真正的src">
复制代码

当我屏幕滚动到能显示这个img标签的位置时,我用data-src去替换src的内容,变为

<img src="我是真正的src" data-src="我是真正的src">
复制代码

你们都知道若是直接改变src的话浏览器也会直接发出一个请求,在红宝书(JS高程)里面的跨域部分还提了一下用img标签的src作跨域。这时候图片才会显示出来。

关于怎么判断一个元素出如今屏幕中的,你们能够去看看这个函数getBoundingClientRect(),这里就不扩展的讲了

好的,那么问题来了,我既然要检测元素是否在浏览器内,那我确定得在scroll事件上绑定检测函数吧。scroll函数和resize函数同样,滑动一下事件触发几十上百次,读者能够本身试一下。

document.addEventListener('scroll', function() {
  console.log('scroll')
})
复制代码

好的,你的检测元素是否在浏览器内的函数每次要检查全部的img标签(至少是全部没有替换src的),并且滑一次要执行几十次,你懂个人意思。

throttle正是你的救星,你可让检测函数每300ms运行一次。

拖动和拉伸

你觉得你只须要防备resizescroll么,太天真了,看下面几个例子。

或者想作相似原生窗口调整大小的效果

那么你必定会须要 mousedownmouseupmousemove事件,前两个用于拖动的开始和结束时的状态变化(好比你要加个标识标识开始拖动了)。 mousemove则是用来调整元素的位置或者宽高。那么一样的咱们来看看 mousemove事件的触发频率。

document.addEventListener('mousemove', function() {
  console.log('mousemove')
})
复制代码

我相信你如今已经知道它比scroll还恐怖并且可让性能瞬间爆炸。那么这时候咱们就能够用函数节流让它300ms触发一次位置计算。

源码分析

debounce.js

这个文件的核心和入口是debounced函数,咱们先看看它

function debounced(...args) {
  const time = Date.now()
  const isInvoking = shouldInvoke(time)

  lastArgs = args       // 记录最后一次调用传入的参数
  lastThis = this       // 记录最后一次调用的this
  lastCallTime = time   // 记录最后一次调用的时间

  if (isInvoking) {
    if (timerId === undefined) {
      return leadingEdge(lastCallTime)
    }
    if (maxing) {
      // Handle invocations in a tight loop.
      timerId = setTimeout(timerExpired, wait)
      return invokeFunc(lastCallTime)
    }
  }
  if (timerId === undefined) {
    timerId = setTimeout(timerExpired, wait)
  }
  return result
}
复制代码

这里面不少变量,用闭包存下的一些值

其实就是保存最后一次调用的上下文(lastThis, lastAargs, lastCallTime)还有定时器的Id之类的。

而后下面是执行部分, 因为maxing是和throttle有关的,为了理解方便这里暂时不看它。

// isInvoking能够暂时理解为第一次或者当上一次触发时间超过设置wait的时候为真
  if (isInvoking) {
    // 第一次触发的时候没有加timer
    if (timerId === undefined) {
      // 和上文说的leading有关
      return leadingEdge(lastCallTime)
    }
    //if (maxing) {
    // // Handle invocations in a tight loop.
    // timerId = setTimeout(timerExpired, wait)
    // return invokeFunc(lastCallTime)
    //}
  }
  // 第一次触发的时候添加定时器
  if (timerId === undefined) {
    timerId = setTimeout(timerExpired, wait)
  }
复制代码

接下来咱们看看这个timerExpired的内容

function timerExpired() {
    const time = Date.now()
    // 这里的这个判断基本只用做判断timeSinceLastCall是否超过设置的wait
    if (shouldInvoke(time)) {
      // 实际调用函数部分
      return trailingEdge(time)
    }
    // 若是timeSinceLastCall还没超过设置的wait,重置定时器以后再进一遍timerExpired
    timerId = setTimeout(timerExpired, remainingWait(time))
  }
复制代码

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) {
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined
    return result
  }
复制代码

总结一下其实就是下面这些东西,不过提供了一些配置和可复用性(throttle部分)因此代码就复杂了些。

// debounce简单实现
var debounce = function(wait, func){
  var timerId
  return function(){
    var thisArg = this, args = arguments
    clearTimeout(last)
    timerId = setTimeout(function(){
        func.apply(thisArg, args)
    }, wait)
  }
}
复制代码

throttle.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': leading,
    'maxWait': wait,
    'trailing': trailing
  })
}
复制代码

其实基本用的都是debounce.js里面的内容,只是多了个maxWait参数,还记得以前分析debounce的时候被咱们注释的部分么。

if (isInvoking) {
    if (timerId === undefined) {
      return leadingEdge(lastCallTime)
    }
    // **看这里**,若是有maxWait那么maxing就为真
    if (maxing) {
      // Handle invocations in a tight loop.
      timerId = setTimeout(timerExpired, wait)
      return invokeFunc(lastCallTime)
    }
  }
  if (timerId === undefined) {
    timerId = setTimeout(timerExpired, wait)
  }
复制代码

能够看到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 
    }
  }
}
复制代码

本文章来源于午安煎饼计划Web组 - 梁王

相关文章
相关标签/搜索