underscore 函数节流的实现

Throttle 上文 我们聊了聊函数去抖(debounce),去抖的作用简单说是 使连续的函数执行降低到一次 (通常情况下此函数为 DOM

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

未登录用户
全部评论0
到底啦