如何修正 setTimeout/requestAnimationFrame 准确度

How to fix setTimeout/requestAnimationFrame accuracy

有关问题的演示,请参阅此处:

https://gyazo.com/06e423d07afecfa2fbdb06a6da77f66a

我在取消暂停通知时出现跳跃行为。这也受鼠标在通知上停留多长时间以及进度接近尾声的影响。

我试了很多东西,我不确定问题是否真的出在 setTimeout 上。

好像从this.timerFinishesAt的计算到requestAnimationFrame的第一次迭代,因为等待了cpu的时间进度跳转了?不过话又说回来了,为什么会受到悬停时间和进度的影响呢

如何减轻跳跃行为?

我 read/tried 在查看其他 Whosebug 问题时从以下资源实施修复:

https://gist.github.com/tanepiper/4215634

What is the reason JavaScript setTimeout is so inaccurate?

https://www.sitepoint.com/creating-accurate-timers-in-javascript/

https://codepen.io/sayes2x/embed/GYdLqL?default-tabs=js%2Cresult&height=600&host=https%3A%2F%2Fcodepen.io&referrer=https%3A%2F%2Fmedium.com%2Fmedia%2Fb90251c55fe9ac7717ae8451081f6366%3FpostId%3D255f3f5cf50c&slug-hash=GYdLqL

https://github.com/Falc/Tock.js/tree/master

https://github.com/philipyoungg/timer

https://github.com/Aaronik/accurate_timer

https://github.com/husa/timer.js

timerStart(){
   // new future date = future date + elapsed time since pausing
   this.timerFinishesAt = new Date( this.timerFinishesAt.getTime() + (Date.now() - this.timerPausedAt.getTime()) );
   // set new timeout
   this.timerId = window.setTimeout(this.toggleVisibility, (this.timerFinishesAt.getTime() - Date.now()));
   // animation start
   this.progressId = requestAnimationFrame(this.progressBar);
},
timerPause(){
   // stop notification from closing
   window.clearTimeout(this.timerId);
   // set to null so animation won't stay in a loop
   this.timerId = null;
   // stop loader animation from progressing
   cancelAnimationFrame(this.progressId);
   this.progressId = null;

   this.timerPausedAt = new Date();
},
progressBar(){
   if (this.progress < 100) {
     let elapsed = Date.now() - this.timerStarted.getTime();
     let wholeTime = this.timerFinishesAt.getTime() - this.timerStarted.getTime();
     this.progress = Math.ceil((elapsed / wholeTime) * 100);

     if (this.timerId) {
       this.progressId = requestAnimationFrame(this.progressBar);
     }

   } else {
     this.progressId = cancelAnimationFrame(this.progressId);
   }
}

我认为简单地使用 SetInterval 就足够了:

const progressBar = {
  MsgBox : document.querySelector('#Message'),
  Info   : document.querySelector('#Message h1'),
  barr   : document.querySelector('#Message progress'),
  interV : 0,
  DTime  : 0,
  D_Max  : 0,

  Init() {
    this.MsgBox.onmouseover=_=> {   // pause
      clearInterval( this.interV )
    }
    this.MsgBox.onmouseout=_=>{     // restart
      this._run()
    }
  },
  Start(t,txt)
  {
    this.DTime = this.D_Max = t * 1000
    this.barr.value = 0
    this.barr.max = this.D_Max
    this.Info.textContent = txt
    this._run()
  },
  _run()
  {
    let D_End = new Date(Date.now() + this.DTime )

    this.interV = setInterval(_=>{
      this.DTime = D_End - (new Date(Date.now()))

      if (this.DTime > 0) { this.barr.value = this.D_Max - this.DTime }
      else                { clearInterval( this.interV ); console.clear(); console.log( "finish" ) }      
    }, 100);
  }
}


progressBar.Init()

progressBar.Start(10, 'Hello!') // 10 seconds
#Message {
  box-sizing: border-box;
  display: block;
  float: right;
  width: 200px;
  height: 80px;
  background-color: darkslategrey;
  padding: 0 1em;
  color:#e4a8b4;
  cursor: pointer;
  margin-right:1.5em;
}
#Message h1 { margin: .3em 0 0 0}
#Message progress { height: .1em; margin: 0; width:100%; background-color:black; }
#Message progress::-moz-progress-bar,
#Message progress::-webkit-progress-value { background-color:greenyellow; }
<div id="Message">
  <progress value="50" max="100" ></progress>
  <h1> </h1>
