-
Notifications
You must be signed in to change notification settings - Fork 642
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
underscore 函数去抖的实现 #21
Comments
基础版最重要的是, 有side effect吧. |
@joesonw 请教下 side effect 具体是? |
改变了输入值, 给function多加了属性. |
发现个问题,这个 clearTimeout() 直接让 setTimeout() 中的函数不执行,而不是调用 clearTimeout 之后立即执行里面的函数。也就是说,setTimeout() 的回调会在最后一次执行 debounce() 后起作用。这样就保证了只执行一次,就是节流啊。。。 |
@jsspace 我的理解是 「节流」(throttle)是控制函数执行的频率,而不是只执行一次(debounce) |
哦哦,我弄混了 |
throttle 和 debounce 的应用场景应该是分的很清楚的
|
@riskers 不错,给 throttle 应用加了这个 case |
@joesonw ,你说的 side effect 是可以消除的。其实不必要非得给 function 添加这个属性,只要是一个在 debounce 函数外部的变量就可以。高程三里的这个写法其实是可改成下面这个样子的: var timer = null;
function debounce(method, context) {
clearTimeout(timer);
timer = setTimeout(function() {
method.call(context);
}, 1000);
}
function print() {
console.log('hello world');
}
window.onscroll = function() {
debounce(print);
}; 为了避免对全局的污染,其实最好的方式是将 timer 放入函数中,成为一个局部变量,所以上面的写法可以改写成下面的方式: function debounce(method, context) {
var timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(function() {
method.call(context);
}, 1000);
}
}
function print() {
console.log('hello world');
}
window.onscroll = debounce(print); 从这个意义上讲,闭包其实就是用来将两个内容隔离用的,将 timer 放入函数中,那么就需要将原来的语句放入函数中,使其与 timer 隔离,最近返回这个函数。结果就会和原来的效果是一样的。 |
@oakland |
debounce 有种 hold 住的感觉,一个动作不停地被触发,但是又不停地被终止,两次触发之间的时间长于给定的时间段才会真正触发这个时间。 function debounce(method, context) {
var timer = null;
var n = 0;
return function() {
clearTimeout(timer);
timer = setTimeout(function() {
method.call(context);
}, 1000);
console.log(n++);
}
}
function print() {
console.log('hello world');
}
window.onscroll = debounce(print); |
我感觉这里是这样的:func作为用户传入的任意函数,有可能会反过来调用debounce返回的新函数,比如 var func, de, i = 0;
func = function() {
i++;
if (i < 10) {
console.log(i);
de();
// setTimeout(de, 10);
}
};
de = _.debounce(func, 40);
de(); 这个会输出1到9,改改条件应该就能出现 de -> func -> de这种嵌套调用了。 更新1:我才意识到我也把debounce当成节流了,抱歉。
var f, d, tick = 0;
f = function() {
console.log('tick:', ++tick, [].slice.call(arguments, 0));
if (tick === 1) {
return d(1, 2) || 'tick-1 but d(1,2) returns empty';
}
return 'tick-' + tick;
};
d = _.debounce(f, 100, true);
var ret1 = d('ni hao');
console.log('first result', ret1);
|
话说您用的understore 1.8.3 和 现在的jashkenas/underscore:master (https://github.com/jashkenas/underscore/blob/97cfcbcbbcedf544a13127dcca3e0ddad94ff830/underscore.js) 差了很多啊,_.debounce 完全被重写了。 我有个疑问是,master上的debounce已经在每次进入时就clearTimeout了,和您的“性能优化”的解释不一样,请问这两个方案的真正差别是什么?是应用场景导致的取舍吗?
|
@joesonw 基础方法确实有隐患,如果传入的method是一个匿名函数,绑定到匿名函数的timer将不会被清理掉 |
@gitwd 请问为什么匿名函数timer不会被清理?区别在哪里呢? |
我认为你这儿说的有问题,setTimeout是不精准延时,debounce里面补充判断如果last在[0,wait)区间,则继续setTimeout一个wait-last的时间再执行函数,保证函数执行程序一定在延时了wait之后执行。 |
@hanzichi 韩老师你好!我想请教一下一个问题。 在您所阅读的underscore源码中(1.8.3),假设有如下代码:
我的理解是: a只有第一次被调用时才会进入later函数,但每次调用a都会更新时间戳Timestamp,而later内部会计算时间差,时间差不足时,递归调用later计算时间差,一旦时间差足够就触发传入的异步函数,最终执行的还是只有最后一个a函数。不知道正不正确? 我现在阅读的源码是最新版的,其中的_.debounce函数已经完全改进了,不再依赖于计算时间差,而是利用了JavaScript的异步机制: 假设有同样一段代码在最新版underscore中执行:
我是否可以这样理解: JavaScript优先执行完执行队列中的同步代码(以上所有代码)之后,再去执行事件队列中的异步代码。上方程序在执行所有同步代码时,每次a函数被调用,都会clearTimeout取消事件队列中的异步任务,导致前文a函数设置的异步任务被取消,直到最后一个a函数被执行时,才会开始计时,最终执行的也会是最后一个a函数。 两者相比较而言,后者使用变量更少,递归调用更少,数据计算更少;利用了JavaScript的异步机制,使用较少的代码较为自然的实现了去抖功能。 |
@hanzichi 韩老师您的文中还有一处小小的笔误:
第一行注释中,wait seconds是否应该改为wait milliseconds? |
@gdh1995 请问为什么匿名函数timer不会被清理?区别在哪里呢? |
underscore版虽然不用每次触发时都清除计时器,但是每次触发时也使用Date对象重新生成了一个时间戳呀。 |
前文 我们对 JavaScript 中的函数节流和函数去抖的概念和应用场景进行了简单的了解,本文我们来深入探究下函数去抖的实现。(不懂函数去抖概念的建议看下前文 JavaScript 函数节流和函数去抖应用场景辨析 )
我们以 scroll 事件为例,探究如何实现滚动一次窗口打印一个 hello world 字符串。
如果不对其进行节流或者去抖控制:
这样每滚动一次,实际上会打印 N 多个 hello world。函数去抖背后的基本思想是指,某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。
《高程三》给出了最简洁最经典的去抖代码(书中说是节流,实则为去抖),调用如下:
在窗口内滚动一次,停止,1000ms 后,打印了 hello world,因为我们设置了一个 1000ms 延迟的定时器,细思非常巧妙。
underscore 在其基础上进行了扩充,直接看代码,含大量注释:
等等,一下子多了这么多代码,那么我们比基础版多了哪些功能(优势)呢?
首先,基础版能做的,我们一样能做,一样让它在连续滚动后停止的 1000ms 后打印 hello world:
我们还可以在滚动刚触发的时候打印字符串,而不是连续滚动结束后,只需传入第三个参数,会自动忽略第二个参数:
这样对于连续的滚动,也只会打印一次,但是是在事件第一次触发的时候。
回调函数需要传入参数?一点问题都没有。
当然,除了功能上的优势,性能也是提高不少,最显而易见的是基础版每此触发事件都会取消定时器,然后重新设置定时器,而 underscore 中会在一定时间后才
取消定时器,重新设置定时器。其他更多可以细究下源码。(对性能有兴趣的可以看看这个 pr https://github.com/jashkenas/underscore/pull/1269)The text was updated successfully, but these errors were encountered: