前端实现的进度条方案

1,909 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情

vue项目中有个需求,需要实现进度条功能,目前所用的组件是element-ui,因此直接用组件自带的<el-progress>实现,那前端部分就是间隔0.1s更新一次进度条,当进度条达到90%时,开始停止进度条,timer是允许的时间展示,percentage是进度条展示。

实现效果如下图:

image.png

首先我们设置布局如下:

<el-tag type="warning" effect="dark" size="small" color="#67C23A" disable-transitions>{{timer}}s</el-tag>
<el-progress :text-inside="true" :stroke-width="14" :percentage="percentage" status="warning"></el-progress>

然后再在执行接口时,进行定时更新进度条。

定时器方式实现

定时顾名思义,首先想到的就是定时器,代码就如下所示:

clearInterval(this.timer)
this.timer = null
this.timer = setInterval(() => {
  result.timer = Math.round((result.timer + 0.1) * 100) / 100
  let per = result.percentage + Math.ceil(Math.random() * 10)
  if (per >= 90 || (result.timer % 1 !== 0)) {
    return
  }
  result.percentage = per
}, 100)

每100ms进一次定时器,改变计时器展示,result.timer % 1 !== 0实现每1s更新一次进度条,而为了让这个假进度条更加可信,可采取随机数的方式每次增加进度条量。当超过90%时停止进度条更新,直至接口返回数据,直接将进度条更新为100%。

这样功能是实现了,但是有几个问题:

  1. setInterval属于宏任务,在任务队列中,会受到队列中其他任务的影响,如果存在很大的其他任务,setInterval的计时将会非常不准

  2. setInterval的里的每次更新percentage,会重新出发重排和重绘,性能较差,下图为进度条运行时的重绘数量,由此可见,只是小小进度条,重绘却有非常多次

    image.png

requestAnimationFrame方式

由于上文提到的,定时器方式实现进度条的几个问题,我尝试了更好的解决方案,即为requestAnimationFrame方式,代码如下:

let preTime = 0
function timerFun (timestamp) {
  console.log('timestamp', timestamp, timestamp - preTime)
  if (result.percentage === 100) {
    cancelAnimationFrame(timerFun)
    return
  }
  if (timestamp - preTime < 100) {
    requestAnimationFrame(timerFun)
    return
  }
  preTime = timestamp
  console.log(2, result.percentage, Date.now())
  result.timer = Math.round((result.timer + 0.1) * 100) / 100
  let per = result.percentage + Math.ceil(Math.random() * 10)
  requestAnimationFrame(timerFun)
  // 每间隔1s,进度条更新一次:result.timer % 1 !== 0
  if (per >= 90 || (result.timer % 1 !== 0)) {
    return
  }
  result.percentage = per
}
requestAnimationFrame(timerFun)

image.png

此方案的优点为:

  1. 并不在任务队列中,因此不会受其他任务的影响,计时更准确

  2. requestAnimationFrame的原理为页面在下一次重绘之前调用传入的方法,因此不会单独触发新的重绘,同样记录了进度条运行时的重绘数量,如下图,可以看出比定时器方式好了很多,现在有的重绘不再包含进度条运行

    image.png

  3. 还有一点,setInterval方式,就算页面隐藏,处于后台状态,比如切换网站tab标签,定时器依然还会继续执行,而requestAnimationFrame在后台状态不会继续执行

    • 根据这一点,对动画来说,性能会很好,但是对于项目中会有个timer变量的计时,就会出现问题

    • 考虑这一点对代码做优化,每次timer的更新将不再是固定的0.1,而是根据回调函数的入参,时间戳来确定,更新后代码如下

      let preTime = 0
      function timerFun (timestamp) {
        if (result.percentage === 100) {
          cancelAnimationFrame(timerFun)
          return
        }
        if (timestamp - preTime < 100) {
          requestAnimationFrame(timerFun)
          return
        }
        // 由于requestAnimationFrame在后台不会运行,因此需要计算interval
        let interVal = !preTime ? 0.1 : +((timestamp - preTime) / 1000).toFixed(1)
        preTime = timestamp
        result.timer = Math.round((result.timer + interVal) * 100) / 100
        let per = result.percentage + Math.ceil(Math.random() * 10)
        requestAnimationFrame(timerFun)
        // 每间隔1s,进度条更新一次:result.timer % 1 !== 0
        if (per >= 90 || (result.timer % 1 !== 0)) {
          return
        }
        result.percentage = per
      }