本系列使用 lodash 4.17.4

前言

本文件引用了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,lastCallTimelet lastInvokeTime = 0let leading = falselet maxing = falselet trailing = trueif (typeof func != 'function') {throw new TypeError('Expected a function')}wait = +wait || 0if (isObject(options)) {leading = !!options.leadingmaxing = 'maxWait' in optionsmaxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWaittrailing = 'trailing' in options ? !!options.trailing : trailing}function invokeFunc(time) {const args = lastArgsconst thisArg = lastThislastArgs = lastThis = undefinedlastInvokeTime = timeresult = 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 - lastCallTimeconst timeSinceLastInvoke = time - lastInvokeTimeconst timeWaiting = wait - timeSinceLastCallreturn maxing? Math.min(timeWaiting, maxWait - timeSinceLastInvoke): timeWaiting}function shouldInvoke(time) {const timeSinceLastCall = time - lastCallTimeconst 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 = undefinedreturn result}function cancel() {if (timerId !== undefined) {clearTimeout(timerId)}lastInvokeTime = 0lastArgs = 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 = argslastThis = thislastCallTime = timeif (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 = canceldebounced.flush = flushdebounced.pending = pendingreturn debounced
}export default debounce复制代码

使用方式

函数防抖(debounce)

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

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

图片来自css-tricks

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

图片来自css-tricks

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

函数节流(throttle)

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

比如你设置了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       // 记录最后一次调用的thislastCallTime = 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) {// 第一次触发的时候没有加timerif (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是否超过设置的waitif (shouldInvoke(time)) {// 实际调用函数部分return trailingEdge(time)}// 如果timeSinceLastCall还没超过设置的wait,重置定时器之后再进一遍timerExpiredtimerId = 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 = undefinedreturn result}
复制代码

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

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

throttle.js

function throttle(func, wait, options) {let leading = truelet trailing = trueif (typeof func != 'function') {throw new TypeError('Expected a function')}if (isObject(options)) {leading = 'leading' in options ? !!options.leading : leadingtrailing = '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 = 0return function(){var time = +new Date()if (time - last > wait){func.apply(this, arguments)last = curr }}
}
复制代码

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

每日源码分析 - lodash(debounce.js和throttle.js)相关推荐

  1. 每日源码分析 - Lodash(remove.js)

    本系列使用 lodash 4.17.4版本 源码分析不包括引用文件分析 一.源码 import basePullAt from './.internal/basePullAt.js'/*** Remo ...

  2. 每日源码分析-Lodash(uniq.js)

    本系列使用lodash 4.17.4 前言 引用internal文件下的baseUniq.js 正文 import baseUniq from './.internal/baseUniq.js'/** ...

  3. redux源码分析之一:createStore.js

    欢迎关注redux源码分析系列文章: redux源码分析之一:createStore.js redux源码分析之二:combineReducers.js redux源码分析之三:bindActionC ...

  4. lodash源码分析之获取数据类型

    所有的悲伤,总会留下一丝欢乐的线索,所有的遗憾,总会留下一处完美的角落,我在冰峰的深海,寻找希望的缺口,却在惊醒时,瞥见绝美的阳光! --几米 本文为读 lodash 源码的第十八篇,后续文章会更新到 ...

  5. Vue.js 源码分析(二十三) 指令篇 v-show指令详解

    v-show的作用是将表达式值转换为布尔值,根据该布尔值的真假来显示/隐藏切换元素,它是通过切换元素的display这个css属性值来实现的,例如: <!DOCTYPE html> < ...

  6. lodash源码分析之compact中的遍历

    小时候, 乡愁是一枚小小的邮票, 我在这头, 母亲在那头. 长大后,乡愁是一张窄窄的船票, 我在这头, 新娘在那头. 后来啊, 乡愁是一方矮矮的坟墓, 我在外头, 母亲在里头. 而现在, 乡愁是一湾浅 ...

  7. 深入理解 Node.js 中 EventEmitter源码分析(3.0.0版本)

    events模块对外提供了一个 EventEmitter 对象,即:events.EventEmitter. EventEmitter 是NodeJS的核心模块events中的类,用于对NodeJS中 ...

  8. Vue.js 源码分析(九) 基础篇 生命周期详解

    先来看看官网的介绍: 主要有八个生命周期,分别是: beforeCreate.created.beforeMount.mounted.beforeupdate.updated   .beforeDes ...

  9. lodash源码分析之baseFindIndex中的运算符优先级

    我悟出权力本来就是不讲理的--蟑螂就是海米:也悟出要造反,内心必须强大到足以承受任何后果才行. --北岛<城门开> 本文为读 lodash 源码的第十篇,后续文章会更新到这个仓库中,欢迎 ...

最新文章

  1. Kafka:你必须要知道集群内部工作原理的一些事!
  2. 软件测试报告重点审核点有哪些,软件测试-测试报告.doc
  3. Spring Boot中使用多数据库
  4. CCIE-LAB-第八篇-SDWAN-Branch1_Branch2_Vmanage
  5. 在RHEL5/CentOS5上配置使用Open×××
  6. rpc调试工具grpcui的安装使用
  7. 重新学习java第一天
  8. 2021湖南永州四中高考成绩查询,2021湖南高中排名一览表 最新排名
  9. LeetCode 669. 修剪二叉搜索树(Trim a Binary Search Tree)
  10. Angular2 的 View Encapsulation(样式封装)
  11. 拓端tecdat|R语言时间序列分析复杂的季节模式
  12. Atitit 法学处罚方式模式 目录 1. 申诫罚、财产罚和能力罚 1 1.1. 申诫罚 (警告和通报批评 ) 1 1.2. 财产罚是指使被处罚人的财产权利和利益受到损害的行政处罚。 2 1.2
  13. 一键部署office的工具——OTool
  14. rxbus 源码_从 RxBus 这辆兰博基尼深入进去
  15. 通过命令行安装egret引擎
  16. 2012考研数学二第(18)题——多元函数积分学:二重积分求面积+画曲线:心形线
  17. android svg 线条动画教程,SVG技术入门:线条动画实现原理
  18. quick Cocos2dx lua 接anysdk
  19. 好佳居窗帘十大品牌-窗帘这样搭才好看
  20. 网络安全--SQL注入介绍

热门文章

  1. 嵌入式处理器的体系架构与内核详解
  2. 结构型模式:外观模式
  3. nginx 认证多个客户端的问题
  4. MyBatis逆向工程:根据table生成Model、Mapper、Mapper.xml
  5. Arduino教程:MPU6050的数据获取、分析与处理
  6. Linux的企业-Mfs高可用corosync+pacemaker+fence+iscci
  7. 虚方法(virtual)和抽象方法(abstract)的区别
  8. Unix高级编程之文件权限
  9. html表格自动换行
  10. Uchome的登录验证机制