节流与防抖的作用都是防止函数多次调用。区别在于,假如用户一直触发这个函数,且每次触发函数的间隔小于阙值,防抖的情况下只会调用一次,而节流会每隔一定时间调用函数。
函数防抖
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
<!DOCTYPE html>
<html>
<head>
<title>节流与防抖</title>
<meta charset="utf-8">
<style>
#container{
width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="text/javascript">
var count = 1;
var container = document.getElementById('container');
function getUserAction() {
container.innerHTML = count++;
};
// container.onmousemove = getUserAction;
container.onmousemove = debounce(getUserAction, 1000);
function debounce(func, wait) {
var timeout;
return function () {
clearTimeout(timeout)
timeout = setTimeout(func, wait);
}
}
</script>
</body>
</html>
这里核心思路就是建立一个定时器,重写onmousemove方法,一旦触发onmousemove,先清除定时器,随后再开一个定时器,指定时间后执行函数。这就实现了一个简易的防抖,指定时间内重复触发函数只会执行一次。已经很好的解决了高频重复触发的问题,但因为直接重写了onmousemove函数,this与event均丢失了。
先解决this指向问题,正常在onmousemove函数中,this指向的应该是调用函数的对象,也就是container,因为在debounce中返回了真正重写onmousemove的函数,形成了一个闭包,导致this丢失。指定this指向的方法有apply与call,用法差别只在参数,call需要的是一个数组,这里就用apply。
修改后的debounce函数为:
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context)
}, wait);
}
}
解决event参数,正常在onmousemove函数中有一个event,指向当前的事件对象。因为debounce函数返回的是一个匿名函数,可以通过arguments属性来获取参数,该属性是一个由函数参数组成的类数组。
修改后的debounce:
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
函数节流
规定在一个单位时间内,只能触发一次函数。如果这个函数单位时间内触发多次函数,只有一次生效。
根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。 我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。
关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
-
使用时间戳
当时间触发时,我们取出当前的时间戳,然后减去之前的时间戳,如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
function throttle(func, wait) { var context, args; var previous = 0; return function() { var now = +new Date(); context = this; args = arguments; if (now - previous > wait) { func.apply(context, args); previous = now; } }
-
使用定时器
当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。
function throttle(func, wait) { var timeout; var previous = 0; return function() { context = this; args = arguments; if (!timeout) { timeout = setTimeout(function(){ timeout = null; func.apply(context, args) }, wait) } } }
这两种方式都是将函数放到定时器里,js并不会立即去执行该函数,因为js是单线程的,定时器里的函数属于异步函数,异步函数会被放置在事件队列中,当同步代码执行完后,才会依次执行事件队列中的代码。如果需要立即执行改函数,就需要小小的改动下:
function throttle(func, wait) { var timeout, context, args, result; var previous = 0; var later = function() { previous = +new Date(); timeout = null; func.apply(context, args) }; var throttled = function() { var now = +new Date(); //下次触发 func 剩余的时间 var remaining = wait - (now - previous); context = this; args = arguments; // 如果没有剩余的时间了或者你改了系统时间 if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; func.apply(context, args); } else if (!timeout) { timeout = setTimeout(later, remaining); } }; return throttled; }
这种方式有个比较巧妙地方就是他计算了下次触发函数的剩余时间,上面根据时间戳方式实现节流是直接比较当前时间戳与上一个时间戳,这里来用时间间隔减去当前时间戳与上一个时间戳之差,计算结果就是函数下次触发的剩余时间,如果这个剩余时间大于0,表明将要停止节流函数,立即清除定时器,立即执行函数。