先了解运动框架的基本概念,如需跳过概念部分直奔主题,请在目录中选取需要跳转的内容
一、requestAnimationFrame运动框架的基本概念
- JS运动框架window.requestAnimationFrame(回调函数),简单来说就是让浏览器在下次重绘之前,执行指定的回调函数。
1、requestAnimationFrame运动框架特性
- requestAnimationFrame跟随浏览器DOM更新频率,实现细腻的特效,例如无限循环的随机满屏花瓣飘落效果,无需将动画过程录制成媒体文件——意味着流畅的视觉体验。
- css3成熟以后,style中的transition和annimation被认为是更方便的Web动画实现方式,别以为requestAnimationFrame就此消失,因为无论多么花哨的响应式动画,都绕不开浏览器DOM更新频率的限制,超过浏览器响应频率的DOM重绘,会出现掉帧,并且对浏览器性能造成浪费。
- 虽然requestAnimationFrame运动框架已经不流行,依然可以发挥作用,活用运动框架的特性,可以处理一些棘手的问题,并且在特定的场景中节省浏览器性能。
2、使用requestAnimationFrame运动框架可处理掉帧问题
- DOM重绘掉帧的现象,容易出现在频繁使用定时器的场景下,当我们用定时器高频率地回调一个函数,而视图中某些元素要跟随这个函数刷新,当定时器间隔越来越短,函数执行了,但元素没有更新。
- 很明显掉帧会影响页面的正常显示,涉及到重要信息的时候,甚至会造成不必要的误解。web显示出现掉帧的情况下,如果不想改变代码逻辑,又要高频刷新视图元素,可以选择用运动框架取代单纯的高频定时。
- requestAnimationFrame自身也消耗浏览器性能,而且相对于transition和annimation,运动框架的代码写起来更麻烦,决定使用运动框架之前需要权衡利弊。
3、不要用requestAnimationFrame运动框架处理的场景
- 不要用运动框架解决前后端交互产生的时延问题
- 不要用运动框架解决输入校验中遇到的数据-视图不一致问题
- 以上两个场景,本质上并不是DOM更新频率问题,而是要做好防抖或节流,并且以上两个交互过程,按通常的业务逻辑,没有必要按照小于浏览器重绘周期的间隔执行。
- 使用了定时器的场景,不要将运动框架作为优先的解决办法,可用数组记录定时器,并及时清除,确保网页中没有非必要的定时器。
- 当运动框架与定时器处于同一线程的时候,会发生运动框架优先运行而定时器的运行受到严重干扰的现象,H5是支持多线程的,感兴趣可以查一下Web Workers的相关内容。
requestAnimationFrame的使用需要一定的数学常识,如果只是用来当定时工具,也不需要太复杂的计算过程,有HTML、JavaScript基础是可以理解的。
二、requestAnimationFrame运动框架-精简代码
运动框架早期的实用代码有三个版本:速度版、时间版和多样式版,由于这三个版本的代码已经用得不多,而且有点复杂不利于初步理解,没有必要贴完整代码,只附上关键部分,使用时需自行补充中间要运行的过程。
1、编辑定时器
window.requestAnimationFrame = window.requestAnimationFrame || function (fn) {
return setTimeout(fn, 1000 / 60);
}
window.cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
2、调用运动函数(调用以后,每隔一个浏览器重绘周期就调用一次回调函数fn)
requestAnimationFrame(fn)
3、回调函数,中间写每隔一个浏览器重绘周期需要执行的代码
function fn(){
//要执行的代码
console.log(0);
//再次让运动框架执行这个函数,以形成循环
requestAnimationFrame(fn)
}
三、运动框架应用于web页面的抗阻塞均匀计数器
终于到了上干货的时刻,铺垫有点亢长了呢,通过目录直接跳这儿也行哦( ̄︶ ̄)
1、为什么要用requestAnimationFrame做计数器?
前端计时器是经常用到的知识点,基于JavaScript单线程的特性,setInterval计数器在阻塞干扰下间隔会变得不均匀,虽然已经有现成的解决方法,通常是将阻塞延迟的时间在下一个计时周期中调整过来,但并不能治本,所以才推荐用requestAnimationFrame运动框架做计数器,达到在阻塞干扰的情况下依然均匀运作的效果。
先来张页面截图:
再来点控制台数据:
2、requestAnimationFrame运动框架与setInterval做计数器的控制台数据:
这是用requestAnimationFrame运动框架作计时器输出的控制台信息,可以看到在百分秒精度下间隔非常准确,下图无阻塞的情况:
下图是requestAnimationFrame运动框架做百分秒精度下的计数器,同时存在阻塞干扰的情况,看控制台信息,误差也是相当小的:
不过运动框架做计数器并不是万能的,由于运动框架随视图刷新,所以不能很好地支持千分秒精度的计时,如果需要用千分秒精度的计数器,还是用setInterval更合适,下图是setInterval为计数器在无附加阻塞情况下的控制台数据:
setInterval做计数器有个明显的缺点,受阻塞影响较大,甚至会出现明显的间隔波动,在阻塞干扰下无法确保计数精度,控制台数据如下图所示:
综上所述,requestAnimationFrame运动框架作为计数器的最高计数精度不如setInterval,但是运动框架对阻塞的抗干扰效果较好,setInterval在阻塞干扰下不稳定,并且运动框架能够在百分秒精度下,无阻塞干扰的前提下,计数间隔相当精准,这一点是setInterval无法做到的。
3、web版计数器(对照版)实现代码
基于以上因素制作了这版web版计数器,可以支持两种模式,一种是千分秒精度的setInterval计数器,一种是百分秒精度的requestAnimationFrame运动框架计数器(推荐),两种模式可以切换使用。并且还附加了阻塞干扰按钮,可以在运行中随时添加/移除阻塞干扰,来观察运行效果。
这个计数器需要在Vue3项目中使用,请自行安装脚手架并创建项目。ui用的element-plus,cmd进入项目所在目录执行安装命令:
npm install element-plus --save
如果你用的不是window系统,可以参考Element Plus官网文档:安装 | Element Plus
安装element-plus以后,在main.js文件里添加以下代码:
// 引用element-plus
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
计数器页面的文件名是vq4.vue,如需改页面名称,要同时修改路由和export default里的name的值,使其一致。如果对文章内容有疑问请在文章下方留言,记得描述问题的前因后果,附上截图或代码。
以下是完整的页面代码:
<template>
<div class="title" :style="{margin:'50px 12px 12px'}">计数器web版</div>
<div class="title2" :style="{margin:'0 12px 3px'}">F12打开浏览器控制台观察运行数据</div>
<div class="title2" :style="{margin:'0 12px 12px'}">目前只能在页面激活状态正常运作,切换至其他页面或其他程序会让计时器延后</div>
<el-radio-group :style="{margin:'12px'}" v-model="radios" :disabled="rDisabled">
<el-radio :label="1">timer-可精确至千分位,容易受阻塞影响</el-radio>
<el-radio :label="2">requestAFrame-可精确至百分位,抗阻塞效果更佳</el-radio>
</el-radio-group>
<div class="btDiv" :style="{margin:'24px'}">
<el-button type="primary" @click="btClick()" >{{ btText.texts[btText.ind] }}</el-button>
<el-button type="primary" @click="blocking()" >{{ block.texts[block.ind] }}</el-button>
</div>
<div class="title2" :style="{margin:'12px'}">计数器开启状态下再次点击计数按钮可停止计数</div>
</template>
<script>
import { Plus } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { ref } from 'vue';
export default {
name: 'vq4',
components:{Plus},
data(){
return{
worker:null,
radios:ref(2),
blockTimerArr:[],
block:{
texts:["开启阻塞","关闭阻塞"],
ind:0,
},
rDisabled:false,
btText:{
texts:["开始计时","-"],
ind:0,
},
requestAF:()=>{
window.requestAnimationFrame = window.requestAnimationFrame || function (fn) {
return setTimeout(fn, 1000 / 60);
}
window.cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
},
}
},
mounted(){
this.showTimeM();
},
methods:{
blocking(){
let block=this.block;
if(block.ind==0){
block.ind=1;
console.log("阻塞已开启");
ElMessage.success('阻塞已开启!');
}else{
block.ind=0;
console.log("阻塞已关闭");
ElMessage.success('阻塞已关闭!');
}
this.blockClock();
},
blockClock(){
//阻塞
if(this.block.ind==1){
if(this.blockTimerArr.length==0){
let timer=setInterval(()=>{
console.log("阻塞");
let i=0;
while(i<100000000){i++;}
}, 0);
this.blockTimerArr.push(timer);
}
}else{
console.log("阻塞",this.blockTimerArr.length);
//清空定时器
this.blockTimerArr.forEach(item=>{
clearInterval(item);
});
this.blockTimerArr.splice(0,this.blockTimerArr.length);
}
},
btClick(){
let btText=this.btText;
if(btText.ind==0){//初始状态
btText.ind=1;
this.rDisabled=true;
if(this.radios==1){
this.countNum('timer');
}else{
this.countNum('AFrame');
}
console.log("开始计数");
ElMessage.success('开始计数!');
}else{
this.rDisabled=false;
btText.ind=0;
console.log("停止计数");
ElMessage.success('停止计数!');
}
},
countNum(mod){
//阀门
if(this.btText.ind==0){return false;}
//文字居中
this.showTimeM();
//参数
//数据记录
let startTime = new Date(); //起始时间
let tmpTime = startTime; //暂存时间(数据记录)
let lastshowTime=0; //上次累计时间(数据记录)
let timerArr=[]; //timer数组
//渲染
let canShowT=false; //渲染起始阀门
let showStep = 0.001; //渲染间隔(秒),大于等于inCycle
if(mod!="timer"){
showStep = 0.01;
}
let numLength = showStep.toString().replace(".","").length - 1; //渲染精度
//设置
let step= 0.1; //精确位数(秒)
let turbulence = 0.01; //偏移幅度,精细计时(百分比)
if(mod!="timer"){
step= 0.1;
turbulence = 0.5;
}
let inCycle=(step * turbulence).toFixed(2); //数据记录最小间隔,大于等于0.001秒
let reWTime=()=>{
let nowTime=new Date();
let showTime = (nowTime - startTime)/1000; //从开始的累计时间
let timePass = (nowTime - tmpTime)/1000; //本次记录到上次记录经历了多久
let offset = timePass-inCycle; //数据记录中的偏移量
if(( timePass + offset ) >= inCycle){ //数据记录
if(canShowT==false){ //稳定之前不输出数据
if((timePass-offset)==inCycle){
canShowT=true;
}
}else{ //首次稳定以后开始输出数据
if((showTime-lastshowTime)>=showStep){
let putText=showTime.toFixed(numLength);
let IntervalError= timePass - inCycle;
//控制台数据输出
console.log(mod,
"输出:"+ putText,
"误差:" + IntervalError);
//视图输出
let lastshowT = this.btText.texts[1]; //记录上次时间
this.btText.texts[1] = putText; //显示数据
//文字居中
if(putText.toString().length!=lastshowT.toString().length){
this.showTimeM();
}
lastshowTime=showTime; //记录上次满足输出条件时的累计时间
}
}
tmpTime=nowTime; //更新上次循环记录的时间
}
};
//调用计时器
if(mod=="timer"){
if(timerArr.length==0){
let timer=setInterval(()=>{
if(this.btText.ind==1){
reWTime();
}else{
//清空定时器
timerArr.forEach(item=>{
clearInterval(item);
});
timerArr.splice(0,timerArr.length);
this.showTimeM();
this.btText.texts[1]="-";
}
}, 0);
timerArr.push(timer);
}
}else{
let requestAFrame=()=>{
if(this.btText.ind==1){
reWTime();
requestAnimationFrame(requestAFrame);
}else{
this.showTimeM();
this.btText.texts[1]="-";
}
}
requestAnimationFrame(requestAFrame);
}
},
showTimeM(){
requestAnimationFrame(()=>{
let elBt=document.querySelector(".el-button");
let elBtSpan=document.querySelector(".el-button>span");
elBtSpan.style.left=(elBt.offsetWidth - elBtSpan.offsetWidth ) * 0.5 + "px";
});
},
stopWorker(){
this.worker.terminate();
this.worker = undefined;
},
},
destroyed() {
this.worker.terminate();
},
}
</script>
<style lang="scss" scoped >
.title,.title2{
color:#409EFF;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', Arial, sans-serif;
}
.title{
font-size: 17px;
font-weight: bold;
}
.title2{
font-size: 14px;
font-weight: normal;
}
.btDiv{
display: flex;
flex-flow: row nowrap;
justify-content: center;
}
.btDiv /deep/ .el-button:nth-of-type(1){
width: 120px;
display: flex;
padding: 0;
flex-flow: row nowrap;
justify-content: start;
&>span{
position: relative;
text-align: left;
}
}
.hide{
visibility: hidden;
}
</style>
版权归原作者 观察蚂蚁的人 所有, 如有侵权,请联系我们删除。