网站开发中经常会遇到一些高频事件操作的需求,比如,通过不停的监听页面滚动事件,来加载可视区的图片,又或者在拖动鼠标时,不断的获取光标的坐标,从而改变模块的位置。

像鼠标滚动、拖动或者键盘输入这些操作,它们都是浏览器的默认行为。这些行为更新的频率取决于各个浏览器生产商,虽然各有差异,但有一点,这些事件操作更新的频率都很高。

当我们在滚动事件上绑定一个事件处理函数,只要轻轻滚动页面,你会发现,其对应的事件处理函数执行了好多次。

如果,根据浏览器默认的事件更新频率来执行对应的事件处理函数(这些函数可能是操作一大堆DOM,或请求接口),那么,页面性能将大大降低。

虽然,我们无法改变浏览器的默认行为,但可以换一种方式,在相同次数的事件操作中,尽量减少事件处理函数的执行次数。

这种减少执行次数的实践,也就是我下面要说的 函数防抖函数节流

函数防抖

debounce函数防抖。表示经过指定周期后,才会触发事件处理函数。如果在该指定周期内试图触发事件处理函数,那么,这个指定周期会被重新计时。

概念有点绕?没关系。我们可以用代码来说明下:

var debounce = function(fn, delay) {
clearTimeout(fn.timer);
fn.timer = setTimeout(fn, delay);
};

上面的代码中,fn 表示事件对应的处理函数,而 delay 则表示指定的周期,然后,将一个定时器挂载到事件处理函数上。

当我们进行事件操作企图触发 fn 时,该函数会先清除原来的定时器,接着,再重新开启一个新的定时器,然后,经过 delay 这么一个指定周期内,最终执行 fn

倘若,debounce 函数被事件在指定周期内重复触发,则表示定时器会被重新计时。

比如,当我们缩放窗口,企图改变页面布局时,那么,便可以使用 函数防抖:

window.addEventListener('resize', function() {
debounce(render, 500);
}, false);

正常情况应该是这样的,当用户停止缩放窗口,并经过 500ms 后, render 函数才会被触发。

但如果用户一直在缩放窗口,则 throttle 函数将一直被触发的。并且 throttle 里面的定时器也将一直被清除,一直被重新计时,直到用户停止缩放窗口,经过 500ms 后,render 函数才会被触发。

函数防抖的主要应用场景有:缩放窗口改变布局、联想输入。

函数节流

throttle函数节流。表示指定周期内,事件处理函数必须触发一次。如果该函数两次触发的时间大于等于这个指定周期,则立马触发该函数,并进入下一个计时周期。

也就是说,在每个指定周期内,事件处理函数都会间隔的被触发。

函数防抖 一样,函数节流的目的也是减少事件处理函数的执行次数,旨在提高页面操作性能。

或许你会疑惑,有了 函数防抖 ,为什么还需要 函数节流 呢?

那是因为你可能还没遇到过交互时效性较高的操作。

比如说,当我们拖动模块,如果使用 函数防抖,那么,在用户停止拖动鼠标前,定时器将会一直被清除,而模块的位置也不会有任何改变,直到用户停止拖动,模块才会瞬间移动到鼠标停留的位置。那么,这就会导致模块从起点 “蹭” 的一下移动到终点,完全没有流畅性可言。

因此,对于这种情况,为了保证交互的流畅性,我们必须让事件处理函数每隔一个指定的时间周期内必须执行一次。

这就是出现 函数节流 的原因。

现在,用代码来阐述上面的定义:

var throttle = function(fn, delay) {
var previous = +new Date();

return function() {
var now = +new Date();

if (now - previous >= delay) {
previous = now;
fn();
}
};
};

在该函数中,也有两个参数,即要触发的事件处理函数 fn 和指定的周期 delay 。函数体内,首先使用 previours 记录了 throttle 函数执行的时间,接着,返回一个匿名函数,它是一个闭包,一直保留着对 previours 的引用。在匿名函数中,又通过 now 来记录匿名函数执行的时间。然后,通过判断这两个时间点的差值,是否大于指定的周期 delay,如果大于等于,则更新 previous 的值,并且紧接着执行事件处理函数 fn

比如,我们将 函数节流 应用到滚动加载图片上,则代码可以是这样的:

window.addEventListener('scroll', throttle(loadImg, 100), false);

页面载入后,throttle 函数便立马执行,其中 previours 记录了执行的时间。并且该函数返回一个匿名函数给滚动事件,当页面不停滚动时,匿名函数也将不停的执行,只要在滚动的某个时刻,nowprevious 的差值大于 100ms(delay),则更新 previous 的值,并触发事件处理函数 fn

总的来说,函数节流适合一些时效性较强的交互。

函数节流的主要应用场景有:滚动页面加载图片、模块的拖拽、双屏互动

两者的异同

相同点

其实,这两者的相同点是显而易见的。它们都是针对频繁的事件操作所引发的性能问题,旨在减少对应的事件处理函数所执行的次数,达到提升页面操作性能所提出的概念。

不同点

通过上文的描述,可以这么简单理解:

throttle 有点像 setInterval,在每一个所指定的时间周期内,就得执行事件处理函数 。而 debounce 则有点类似 setTimeout,得经过指定的时间周期,并在该周期内,对应的事件处理函数没被触发,才会执行事件处理函数。

只不过,setIntervalsetTimeout 是一次性触发的(比如 鼠标点击)。

throttledebounce 则需要不断的进行人为的事件驱动以及干预,才能执行(比如 页面滚动、窗口缩放、键盘输入)。

另一方面,如果从两者的事件处理函数执行的时间上考虑,throttle 对应的事件处理函数,几乎每次触发的时间都差不多。但 debounce 所对应的事件处理函数,则取决于用户的事件操作什么时候停下来。如果不停,那么指定周期又会被重新计时。