lodash防抖节流源码理解

用过lodash库的应该熟悉_.debounce_.throttle,也就是函数防抖(debounce)和节流(throttle)。javascript

  • 函数防抖和节流分别是什么?
  • 为何《JavaScript高级程序设计》中throttle函数和防抖写法同样?
  • lodash库的debounce和throttle是什么关系,为何throttle返回的是debounce的执行结果?

概念

关于防抖动(debounce),能够从接点弹跳(bounce)了解起:css

接点弹跳(bounce)是一个在机械开关与继电器上广泛的问题。开关与继电器的接点一般由弹性金属制成。当接点一块儿敲击,接点在冲力与弹力一块儿做用下,致使弹跳部分在接点稳定前发生一次或屡次。排除接点弹跳效应的方法就是所谓的"去弹跳"(debouncing)电路。java

由此衍生"去弹跳"(debounce)一词,出现于软件开发工业中,用来描述一个消除开关弹跳实施方法的比率限制或是频率调节。git

关于节流阀(throttle):github

节流阀是一个能够调节流体压力的构造,可调整进入引擎的流量,进而调整引擎的出力。浏览器

众所周知,JavaScript是单线程做业,原本就至关忙碌,就不能总是堵着人家,严重的话可能致使浏览器挂起甚至崩溃。要避免高频的调用昂贵的计算操做,好比DOM交互。app

防抖和节流都能作到优化函数执行效率的效果,是很类似的技术。再加上高程中throttle函数和理解中的防抖一致,因此很容易让人混淆。查阅资料的时候很多人都表示高程上throttle函数实现的其实是防抖动,函数命名错误。可是换种理解:防抖和节流都属于节流技术,它们的基本思想是同样的:函数

函数节流背后的基本思想是指,某些代码不能够在没有间断的状况连续重复执行。(《JavaScript高级程序设计》)优化

高程原文的重点在于setTimeout优化函数执行频率,这种状况下不用去刻意区分防抖和节流的区别。ui

函数防抖

规定间隔不超过n毫秒的连续调用内只有一次调用生效。

咱们来模拟文本输入搜索:

function searchAjax(query) {
    console.log(`Results for "${query}"`);
}
document.getElementById("searchInput").addEventListener("input", function(event) {
    searchAjax(event.target.value);
});
复制代码

运行上面一段代码:

搜索“珍珠奶茶”

在用户输入结束以前,input事件调用了15次搜索请求。实际上,只有字符输入结束那一刻的input事件的搜索请求才是有用的(中文输入法尤其明显),其他都是在白白浪费资源。

接下来看一下添加防抖以后的效果:

function debounce(func, wait) {
    let timeId;
    return function(...args) {
    	let _this = this;
        clearTimeout(timeId);
        timeId = setTimeout(function() {
            func.apply(_this, args);
        }, wait);
    }
}
let searchDebounce = debounce(function(query) {
    console.log(`Results for "${query}"`);
}, 500);
document.getElementById("searchInput").addEventListener("input", function(event) {
    searchDebounce(event.target.value);
});
复制代码

运行结果以下:

防抖效果

用户输入结束前,不会再频繁的调用搜索请求了,只保留最关键的一次“珍珠奶茶”搜索。

debounce函数接收searchAjax方法,并将它包装成拥有防抖能力的searchDebounce方法:设置调用延时为0.5s,若是延时过程当中searchDebounce被再次调用,从新延时0.5s,直至有明显的停顿(searchDebounce没有再次被调用),这才开始处理搜索请求(searchAjax)。

函数被调用后延迟n毫秒执行,在等待过程当中若是函数再次被调用,从新计时。

函数防抖(trailing)

leading和trailing

在文本输入搜索的例子中,搜索函数被延迟执行。不过并不是全部处理都须要延迟执行,比方说:submit按钮,用户更但愿提交动做在第一时间完成,而且能避免短期内的重复提交。

函数被调用后当即执行,而且在将来n毫秒内不重复执行,若是停顿过程当中函数被再次调用,从新计时。

函数防抖(leading)

根据函数执行的前后顺序,出现两种选项:先执行后等待为leading,先等待后执行为trailing

函数节流

规定n秒内函数只有一次调用生效。

以滚动加载内容为例:

页面滚动过程当中势必会高频触发scroll事件(滚动500px能够触发100+次scroll事件),若是每一个scroll事件都须要处理昂贵的计算,那么整个滚动体验可能会致使心态崩裂。

