Как цикл обработки событий в браузере обрабатывает макрокоманды? - PullRequest
0 голосов
/ 15 января 2019

Я смотрел выступление Джейка Арчибальда о цикле событий - https://vimeo.com/254947206. Основываясь на этом разговоре, я понял, что цикл событий будет выполнять столько макрозадач, сколько может уместиться в одном кадре, и если есть какая-то долго выполняющаяся макрорежим это приведет к пропуску кадров. Поэтому я ожидал, что любая задача, выполняющаяся дольше, чем обычная длительность кадра, приведет к выполнению других задач в следующем кадре. Я проверил это, создав одну кнопку и несколько обработчиков, как это https://codepen.io/jbojcic1/full/qLggVW

Я заметил, что, хотя handlerOne работает долго (из-за вычисления вычислительных фибоначчи), обработчики 2, 3 и 4 по-прежнему выполняются в одном кадре. Только timeoutHandler выполняется в следующем кадре. Вот логи, которые я получаю:

  animationFrameCallback - 10:4:35:226
  handler one called. fib(40) = 102334155
  handler two called.
  handler three called.
  handler four called.
  animationFrameCallback - 10:4:36:37
  timeout handler called
  animationFrameCallback - 10:4:36:42

поэтому вопрос в том, почему обработчики два, три и четыре выполняются в том же кадре, что и обработчик один?

Чтобы сделать вещи еще более запутанными согласно https://developer.mozilla.org/en-US/docs/Web/API/Frame_Timing_API,

Фрейм представляет объем работы, выполняемый браузером в одном событии. итерация цикла, такая как обработка событий DOM, изменение размера, прокрутка, рендеринг, CSS анимация и т. д.

и для объяснения "одной итерации цикла событий" они связали https://html.spec.whatwg.org/multipage/webappapis.html#processing-model-8, где указано, что за одну итерацию:

  • обработано одно макро-задание,
  • все микро-задачи обрабатываются
  • рендеринг обновлен
  • ... (есть и другие шаги, которые не важно для этого)

, что, похоже, не совсем правильно.

1 Ответ

0 голосов
/ 15 января 2019

Вы смешиваете несколько понятий здесь.

«Кадр», который вы измеряете в коде, - это один из шагов 10 - Обновление рендеринга . Цитирую спецификации:

Эта спецификация не требует какой-либо конкретной модели для выбора возможностей рендеринга. Но, например, если браузер пытается достичь частоты обновления 60 Гц, то возможности рендеринга появляются максимум каждые 60 секунд (около 16,7 мс). Если браузер обнаружит, что контекст просмотра не может поддерживать эту скорость, он может упасть до более устойчивых 30 возможностей рендеринга в секунду для этого контекста просмотра, а не время от времени отбрасывать кадры. Точно так же, если контекст просмотра не виден, пользовательский агент может решить отбросить эту страницу до гораздо более медленных 4 возможностей рендеринга в секунду или даже меньше.

Таким образом, неизвестно, на какой частоте будет срабатывать этот * frame , но обычно он работает на частоте 60 кадров в секунду (большинство мониторов обновляется на частоте 60 Гц), поэтому за этот промежуток времени много итераций циклов событий обычно происходит.

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


В статье, о которой вы ссылаетесь, речь идет о « frame » в области PerformanceFrameTiming API . Я должен прямо признать, что у меня мало знаний об этом конкретном API, и, учитывая его очень ограниченную поддержку браузера, я не думаю, что нам следует тратить на него слишком много времени, кроме как сказать, что это не имеет ничего общего с рамкой для покраски.

Я думаю, что наиболее точным инструментом для измерения итерации EventLoop в настоящее время является Messaging API .
Создав цикл вызова сообщения с самозвонком, мы можем подключиться к каждой итерации EventLoop.

let stopped = false;
let eventloops = 0;
onmessage = e => {
  if(stopped) {
    console.log(`There has been ${eventloops} Event Loops in one anim frame`);
    return;
  }
  eventloops++
  postMessage('', '*');
};
requestAnimationFrame(()=> {
  // start the message loop
  postMessage('', '*');
  // stop in one anim frame
  requestAnimationFrame(()=> stopped = true);
});

Давайте посмотрим, как ваш код ведет себя на более глубоком уровне:

let done = false;
let started = false;
onmessage = e => {
  if (started) {
    let a = new Date();
    console.log(`new EventLoop - ${a.getHours()}:${a.getMinutes()}:${a.getSeconds()}:${a.getMilliseconds()}`);
  }
  if (done) return;
  postMessage('*', '*');
}

document.getElementById("button").addEventListener("click", handlerOne);
document.getElementById("button").addEventListener("click", handlerTwo);
document.getElementById("button").addEventListener("click", handlerThree);
document.getElementById("button").addEventListener("click", handlerFour);

function handlerOne() {
  started = true;
  setTimeout(timeoutHandler);
  console.log("handler one called. fib(40) = " + fib(40));
}

function handlerTwo() {
  console.log("handler two called.");
}

function handlerThree() {
  console.log("handler three called.");
}

function handlerFour() {
  console.log("handler four called.");
  done = true;
}

function timeoutHandler() {
  console.log("timeout handler called");
}

function fib(x) {
  if (x === 1 || x === 2) return 1
  return fib(x - 1) + fib(x - 2);
}
postMessage('*', '*');
<button id="button">Click me</button>

Хорошо, так что на самом деле есть один кадр , как в итерации EventLoop для запуска между обработчиками событий и обратным вызовом setTimeout. Мне нравится больше.

Но как насчет "длинных кадров" , о которых мы слышали?

Полагаю, вы говорите о алгоритме «вращения цикла событий» , который действительно предназначен для того, чтобы цикл событий не блокировал весь пользовательский интерфейс при некоторых обстоятельствах ,

Во-первых, спецификации только говорят разработчикам, что рекомендуется вводить этот алгоритм для долго выполняющихся сценариев, это не обязательно.

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

Так что на самом деле у js нет способа узнать, вводили ли мы этот алгоритм.

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

Вот попытка изобразить более графически, рискуя быть технически неточным:

/**
 * ...
 * - handle events
 *    user-click => push([cb1, cb2, cb3]) to call stack
(* - paint if needed (may execute rAF callbacks if any))
 *
 * END OF LOOP
—————————————————————————
 * BEGIN OF LOOP
 *
 * - execute call stack
 *    cb1()
 *      schedule `timeoutHandler`
 *      fib()
 *      ...
 *      ...
 *      ...
 *      ... <-- takes too long => "spin the event loop"
 * [ pause call stack ]
 * - handle events
(* - paint if needed (but do not execute rAF callbacks))
 *
 * END OF LOOP
—————————————————————————
 * BEGIN OF LOOP
 *
 * - execute call stack
 * [ resume call stack ]
 *      (*fib()*)
 *      ...
 *      ...
 *    cb2()
 *    cb3()
 * - handle events
 *   `timeoutHandler` timed out => push to call stack
(* - paint if needed (may execute rAF callbacks if any) )
 *
 * END OF LOOP
—————————————————————————
 * BEGIN OF LOOP
 *
 * - execute call stack
 *   `timeoutHandler`()
 * - handle events
 ...
 */
...