underscore 函数节流的实现-演道网
Throttle
上文 我们聊了聊函数去抖(debounce),去抖的作用简单说是 使连续的函数执行降低到一次(通常情况下此函数为 DOM 事件的回调函数),核心实现也非常简单,重复添加定时器即可(具体可以参考 上文)。本文我们聊聊函数节流(throttle)。
简单的说,函数节流能使得连续的函数执行,变为 固定时间段 间断地执行。
还是以 scroll 事件为例,如果不加以节流控制:
window.onscroll = function() { console.log('hello world'); };
轻轻滚动下窗口,控制台打印了 N 多个 hello world 字符串。如果 scroll 回调不是简单的打印字符串,而是涉及一些 DOM 操作,这样频繁的调用,低版本浏览器可能就会直接假死,我们希望回调可以间隔时间段触发,比如上面的例子每 1000ms 打印一次,如何实现之?
大概有两种方式(underscore 也并用了这两种方式)。其一是用时间戳来判断是否已到回调该执行时间,记录上次执行的时间戳,然后每次触发 scroll 事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经到达 1000ms,如果是,则执行,并更新上次执行的时间戳,如此循环;第二种方法是使用定时器,比如当 scroll 事件刚触发时,打印一个 hello world,然后设置个 1000ms 的定时器,此后每次触发 scroll 事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler 被清除,然后重新设置定时器。
underscore 实现
如果是一般的使用场景,则上面的两个方式大同小异,都可以应用,但是 underscore 考虑了高级配置,即可以选择是否需要响应事件刚开始的那次回调(配置 leading 参数),以及事件结束后的那次回调(配置 trailing 参数)。 还是以 scroll 举例,设置 1000ms 触发一次,并且不配置 leading 和 trailing 参数,那么 scroll 开始的时候会响应回调,scroll 停止后还会触发一次回调。如果配置 {leading: false},那么 scroll 开始的那次回调会被忽略,如果配置 {trailing: false},那么 scroll 结束的后的那次回调会被忽略。需要特别注意的是,两者不能同时配置!
所以说,underscore 的函数节流有三种调用方式,默认的(有头有尾),设置 {leading: false} 的,以及设置 {trailing: false} 的。再来看上面说的 throttle 的两种实现,第一种方式有缺陷,当事件停止触发时,便不能响应回调,所以如果没有设置 {trailing: false} (需要执行最后一次方法)也不能执行最后一次方法,这时我们需要用到定时器;而单纯的定时器方式,也有漏洞,因为使用了定时器延迟执行,所以当事件触发结束时还存在定时器,{trailing: false} 设置无法生效(还会执行最后一次方法)。所以我们需要两者并用。
上 underscore 源码,包含大量注释:
// Returns a function, that, when invoked, will only be triggered at most once // during a given window of time. Normally, the throttled function will run // as much as it can, without ever going more than once per `wait` duration; // but if you'd like to disable the execution on the leading edge, pass // `{leading: false}`. To disable execution on the trailing edge, ditto. // 函数节流(如果有连续事件响应,则每间隔一定时间段触发) // 每间隔 wait(Number) milliseconds 触发一次 func 方法 // 如果 options 参数传入 {leading: false} // 那么不会马上触发(等待 wait milliseconds 后第一次触发 func) // 如果 options 参数传入 {trailing: false} // 那么最后一次回调不会被触发 // **Notice: options 不能同时设置 leading 和 trailing 为 false** // 示例: // var throttled = _.throttle(updatePosition, 100); // $(window).scroll(throttled); // 调用方式(注意看 A 和 B console.log 打印的位置): // _.throttle(function, wait, [options]) // sample 1: _.throttle(function(){}, 1000) // print: A, B, B, B ... // sample 2: _.throttle(function(){}, 1000, {leading: false}) // print: B, B, B, B ... // sample 3: _.throttle(function(){}, 1000, {trailing: false}) // print: A, A, A, A ... // ----------------------------------------- // _.throttle = function(func, wait, options) { var context, args, result; // setTimeout 的 handler var timeout = null; // 标记时间戳 // 上一次执行回调的时间戳 var previous = 0; // 如果没有传入 options 参数 // 则将 options 参数置为空对象 if (!options) options = {}; var later = function() { // 如果 options.leading === false // 则每次触发回调后将 previous 置为 0 // 否则置为当前时间戳 previous = options.leading === false ? 0 : _.now(); timeout = null; // console.log('B') result = func.apply(context, args); // 这里的 timeout 变量一定是 null 了吧 // 是否没有必要进行判断? if (!timeout) context = args = null; }; // 以滚轮事件为例(scroll) // 每次触发滚轮事件即执行这个返回的方法 // _.throttle 方法返回的函数 return function() { // 记录当前时间戳 var now = _.now(); // 第一次执行回调(此时 previous 为 0,之后 previous 值为上一次时间戳) // 并且如果程序设定第一个回调不是立即执行的(options.leading === false) // 则将 previous 值(表示上次执行的时间戳)设为 now 的时间戳(第一次触发时) // 表示刚执行过,这次就不用执行了 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 // ========= // // remaining > wait,表示客户端系统时间被调整过 // 则马上执行 func 函数 // @see https://blog.coding.net/blog/the-difference-between-throttle-and-debounce-in-underscorejs // ========= // // console.log(remaining) 可以打印出来看看 if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); // 解除引用,防止内存泄露 timeout = null; } // 重置前一次触发的时间戳 previous = now; // 触发方法 // result 为该方法返回值 // console.log('A') result = func.apply(context, args); // 引用置为空,防止内存泄露 // 感觉这里的 timeout 肯定是 null 啊?这个 if 判断没必要吧? if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // 最后一次需要触发的情况 // 如果已经存在一个定时器,则不会进入该 if 分支 // 如果 {trailing: false},即最后一次不需要触发了,也不会进入这个分支 // 间隔 remaining milliseconds 后触发 later 方法 timeout = setTimeout(later, remaining); } // 回调返回值 return result; }; };
调用也是非常的简单:
function log() { console.log('hello world'); } window.onscroll = _.throttle(log, 1000); window.onscroll = _.throttle(log, 1000, {leading: false}); window.onscroll = _.throttle(log, 1000, {trailing: false});
有兴趣的可以琢磨下它是如何实现两种方式并用的,可以将我代码块中的三处注释打开看下(分别打印了 A,B 以及 remaining )。
Read more
转载自演道,想查看更及时的互联网产品技术热点文章请点击http://go2live.cn