无限滚动过程当中debounce也起不了做用,由于只有明显的停顿debounce才会处理scroll事件。用户更但愿滚动过程流畅无感知,这须要咱们以合理的频率不断检查是否须要加载更多的内容。

function throttle(func, wait) {
    let lastTime, deferTimer;
    return function(...args) {
        let _this = this;
        let currentTime = Date.now();
        if (!lastTime || currentTime >= lastTime + wait) {
        	lastTime = currentTime;
            func.apply(_this, args);
        } else {
            clearTimeout(deferTimer);
            deferTimer = setTimeout(function() {
        		lastTime = currentTime;
                func.apply(_this, args);
            }, wait);
        }
    }
}

function addContent() {/*...*/}
$(document).on("scroll", throttle(addContent, 300));
复制代码

与debounce函数同样,throttle一样将addContent函数封装成带有节流功能的函数:设置延时时间为0.3s,每0.3s调用一次func函数。若是不知足调用的时间条件,使用定时器预留一次func调用备用,即便最终没达到0.3s,调用也能生效一次。

lodash源码解读

debounce

源码地址

为方便理解,如下是简化过的版本:

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
  }

  // 调用func
  function invokeFunc(time) {
    const args = lastArgs
    const thisArg = lastThis

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

  // 启动延时
  function startTimer(pendingFunc, wait) {
    return setTimeout(pendingFunc, wait)
  }

  // 延时开始前
  function leadingEdge(time) {
    lastInvokeTime = time
    // 启动延时
    timerId = startTimer(timerExpired, wait)
    // 若是是leading模式,延时前调用func
    return leading ? invokeFunc(time) : result
  }

  //计算剩余的延时时间:
  //1. 不存在maxWait:(上一次debouncedFunc调用后)延时不能超过wait
  //2. 存在maxWait:func调用不能被延时超过maxWait
  //根据这两种状况计算出最短期
  function remainingWait(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    const timeWaiting = wait - timeSinceLastCall

    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting
  }

  //判断当前时间是否能调用func:
  //1.首次调用debouncedFunc
  //2.距离上一次debouncedFunc调用后已延迟wait毫秒
  //3.func调用总延迟达到maxWait毫秒
  //4.系统时间倒退
  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
    //若是是trailing模式,调用func
    if (trailing && lastArgs) {
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined
    return result
  }

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

    lastArgs = args
    lastThis = this
    lastCallTime = time

    if (isInvoking) {
      //timerId不存在有两种缘由:
      //1. 首次调用
      //2. 上次延时调用结束
      if (timerId === undefined) {
        return leadingEdge(lastCallTime)
      }
      // 存在func调用最长延时限制时,执行func并启动下一次延时,可实现throttle
      if (maxing) {
        timerId = startTimer(timerExpired, wait)
        return invokeFunc(lastCallTime)
      }
    }
    if (timerId === undefined) {
      timerId = startTimer(timerExpired, wait)
    }
    return result
  }
  return debounced
}

export default debounce
复制代码

提示:理解debounce的时候能够暂时忽略maxWait,后面会解释maxWait的做用。

debouncedFunc是如何工做的:

  1. 首次调用

    执行leadingEdge函数,leading选项为true时表示在延时以前调用func,而后启动延时器。延时器的做用是:在延时结束以后执行trailingEdge函数,trailing选项为true时表示在延迟结束以后调用func,最终结束一次func调用延迟的过程。

  2. 再次调用

    若是上一次的func延时调用已经结束,再次执行leadingEdge函数来启动延时过程。不然,忽略这次调用。(若是设置了maxWait且当前知足调用的时间条件,那么当即调用func而且启动新的延时器)

若是leading和trailing选项同时为true,那么func在一次防抖过程能被调用屡次。

lodash在debounce的基础上添加了maxWait选项,用于规定func调用不能延迟超过maxWait毫秒,也就是说每段maxWait时间内func必定会被调用一次。因此只要设置了maxWait选项,那么效果就等同于函数节流了。这一点也能够经过lodash的throttle源码获得验证: throttle的wait做为debounce的maxWait传入。

throttle

源码地址

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
复制代码

参考资料:

《JavaScript高级程序设计》

Debouncing and Throttling Explained Through Examples

The Difference Between Throttling and Debouncing

相关文章
相关标签/搜索