setTimeout 与 requestAnimationFrame

setTimeout vs requestAnimationFrame

我对“setTimeout”与“requestAnimationFrame”进行了 example 比较,以了解它们有何不同。

如您所见,橙色盒子最先到达目的地。绿框跳动了几次,速度变慢了。

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

这就是为什么我更喜欢 requestAnimationFrame 而不是 setTimeout 的原因。因为 requestAnimationFrame(move)move() 保证在重绘之前被调用。

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

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

setTimeout 总是迟到。

它的工作方式是

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

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

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

另一方面,

requestAnimationFrame (rAF) 不会出现这种漂移。实际上,监视器的 V-Sync 信号告诉事件循环何时必须进入“update the rendering”步骤。此信号未绑定到 CPU activity 并将用作稳定时钟。如果在一帧 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 实际上会在 non-animated 文档中第一次调用 rAF 后立即触发绘制帧,因此 rAF 可能比 setTimeout 早一帧 在这个演示中。


requestAnimationFrame的频率是相对于显示器的refresh-rate.

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


另请注意,setTimeout(fn, delay) 中的 delaylong,这意味着您传递的值将被取整数。

最后一点,Chrome 确实在其 setInteval() 实现中自我纠正了这次漂移,Firefox 和规范仍然没有,但它是 under (not so active) discussion