Почему requestAnimationFrame () выполняет мой код в конце кадра, а не в его начале? - PullRequest
1 голос
/ 25 сентября 2019
var y = 0
canvas.height *= 5
ctx.fillStyle = 'green'
function update () {
  requestAnimationFrame(update)
   ctx.clearRect(0, 0, canvas.width, canvas.height)
  ctx.fillRect(0, y, 300, 300)
  y++
}
update()

Для этого простого JSBin (https://jsbin.com/yecowob/edit?html,js,output), когда по экрану движется квадрат, вот так выглядит временная шкала инструментов Chrome dev:

https://imgur.com/Ai0BxY0

Как я понимаю, вертикальная пунктирная серая линия - это конец текущего кадра и начало следующего. На скриншоте у нас есть кадр 19,3 мс, где браузер почти ничего не делает (много времени простоя).t браузер избегает этого, если он просто запускает весь код в правильном порядке при запуске кадра?

Если, однако, я рисую квадрат 500 раз при 6-кратном замедлении ЦП (https://jsbin.com/yecowob/4/edit?js,output), Я получаю периоды, когдабраузер делает именно то, что я хочу (чтобы запустить код при запуске кадра), но он снова не синхронизируется:

https://imgur.com/Y04XCrz

Когда он запускается по пунктирной линииfps намного более плавный, но я могу заставить его работать только тогда, когда браузер должен сделать тяжелую работу.

Так почему же requestAnimationFrame () не запускается каждый раз в начале кадра,и как я могу сделать это со?

Большое спасибо за любую помощь, которую вы можете оказать мне в этом.

1 Ответ

1 голос
/ 25 сентября 2019

Потому что это то, что делает requestAnimationFrame: он планирует обратный вызов для запуска в следующем «кадре рисования», непосредственно перед фактическим рисованием на экране.

Здесь «кадр» относится к итерации цикла события,не визуальный, и в дальнейшем я буду продолжать использовать итерацию цикла событий, чтобы провести различие.

Итак, если мы посмотрим на структуру итерации цикла события , как описано в спецификации HTML, мы увидим, что " запускает обратные вызовы фрейма анимации "алгоритм вызывается изнутри алгоритма" update рендеринга ".
Этот алгоритм отвечает на этапе 2 определения,текущая итерация цикла событий является рисованием или нет, проверяя « возможности рендеринга » каждого активного документа.Если это не так, то все внутренние шаги, приведенные ниже, отбрасываются, включая наш " запуск обратных вызовов кадра анимации ".
Это означает, что наши requestAnimationFrame запланированные обратные вызовы будутвыполняются только в очень особенной итерации цикла событий: следующая с возможностью рендеринга .

Спецификации не описывают точно, с какой частотой должны возникать эти "рисованные кадры", но в основномбольшинство современных производителей стараются поддерживать 60 Гц, в то время как Chrome ограничит частоту обновления активного дисплея. Ожидается, что поведение Chrome распространится и на других поставщиков.


То, что вы описываете, является нормальным.Если вам нужна упрощенная версия этого, вы можете думать о requestAnimationFrame( fn ) как о setTimeout( fn, time_needed_until_the_next_painting_frame ) (с незначительным отличием, что обратные вызовы по тайм-ауту выполняются в начале итерации цикла обработки событий 1039 * при анимации обратные вызовы кадров выполняются в конце).

Почему он был спроектирован таким образом?

Хорошо, потому что большую часть времени мы хотим иметь самые свежиеинформация нарисована на экране.Таким образом, наличие этих обратных вызовов для запуска непосредственно перед рисованием гарантирует, что все, что должно быть нарисовано, находится на своей самой последней позиции.

Но это также означает, что действительно, у нас не должно быть слишком тяжелых операций, происходящих там, нариск потери возможности рисования.


Теперь я должен отметить, что существует текущее предложение о включении requestPostAnimationFrame, которое запланировало бы обратные вызовы для следующего«рисовать рамку», просто после фактического рисования на экране.
При использовании этого метода вы получите ожидаемое поведение.
К сожалению, это всего лишь предложение, которое не было включенок спецификациям, и неясно, будет ли это когда-либо.

Несмотря на то, что он уже реализован в Chrome, за флагом « Experimental Web Platform », лучшее, что мы можем сделать, чтобы приблизиться к его поведению в обычных браузерах, - это запланировать обратный вызов на самомначало следующей итерации цикла события .
Вот пример реализации, которую я сделал для другого Q / A :

if (typeof requestPostAnimationFrame !== 'function') {
  monkeyPatchRequestPostAnimationFrame();
}

requestAnimationFrame( animationFrameCallback );
requestPostAnimationFrame( postAnimationFrameCallback );

// monkey-patches requestPostAnimationFrame
//!\ Can not be called from inside a requestAnimationFrame callback
function monkeyPatchRequestPostAnimationFrame() {
  console.warn('using a MessageEvent workaround');
  const channel = new MessageChannel();
  const callbacks = [];
  let timestamp = 0;
  let called = false;
  channel.port2.onmessage = e => {
    called = false;
    const toCall = callbacks.slice();
    callbacks.length = 0;
    toCall.forEach(fn => {
      try {
        fn(timestamp);
      } catch (e) {}
    });
  }
  window.requestPostAnimationFrame = function(callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('Argument 1 is not callable');
    }
    callbacks.push(callback);
    if (!called) {
      requestAnimationFrame((time) => {
        timestamp = time;
        channel.port1.postMessage('');
      });
      called = true;
    }
  };
}


// void loops, look at your dev-tools' timeline to see where each fires
function animationFrameCallback() {
  requestAnimationFrame( animationFrameCallback );
}
function postAnimationFrameCallback() {
  requestPostAnimationFrame( postAnimationFrameCallback )
}
...