Open lessfish opened 8 years ago
基础版最重要的是, 有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 恩, 这样就是接近underscore的方法的. 我的意思是原代码那样是会有隐患.
debounce 有种 hold 住的感觉,一个动作不停地被触发,但是又不停地被终止,两次触发之间的时间长于给定的时间段才会真正触发这个时间。 不断触发又终止的过程,其实有点像卡带一样,不停地在重复一个声音,但是这个声音刚出来就被终止刚出来就被终止,直到不再卡带才会顺畅的播放一次。 上面的代码再做修改,可以发现,其实 debounce() 函数被触发了很多次,不过 print 函数被不断地触发禁止,触发禁止...
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);
// 这里的 timeout 一定是 null 了吧
我感觉这里是这样的: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当成节流了,抱歉。 更新2:那么debounce函数里应该可以判断immediate,如果是true则不用储存context / args了,正如最新的jashkenas/underscore:master里的写法。
那么,同理我发现底下的if (callNow)
有问题,可能会造成context和args被提前释放:
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);
输出是:
VM94:63 call now: begin with ["ni hao"]
VM94:77 tick: 1 ["ni hao"]
VM94:66 call now: end with [1, 2]
VM94:85 first result tick-1 but d(1,2) returns empty
demo见https://jsfiddle.net/emx3zdd9/1/
话说您用的understore 1.8.3 和 现在的jashkenas/underscore:master (https://github.com/jashkenas/underscore/blob/97cfcbcbbcedf544a13127dcca3e0ddad94ff830/underscore.js) 差了很多啊,_.debounce 完全被重写了。
我有个疑问是,master上的debounce已经在每次进入时就clearTimeout了,和您的“性能优化”的解释不一样,请问这两个方案的真正差别是什么?是应用场景导致的取舍吗?
<input>.oninput
做debounce (初步认为300ms比较好),已知用户打字够快且有时会连续输入大段文本,请问是否该每次清理计时器呢?@joesonw 基础方法确实有隐患,如果传入的method是一个匿名函数,绑定到匿名函数的timer将不会被清理掉
@gitwd 请问为什么匿名函数timer不会被清理?区别在哪里呢?
最显而易见的是基础版每此触发事件都会取消定时器,然后重新设置定时器,而 underscore 中会在一定时间后才
取消定时器,重新设置定时器
我认为你这儿说的有问题,setTimeout是不精准延时,debounce里面补充判断如果last在[0,wait)区间,则继续setTimeout一个wait-last的时间再执行函数,保证函数执行程序一定在延时了wait之后执行。
@hanzichi 韩老师你好!我想请教一下一个问题。
在您所阅读的underscore源码中(1.8.3),假设有如下代码:
var a = _.debounce((a)=>{
console.log(a);
}, 5000);
a(1);
a(2);
a(3);
a(4);
a(5);
a(6);
我的理解是:
a只有第一次被调用时才会进入later函数,但每次调用a都会更新时间戳Timestamp,而later内部会计算时间差,时间差不足时,递归调用later计算时间差,一旦时间差足够就触发传入的异步函数,最终执行的还是只有最后一个a函数。不知道正不正确?
我现在阅读的源码是最新版的,其中的_.debounce函数已经完全改进了,不再依赖于计算时间差,而是利用了JavaScript的异步机制:
假设有同样一段代码在最新版underscore中执行:
var a = _.debounce((a)=>{
console.log(a);
}, 5000);
a(1);
a(2);
a(3);
a(4);
a(5);
a(6);
我是否可以这样理解:
JavaScript优先执行完执行队列中的同步代码(以上所有代码)之后,再去执行事件队列中的异步代码。上方程序在执行所有同步代码时,每次a函数被调用,都会clearTimeout取消事件队列中的异步任务,导致前文a函数设置的异步任务被取消,直到最后一个a函数被执行时,才会开始计时,最终执行的也会是最后一个a函数。
两者相比较而言,后者使用变量更少,递归调用更少,数据计算更少;利用了JavaScript的异步机制,使用较少的代码较为自然的实现了去抖功能。
@hanzichi 韩老师您的文中还有一处小小的笔误:
// 设置 wait seconds 后触发 later 方法 // 无论是否 callNow(如果是 callNow,也进入 later 方法,去 later 方法中判断是否执行相应回调函数) // 在某一段的连续触发中,只会在第一次触发时进入这个 if 分支中 if (!timeout) // 设置了 timeout,所以以后不会进入这个 if 分支了 timeout = setTimeout(later, wait);
第一行注释中,wait seconds是否应该改为wait milliseconds? 冒昧啦!
@gdh1995 请问为什么匿名函数timer不会被清理?区别在哪里呢? /****分割线****/ 因为每次执行throttle 都会创建一个新的匿名函数, 匿名函数身上没有tId.
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)