一、什么是节流?
节流就是一定时间内,一个动作只执行一次。
二、为什么要节流?
js有一些事件触发的频率肥肠高。比如:调整窗口事件(resize)、滚动条事件(scroll)、鼠标悬浮事件(mouseover、mouseout、mouseenter、mouseleave)、键盘事件(keyup、keydown)等等,如果在这些事件的回调函数中有ajax请求、动画和dom的操作,那不仅会增加服务器负担,而且会造成页面抖动、卡死甚至崩溃。
三、如何实现节流?
常见的方案是用setTimeout定时器来实现,先来看一个简易版的。
function throttle(fn,wait){
var timer = null;
return function(){
var context = this;
var args = arguments;
console.log('timer>>',timer)
if(!timer){
timer = setTimeout(function(){
fn.apply(context,args);
clearTimeout(timer)
timer = null
},wait)
}
}
}
这个版本很好理解,我们利用闭包,在函数内部返回一个匿名函数,让timer变量在内存中不释放,这样下次触发时,内存检测到timer已经赋值了,所以不会再让fn执行。
我们测试一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>throttle</title>
</head>
<body>
</body>
</html>
<script>
function throttle(fn,wait){
var timer = null;
return function(){
var context = this;
var args = arguments;
console.log('timer>>',timer)
if(!timer){
timer = setTimeout(function(){
fn.apply(context,args);
clearTimeout(timer)
timer = null
},wait)
}
}
}
function resizeFunc(){
console.log('resize')
}
window.addEventListener('resize',throttle(resizeFunc,500))
</script>
测试结果:

大家应该发现问题了,就是timer的值并未清零。每次setTimeout,系统都会分配一个计数器id,这个id从1开始自增。虽然我们调用了clearTimeout来清除timer,但实际上计数器还是自增,这是否说明定时器仍在内存中未被释放呢?难道是因为闭包的原因?
如果是这样,我们不妨摒弃闭包的方式,换一种思维来实现。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>throttle</title>
</head>
<body>
</body>
</html>
<script>
var throttleID = null;
function throttleLow(){
var isClear = arguments[0],fn;
if(typeof isClear === 'boolean'){
fn = arguments[1]
console.log(throttleID)
//清除定时器
clearTimeout(throttleID)
} else{
fn = isClear
var wait = arguments[1]
arguments.callee(true,fn)
var that = this
throttleID = setTimeout(function(){
fn.call(that)
},wait)
}
}
function resizeFunc(){
console.log('resize')
}
window.addEventListener('resize',function(){
throttleLow(resizeFunc,1000)
})
</script>
throttleID是全局变量,只要确保释放掉它就可以了。但实际经过测试发现,throttleID仍旧是自增的,为啥呢?
我们知道,定时器是异步的,是定时器触发线程来执行的,每新增一个定时任务,序号就会加1。而一旦定时器线程运行完成后,就会将回调推入宏任务队列,等JS引擎执行栈空闲了,就从宏任务队列调入回调函数并执行。
因此,销毁定时器的动作是在JS引擎线程中完成的,而新定时任务的序号分配是定时器线程控制,这两个线程之间彼此独立互不干扰。
但这样实现还有个问题:
throttleID这个变量必须是全局的!
如果throttleID是局部变量,那每次进来都是null,那clearTimeout就没用了。真是让人掉头发啊,如果能保存住throttleID又不用全局变量就好了。有办法吗?
当然有!就是利用fn。因为fn是对象,当它作为引用型变量传入形参后,可以在fn下(函数本身也是对象)新增一个键保存计数器id,下次再进来时,同一个fn下的计数器id仍在。
function throttleSuper(){
var isClear = arguments[0],fn;
if(typeof isClear === 'boolean'){
fn = arguments[1]
//清除定时器
fn._throttleID && clearTimeout(fn._throttleID)
} else{
fn = isClear
var wait = arguments[1]
arguments.callee(true,fn)
var that = this
fn._throttleID = setTimeout(function(){
fn.call(that)
},wait)
}
}
两种实现办法大家择其一便可。
网友评论