相信很多人最初的时候都喜欢使用setInterval来实现一些动画效果,或者定时获取数据刷新效果,但是当了解了setInterval的缺陷后都更倾向于setTimeout来模拟实现setInterval的效果。当处理动画的时候则使用同系统帧率同步的requestAnimtionFrame来实现流畅的动画效果。
1.setInterval
首先来谈谈setInterval的实现机制吧
function refreshMyOperation(){
setInterval(()={
//do something
},30)
}
如上定义了一个定时器,每隔30ms就重复执行一下定时器中的内容。这在正常情况下(执行栈空闲)的情况下是很稳定的,没有任何问题,但是如果当执行栈中正在执行任务,或者主线程任务还在等待执行,所以每隔30ms,定时器不管主线程有没有任务,或者说上一个任务有没有执行完,都会往在异步的任务队列中添加一个新的任务(函数执行一次)。
比如我现在主线程正在执行一个耗时的任务,setInterval新任务添加到异步任务队列中,定时器在第一个时刻往任务队列中添加了一个任务A,然后又过了30ms,主线程依然非空闲,但是定时器的30ms到了,他会又往任务队列中添加一个任务A,我们考虑到特殊情况时,任务队列里面堆积了很多的任务A。当主线程一旦空闲开始执行异步任务,那么这一系列的任务A就会迅速依次执行,本来每个A任务之间应该是有一个30ms的间隔的,但是由于这些A直接堆积不再由定时器控制,所以会一次执行完,中间不再符合预期的30ms间隔。
大家可以演示的效果时,写一个定时器动画,然后把浏览器隐藏,主线程就会挂起,但是定时器线程依然会继续执行,当你过一段时间再次打开时,就会看到动画一次性快速执行到当前时刻的位置。(这个挂起只是模拟主线程阻塞)。
2.setTimeout
为了解决setInterval这个问题,我们可以用setTimeout来模拟setInterval来实现相同效果
function refreshMyoperation(val){
console.log(val);//do something
setTimeout(arguments.callee,800,val+1);
}
refrenshMyoperation(666);//666,667,668,669.....
使用setTimeout中如果主线程非空闲,首次调用refreshMyoperation方法定时器只会在30ms后把setTimeout中的任务添加到任务队列中,然后定时器线程就不再需要继续计时添加了。等待主线程空闲后会执行刚刚添加的任务,当这个任务执行结束时,会再次条用refreshMyoperation。这样计时do something很耗时或者被阻塞,那么新的定时器中任务总是在当前定时器任务执行完成后才会开启定时器线程执行新的任务,从而不会出现像setInterval中不断往任务队列中添加任务的情况。
至于requestAnimationFrame的使用场景为动画的执行,例如我们使用canvas绘制动画。
那为什么不适用setTimeout呢?这里涉及到帧率的问题,玩过LOL的伙伴们都可能看到电脑右上角有个FPS数据,并且还会变动,这个就是帧率,我们人类的眼睛具有视觉停留效应,即前一幅画面留在大脑里面的印象还没有消失,紧接着后一幅画面就跟上来了。
当我们把一系列图片按顺序快速切换的时候仿佛就像时看电影一样,实际上这就是动画的原理:动画的本质就是要让人眼看到图像被刷新而引起变化的视觉效果,这个变化要以连贯的平滑的方式进行过渡,我们肉眼看到动画有时候卡顿就是由于帧率不够,人眼流畅的画面帧率是60,所以时间上是100/60≈16.7ms每帧。
有人说我们可以把setTimeout设置为16ms或者10ms就可以了。这完全是可以的。并且大部分情况来说是有用的。但是在低端机上会出现卡顿抖动的现像。产生这种现象有两个原因:
- setTimeout的执行时间是不确定的,在js中setTimeout任务被放到异步任务队列中,只有当主线程空闲的时候才会去检查该队列里面是否有任务等待执行,因此setTimeout的实际执行时间一般比其设定的时间要晚一些。
- 刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同的设备屏幕刷新频率可能会不同,而setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相等
以上两种情况都会导致setTimeout的执行步调和屏幕刷新步调不一致,从而引起丢帧现像,那么为什么步调不一致就会引起丢帧现像呢?
首先要明白,setTimeout的执行只是在内存中对图像属性进行改变,这个改变必须要等到屏幕下次刷新的时候才会被更新到屏幕上。如果两者步调不一致,就可能导致某一帧的操作被跳过刷新到屏幕上,而直接去更新下一帧的图像。假如屏幕每隔16.7ms刷新一次,而setTimeout每隔10ms设置将图像向左移动1px,就会出现如下绘制过程:
- 第0ms,屏幕未刷新,等待中,setTimeou也未执行,等待中
- 第10ms,屏幕未刷新,等待中,setTimeout开始执行,并将图像像左移动1px,left=1px
- 第16.7ms,屏幕刷新,更新第10ms内存中图像位置的改变向左移动1px。setTimeout未执行,等待中
- 第20ms,屏幕未刷新,等待中,setTimeout开始执行,图像再次向左移动1px,left=2px
- 第30ms,屏幕未刷新,等待中,setTimeou继续执行,图像再次向左移动1px,left=3px
- 第33.4ms,屏幕刷新,直接更新内存中最新的值即30ms的值,left=3px。setTimeout未执行,等待中
- .....
从上面的绘制过程我们可以看出,屏幕并没有更新在20ms,left=2px那一帧画面,图像直接从1px的位置跳到了3px的位置,这就是丢帧现像,这种现像就会引起动画卡顿或抖动。
3.requestAnimationFrame
与setTimeout相比,requestAnimationFrame最大的优势就是由系统来决定回调函数的执行时机。具体来说,如果屏幕的刷新频率是60HZ,那么回调函数就每1000/60≈16.7ms被执行一次,如果刷新频率为75HZ,那么这个时间间隔就变成了1000/75≈13.3ms。换句话说,requestAnimationFrame的步伐跟着系统的刷新步伐走。他能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现像,也不会导致动画出现卡顿的现像。
//the purpose of this wrap function existing is for the paras to transmit
function renderAnimation(paras){
//handle paras
animation();//follow wrap function implement
function animation(){
//do some animation
if(true){//continue animation condition
window.requestAnimationFrame(arguments.callee);
}
}
}
renderAnimation(paras);//first time call method
除此之外,requestAniamtionFrame还有以下两个优势:
-
CPU节能:使用setTimeout实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源。而requestAnimationFrame则完全不同,当页面处于未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的requestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。
-
函数节流:在高频率事件(resize,scroll等)中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的,因为显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。
好了上面已经讲了关于setInterval,setTimeout,requestAnimationFrame的相关知识和区别,现在来使用setTimeout实现一下扫码枪效果:
逻辑:
1.输入框是获取焦点状态,等待内容的输入
2.按下扫码枪,扫码枪获取二维码中的数据并把数据填充到输入框中
3.获取到输入框中的数据并处理,提交给后台就行校验
4.校验通过,则显示之后的业务逻辑,关闭扫码枪输入框
5.校验不通过,弹出提示信息,显示输入框并再次重复1步骤
这里有需要注意的地方:
1.就是扫码枪填充数据是有延时的,根据二维码的数据长度,填充到数据框所需要的时间也是不一样的。
2.扫码枪空闲的时候,定时器需要去判断输入框中是否有数据,如果有数据则提交则等待扫码枪延时输入完成再提交数据,如果没有数据则继续等待下一次的校验输入框的状态。
3.实现上面两点,每次定时器刷新后,判断有数据则再定义一个定时器(500ms或800ms)后提交数据,以确保二维码数据全部录入完成并提交。
直接上代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>setTimeout</title>
</head>
<body>
<input type="text" id="qrCode" autofocus />
</body>
<script>
function checkQrcode(){
const inputElemVal = document.getElementById('qrCode').value;
//当有值的时候可能是扫码枪正在输入阶段,所以延迟600ms等待数据读取完成再提交数据
if(inputElemVal.trim()){
setTimeout(()=>{
//submitdata,checkdata
submitData(inputElemVal);
},600)
}else{//如果没有检测到数据输入则再次等待一段时间再次进行数据校验
setTimeout(arguments.callee,400);
}
}
function submitData(val){
$.ajax({
url:'...',
data:{
value:val
},
success:function(data){
if(...){//校验通过
//通过后弹出成功,显示之后的信息
}else{
//信息校验不通过,需要提示无效信息,然后再显示输入框并获取焦点,
//最后重复扫码枪校验信息方法
//alert('无效信息,请重新输入');
document.getElementById('qrCode').focus();
checkQrcode();
}
}
});
}
checkQrcode();//方法调用,在输入框弹出来的时候调用
</script>
</html>
好了,一个扫码枪效果就实现了,总结,能用setTimeout就别用setInterval,处理动画或高频事件什么的可以使用requestAnimationFrame,当然,setTimeout也是可以实现函数节流的,凭自己的理解使用。
文章借鉴:www.cnblogs.com/onepixel/p/7078617.html