上一节咱们详细聊了聊高阶函数之柯里化,经过介绍其定义和三种柯里化应用,并在最后实现了一个通用的 currying 函数。这一小节会继续以前的篇幅聊聊函数节流 throttle,给出这种高阶函数的定义、实现原理以及在 underscore 中的实现,欢迎你们拍砖。前端
有什么想法或者意见均可以在评论区留言,下图是本文的思惟导图,高清思惟导图和更多文章请看个人 Github。git
函数节流指的是某个函数在必定时间间隔内(例如 3 秒)只执行一次,在这 3 秒内 无视后来产生的函数调用请求,也不会延长时间间隔。3 秒间隔结束后第一次遇到新的函数调用会触发执行,而后在这新的 3 秒内依旧无视后来产生的函数调用请求,以此类推。github
举一个小例子,不知道你们小时候有没有养太小金鱼啥的,养金鱼确定少不了接水,刚开始接水时管道中水流很大,水到半满时开始拧紧水龙头,减小水流的速度变成 3 秒一滴,经过滴水给小金鱼增长氧气。面试
此时「管道中的水」就是咱们频繁操做事件而不断涌入的回调任务,它须要接受「水龙头」安排;「水龙头」就是节流阀,控制水的流速,过滤无效的回调任务;「滴水」就是每隔一段时间执行一次函数,「3 秒」就是间隔时间,它是「水龙头」决定「滴水」的依据。性能优化
若是你还没法理解,看下面这张图就清晰多了,另外点击 这个页面 查看节流和防抖的可视化比较。其中 Regular 是不作任何处理的状况,throttle 是函数节流以后的结果,debounce 是函数防抖以后的结果(下一小节介绍)。闭包
函数节流很是适用于函数被频繁调用的场景,例如:window.onresize() 事件、mousemove 事件、上传进度等状况。使用 throttle API 很简单,那应该如何实现 throttle 这个函数呢?app
实现方案有如下两种less
这里咱们采用第一种方案来实现,经过闭包保存一个 previous 变量,每次触发 throttle 函数时判断当前时间和 previous 的时间差,若是这段时间差小于等待时间,那就忽略本次事件触发。若是大于等待时间就把 previous 设置为当前时间并执行函数 fn。前端性能
咱们来一步步实现,首先实现用闭包保存 previous 变量。函数
const throttle = (fn, wait) => { // 上一次执行该函数的时间 let previous = 0 return function(...args) { console.log(previous) ... } }
执行 throttle 函数后会返回一个新的 function,咱们命名为 betterFn。
const betterFn = function(...args) { console.log(previous) ... }
betterFn 函数中能够获取到 previous 变量值也能够修改,在回调监听或事件触发时就会执行 betterFn,即 betterFn()
,因此在这个新函数内判断当前时间和 previous 的时间差便可。
const betterFn = function(...args) { let now = +new Date(); if (now - previous > wait) { previous = now // 执行 fn 函数 fn.apply(this, args) } }
结合上面两段代码就实现了节流函数,因此完整的实现以下。
// fn 是须要执行的函数 // wait 是时间间隔 const throttle = (fn, wait = 50) => { // 上一次执行 fn 的时间 let previous = 0 // 将 throttle 处理结果看成函数返回 return function(...args) { // 获取当前时间,转换成时间戳,单位毫秒 let now = +new Date() // 将当前时间和上一次执行函数的时间进行对比 // 大于等待时间就把 previous 设置为当前时间并执行函数 fn if (now - previous > wait) { previous = now fn.apply(this, args) } } } // DEMO // 执行 throttle 函数返回新函数 const betterFn = throttle(() => console.log('fn 函数执行了'), 1000) // 每 10 秒执行一次 betterFn 函数,可是只有时间差大于 1000 时才会执行 fn setInterval(betterFn, 10)
上述代码实现了一个简单的节流函数,不过 underscore 实现了更高级的功能,即新增了两个功能
配置 { leading: false } 时,事件刚开始的那次回调不执行;配置 { trailing: false } 时,事件结束后的那次回调不执行,不过须要注意的是,这二者不能同时配置。
因此在 underscore 中的节流函数有 3 种调用方式,默认的(有头有尾),设置 { leading: false } 的,以及设置 { trailing: false } 的。上面说过实现 throttle 的方案有 2 种,一种是经过时间戳判断,另外一种是经过定时器建立和销毁来控制。
第一种方案实现这 3 种调用方式存在一个问题,即事件中止触发时没法响应回调,因此 { trailing: true } 时没法生效。
第二种方案来实现也存在一个问题,由于定时器是延迟执行的,因此事件中止触发时必然会响应回调,因此 { trailing: false } 时没法生效。
underscore 采用的方案是两种方案搭配使用来实现这个功能。
const throttle = function(func, wait, options) { var timeout, context, args, result; // 上一次执行回调的时间戳 var previous = 0; // 无传入参数时,初始化 options 为空对象 if (!options) options = {}; var later = function() { // 当设置 { leading: false } 时 // 每次触发回调函数后设置 previous 为 0 // 否则为当前时间 previous = options.leading === false ? 0 : _.now(); // 防止内存泄漏,置为 null 便于后面根据 !timeout 设置新的 timeout timeout = null; // 执行函数 result = func.apply(context, args); if (!timeout) context = args = null; }; // 每次触发事件回调都执行这个函数 // 函数内判断是否执行 func // func 才是咱们业务层代码想要执行的函数 var throttled = function() { // 记录当前时间 var now = _.now(); // 第一次执行时(此时 previous 为 0,以后为上一次时间戳) // 而且设置了 { leading: false }(表示第一次回调不执行) // 此时设置 previous 为当前值,表示刚执行过,本次就不执行了 if (!previous && options.leading === false) previous = now; // 距离下次触发 func 还须要等待的时间 var remaining = wait - (now - previous); context = this; args = arguments; // 要么是到了间隔时间了,随即触发方法(remaining <= 0) // 要么是没有传入 {leading: false},且第一次触发回调,即当即触发 // 此时 previous 为 0,wait - (now - previous) 也知足 <= 0 // 以后便会把 previous 值迅速置为 now if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); // clearTimeout(timeout) 并不会把 timeout 设为 null // 手动设置,便于后续判断 timeout = null; } // 设置 previous 为当前时间 previous = now; // 执行 func 函数 result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // 最后一次须要触发的状况 // 若是已经存在一个定时器,则不会进入该 if 分支 // 若是 {trailing: false},即最后一次不须要触发了,也不会进入这个分支 // 间隔 remaining milliseconds 后触发 later 方法 timeout = setTimeout(later, remaining); } return result; }; // 手动取消 throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = context = args = null; }; // 执行 _.throttle 返回 throttled 函数 return throttled; };
节流能够理解为养金鱼时拧紧水龙头放水,3 秒一滴
节流实现方案有 2 种
underscore.js
若是你以为这篇内容对你挺有启发,我想邀请你帮我三个小忙: