setTimeout 与 requestAnimationFrame

发布于 2025-01-15 11:42:38 字数 575 浏览 1 评论 0 原文

我制作了一个“setTimeout”与“requestAnimationFrame<”的示例 /code>' 来找出它们有多么不同。

正如你所看到的,橙色的盒子最先到达目的地。绿色盒子跳跃了几次并且速度较慢。

我明白为什么绿框有时会跳动。因为任务(调用 move 函数)在重绘之前有时不会插入到 MacroTaskQueue 中(这称为卡顿或跳帧)。

这就是为什么在为元素添加动画时我更喜欢 requestAnimationFrame 而不是 setTimeout。因为 requestAnimationFrame(move)move() 保证在重绘之前被调用。

现在,我想知道的是为什么绿色框比橙色框慢

也许这意味着move()不是每1000/60毫秒调用一次?

I made an example of 'setTimeout vs requestAnimationFrame' to find out how different they are.

As you can see, the orange box arrives to the destination first. The green box jump some times and slower.

I understand why the green box jump some times. Because the task(calling move function) would not be inserted in macroTaskQueue before repaint some times(this is called jank or frame skip).

This is why I prefer requestAnimationFrame than setTimeout when animate element. Because the move() of requestAnimationFrame(move) is guaranteed to be called right before repaint.

Now, what I'm wondering is that why the green box is slower than orange box

Maybe does it mean that the move() is not called at each 1000/60 ms?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

离不开的别离 2025-01-22 11:42:39

setTimeout 总是迟到。

它的工作方式是

  • 注册执行任务的时间戳。
  • 在每个事件循环的迭代中,检查 now 是否在该时间戳之后。
  • 执行任务。

通过这种设计,setTimeout() 被迫花费至少延迟定义的时间量。它可以(并且经常)更多,例如,如果事件循环正忙于执行其他操作(例如处理用户手势、调用垃圾收集器等)。

现在,由于您仅在调用前一个回调时请求新的超时,因此您的 setTimeout() 循环会受到时间漂移的影响。每次迭代都会累积这种漂移,并且永远无法从中恢复,从而摆脱挂钟时间。

另一方面,requestAnimationFrame (rAF) 则不会受到这种漂移的影响。事实上,显示器的垂直同步信号告诉事件循环何时必须进入“更新渲染”步骤。该信号与 CPU 活动无关,并将作为稳定时钟工作。如果在一帧中 rAF 回调延迟了几毫秒,则下一帧之​​间的时间将会减少,但标志将定期设置,不会出现漂移。

您可以通过提前安排所有计时器来验证这一点,您的 setTimeout 框将不再受到这种漂移的影响:

const startBtn = document.querySelector('#a');
const jankBtn = document.querySelector('#b');
const settimeoutBox = document.querySelector('.settimeout-box');
const requestAnimationFrameBox = document.querySelector('.request-animation-frame-box');
settimeoutBox._left = requestAnimationFrameBox._left = 0;
let i = 0;

startBtn.addEventListener('click', () => {
  startBtn.classList.add('loading');
  startBtn.classList.add('disabled');
  scheduleAllTimeouts(settimeoutBox);
  moveWithRequestAnimationFrame(requestAnimationFrameBox);
});

function reset() {
  setTimeout(() => {
    startBtn.classList.remove('loading');
    startBtn.classList.remove('disabled');
    i = 0;
    settimeoutBox.style.left = '0px';
    requestAnimationFrameBox.style.left = '0px';
    settimeoutBox._left = requestAnimationFrameBox._left = 0;
  }, 300);
}

function move(el) {
  el._left += 2;
  el.style.left = el._left + 'px';
  if (el._left > 1000) {
    return false;
  }
  return true;
}

function scheduleAllTimeouts(el) {
  for (let i = 0; i < 500; i++) {
    setTimeout(() => move(el), i * 1000 / 60);
  }
}

function moveWithRequestAnimationFrame(el) {
  if (move(el)) {
    requestAnimationFrame(() => {
      moveWithRequestAnimationFrame(el);
    });
  } else reset();
}
.grid {
  margin: 30px !important;
  padding: 30px;
}

.box {
  width: 200px;
  height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  color: white;
  font-size: 18px;
}

.settimeout-box {
  background-color: green;
}

.request-animation-frame-box {
  background-color: orange;
}
<div class="ui grid container">
  <div class="row">
    <button class="ui button huge blue" id="a">Start!</button>
  </div>
  <div class="row">
    <div class="box settimeout-box">
      <span>setTimeout</span>
    </div>
  </div>
  <div class="row">
    <div class="box request-animation-frame-box">
      <span>requestAnimationFrame</span>
    </div>
  </div>
</div>

请注意,Firefox 和 Chrome 实际上会在非动画文档中第一次调用 rAF 后立即触发绘画帧,因此在此演示中 rAF 可能比 setTimeout 早一帧。


requestAnimationFrame 的频率与显示器的刷新率相关。

上面的示例假设您在 60Hz 显示器上运行它。刷新率较高或较低的显示器将以不同的频率进入此“更新渲染”步骤。


另请注意,setTimeout(fn,delay) 中的delay 是一个long,这意味着您传递的值将被底为整数。

最后一点,Chrome 在其 setInteval() 实现中确实会自我纠正这次时间漂移,Firefox 和规范仍然没有,但它是 在(不那么活跃)讨论下

setTimeout is always late.

The way it works is

  • Register a timestamp when to execute our task.
  • At each Event loop's iteration, check if now is after that timestamp.
  • Execute the task.

By this very design, setTimeout() is forced to take at least the amount of time defined by delay. It can (and will often) be more, for instance if the event loop is busy doing something else (like handling user gestures, calling the Garbage Collector, etc.).

Now since you are requesting a new timeout only from when the previous callback got called, your setTimeout() loop suffers from time-drift. Every iteration it will accumulate this drift and will never be able to recover from it, getting away from the wall-clock time.

requestAnimationFrame (rAF) on the other hand doesn't suffer from such a drift. Indeed, the monitor's V-Sync signal is what tells when the event loop must enter the "update the rendering" steps. This signal is not bound to the CPU activity and will work as a stable clock. If at one frame rAF callbacks were late by a few ms, the next frame will just have less time in between, but the flag will be set at regular intervals with no drift.

You can verify this by scheduling all your timers ahead of time, your setTimeout box won't suffer from this drift anymore:

const startBtn = document.querySelector('#a');
const jankBtn = document.querySelector('#b');
const settimeoutBox = document.querySelector('.settimeout-box');
const requestAnimationFrameBox = document.querySelector('.request-animation-frame-box');
settimeoutBox._left = requestAnimationFrameBox._left = 0;
let i = 0;

startBtn.addEventListener('click', () => {
  startBtn.classList.add('loading');
  startBtn.classList.add('disabled');
  scheduleAllTimeouts(settimeoutBox);
  moveWithRequestAnimationFrame(requestAnimationFrameBox);
});

function reset() {
  setTimeout(() => {
    startBtn.classList.remove('loading');
    startBtn.classList.remove('disabled');
    i = 0;
    settimeoutBox.style.left = '0px';
    requestAnimationFrameBox.style.left = '0px';
    settimeoutBox._left = requestAnimationFrameBox._left = 0;
  }, 300);
}

function move(el) {
  el._left += 2;
  el.style.left = el._left + 'px';
  if (el._left > 1000) {
    return false;
  }
  return true;
}

function scheduleAllTimeouts(el) {
  for (let i = 0; i < 500; i++) {
    setTimeout(() => move(el), i * 1000 / 60);
  }
}

function moveWithRequestAnimationFrame(el) {
  if (move(el)) {
    requestAnimationFrame(() => {
      moveWithRequestAnimationFrame(el);
    });
  } else reset();
}
.grid {
  margin: 30px !important;
  padding: 30px;
}

.box {
  width: 200px;
  height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  color: white;
  font-size: 18px;
}

.settimeout-box {
  background-color: green;
}

.request-animation-frame-box {
  background-color: orange;
}
<div class="ui grid container">
  <div class="row">
    <button class="ui button huge blue" id="a">Start!</button>
  </div>
  <div class="row">
    <div class="box settimeout-box">
      <span>setTimeout</span>
    </div>
  </div>
  <div class="row">
    <div class="box request-animation-frame-box">
      <span>requestAnimationFrame</span>
    </div>
  </div>
</div>

Note that Firefox and Chrome actually do trigger the painting frame right after the first call to rAF in a non-animated document, so rAF may be one frame earlier than setTimeout in this demo.


requestAnimationFrame's frequency is relative to the monitor's refresh-rate.

Above example assumes that you run it on a 60Hz monitor. Monitors with higher or lower refresh rate will enter this "update the rendering" step at different frequencies.


Also beware, delay in setTimeout(fn, delay) is a long, this means the value you pass will be floored to integer.

An a last note, Chrome does self correct this time drift in its setInteval() implementation, Firefox and the specs still don't, but it's under (not so active) discussion.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文