</div>

当您计算计时器的当前进度时,您没有考虑暂停时间。因此跳跃:这部分代码只知道开始时间和当前时间,它不会受到暂停的影响。

为了规避它,您可以在 startTimer 函数中累积所有这些暂停时间

class Timer {
  constructor() {
    this.progress = 0;
    this.totalPauseDuration = 0;
    const d = this.timerFinishesAt = new Date(Date.now() + 10000);
    this.timerStarted = new Date();
    this.timerPausedAt = new Date();
  }
  timerStart() {
    const pauseDuration = (Date.now() - this.timerPausedAt.getTime())

    this.totalPauseDuration += pauseDuration;

    // new future date = future date + elapsed time since pausing
    this.timerFinishesAt = new Date(this.timerFinishesAt.getTime() + pauseDuration);
    // set new timeout
    this.timerId = window.setTimeout(this.toggleVisibility.bind(this), (this.timerFinishesAt.getTime() - Date.now()));
    // animation start
    this.progressId = requestAnimationFrame(this.progressBar.bind(this));
  }
  timerPause() {
    // stop notification from closing
    window.clearTimeout(this.timerId);
    // set to null so animation won't stay in a loop
    this.timerId = null;
    // stop loader animation from progressing
    cancelAnimationFrame(this.progressId);
    this.progressId = null;

    this.timerPausedAt = new Date();
  }
  progressBar() {
    if (this.progress < 100) {
      let elapsed = (Date.now() - this.timerStarted.getTime()) - this.totalPauseDuration;
      let wholeTime = this.timerFinishesAt.getTime() - this.timerStarted.getTime();
      this.progress = Math.ceil((elapsed / wholeTime) * 100);
      
      log.textContent = this.progress;
      
      if (this.timerId) {
        this.progressId = requestAnimationFrame(this.progressBar.bind(this));
      }

    } else {
      this.progressId = cancelAnimationFrame(this.progressId);
    }
  }
  toggleVisibility() {
    console.log("done");
  }
};

const timer = new Timer();

btn.onclick = e => {
  if (timer.timerId) timer.timerPause();
  else timer.timerStart();
};
<pre id="log"></pre>

<button id="btn">toggle</button>

或者更新startTime,这个好像比较靠谱:

class Timer {
  constructor() {
    this.progress = 0;
    const d = this.timerFinishesAt = new Date(Date.now() + 10000);
    this.timerStarted = new Date();
    this.timerPausedAt = new Date();
  }
  timerStart() {
    const pauseDuration = (Date.now() - this.timerPausedAt.getTime())

    // update timerStarted
    this.timerStarted = new Date(this.timerStarted.getTime() + pauseDuration);

    // new future date = future date + elapsed time since pausing
    this.timerFinishesAt = new Date(this.timerFinishesAt.getTime() + pauseDuration);
    // set new timeout
    this.timerId = window.setTimeout(this.toggleVisibility.bind(this), (this.timerFinishesAt.getTime() - Date.now()));
    // animation start
    this.progressId = requestAnimationFrame(this.progressBar.bind(this));
  }
  timerPause() {
    // stop notification from closing
    window.clearTimeout(this.timerId);
    // set to null so animation won't stay in a loop
    this.timerId = null;
    // stop loader animation from progressing
    cancelAnimationFrame(this.progressId);
    this.progressId = null;

    this.timerPausedAt = new Date();
  }
  progressBar() {
    if (this.progress < 100) {
      let elapsed = Date.now() - this.timerStarted.getTime();
      let wholeTime = this.timerFinishesAt.getTime() - this.timerStarted.getTime();
      this.progress = Math.ceil((elapsed / wholeTime) * 100);
      
      log.textContent = this.progress;
      
      if (this.timerId) {
        this.progressId = requestAnimationFrame(this.progressBar.bind(this));
      }

    } else {
      this.progressId = cancelAnimationFrame(this.progressId);
    }
  }
  toggleVisibility() {
    console.log("done");
  }
};

const timer = new Timer();

btn.onclick = e => {
  if (timer.timerId) timer.timerPause();
  else timer.timerStart();
};
<pre id="log"></pre>

<button id="btn">toggle</button>

至于最后的差距,没有看到这段代码如何与您的 UI 链接,很难说会发生什么。