0


requestAnimationFrame运动框架基本概念+运动框架应用于web页面的抗阻塞均匀计数器

先了解运动框架的基本概念,如需跳过概念部分直奔主题,请在目录中选取需要跳转的内容

一、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>
标签: 前端 vue.js elementui

本文转载自: https://blog.csdn.net/weixin_44841138/article/details/123753847
版权归原作者 观察蚂蚁的人 所有, 如有侵权,请联系我们删除。

“requestAnimationFrame运动框架基本概念+运动框架应用于web页面的抗阻塞均匀计数器”的评论:

还没有评论