Предыдущий обзор:Серия HTML Standard: как браузеры анализируют страницы и скрипты
Введение
Цель этой статьи
- Понимать цель разработки и логику выполнения цикла событий и его связь с рендерингом.
- Понять цель дизайна requestIdleCallBack/requestAnimationFrame
- Понимать взаимосвязь между Event Loop и IdleCallBack/AnimationFrame/временем рендеринга.
Вы можете прочитать эту статью с этими вопросами:
- Зачем нужны циклы событий?
- Какое отношение Event Loop имеет к рендерингу?
- Как браузер реализует requestAnimationFrame, почему он может хранить номер кадра JS-анимации в рамках модели Event Loop?
- Как браузер реализует requestIdleCallBack, почему реакция выбирает его для планирования задач сравнения и как он рассчитывает время простоя браузера?
Описание цикла событий
В реализациях браузера, таких какЗадачи рендеринга, выполнение сценария JavaScript, взаимодействие с пользователем, сетьОбработка все работает в той же ните. Когда один тип задачи выполнен, это означает, что другие задачи заблокированы. Для того, чтобы выполнить каждую задачу в упорядоченном порядке, браузер реализует то, что мы называем процесс планирования петли события.
Эта модель проектирования приводит к изначально асинхронной природе JavaScript, что означает, что обратные вызовы, вызываемые интерфейсами браузера, такими как Ajax, будут происходить в будущих циклах цикла событий, а не блокировать текущую задачу.
Модель параллелизма по отношению к этой модели, аналогичная рендерингу и выполнению скрипта в iOS, также находится в одном потоке.Когда разработчик iOS инициирует сетевой запрос, для выполнения задачи запроса обычно используется поток с более низким приоритетом, и поток задачи завершен.После выполнения задачи вызывается основной поток для выполнения работы после сетевого запроса.
Давайте посмотрим на простое сравнение кода.
Производительность запроса URL-адреса изображения в JavaScript:
// 将一个网络请求任务推入Event Loop中的任务队列中
http.request('some.img').then(
// 将回调推入任务队列中排队,并在网络任务完成后置位可执行任务,等待执行(这里暂时忽视微任务和宏任务的区别)
(imgUrl) => imgElement.src = imgUrl;
)
Давайте посмотрим, как запросить изображение, не блокируя пользовательский интерфейс в iOS:
// 将请求任务主动推到额外线程的队列上
DispatchQueue.global().async {
// 在额外的线程中同步请求
let imageData = doRequestStuff()
// 完成后主动将后续任务推入到主线程中完成。
DispatchQueue.main.async {
UIImage(data: imageData)
}
}
Можно видеть, что самая большая разница между ними для разработчиков — активный и пассивный. Разработчики JavaScript несут ответственность только за инициирование запросов, а последующее планирование задач полностью передается циклу событий, скрытому за кулисами. Разработчики iOS должны активно поддерживать подробнее отношения между потоками.
requestIdleCallback, введение requestAnimationFrame
Почему эти два метода должны обсуждаться с Event Loop? В приведенном выше примере мы обнаружили, что реализация Event Loop значительно упрощает разработку пользовательского интерфейса.Как правило, разработчикам нужно только помещать задачи для выполнения в очередь Event Loop, и Event Loop, естественно, не блокирует отрисовку пользовательского интерфейса.
Хотя цикл событий имеет много преимуществ, все же есть некоторые проблемы. Цикл событий абстрагирует многопоточную модель разработки и скрывает сложные детали. Например, нам не нужно заботиться о том, чтобы сетевые запросы были параллельными запросами внутри браузера. что степень свободы, которую имеют разработчики сложных сцен, будет уменьшена.
Типичная проблема — это длинная задача, когда время выполнения задачи в цикле событий превышает 50 мс, пользователь может чувствовать себя застрявшим, другая проблема заключается в том, что в эветлупе слишком много задач, в результате чего задачи с высоким приоритетом не могут быть выполнены. выполняются вовремя (мы не можем контролировать приоритет задач); например, эффекты анимации Js.
Студенты, знакомые с таймерами JS, должны знать, что синхронизация settimeout и setinterval неточна.Рассмотрите следующий код:
// 我们期待这个动画帧数为20帧
var i = 1;
setInterval(() => {
element.style.width = `${i++}px`
}, 50)
// 在某些情况下我插入了一堆任务到队列中
for(var j = 0; j < 10000, j++) {
setTimeout(() => {
doSomeStuff()
}, 99)
}
Очевидно, что когда эта анимация хочет выполнить JS-скрипт второго кадра, впереди ставится в очередь 10 000 задач, а этот скрипт стоит в очереди на 10 001 (на самом деле ситуация должна быть сложнее), хотя браузер всегда рендерит после выполнения задачи .Он работает, но ключевые скрипты не выполняются, и отрендеренный интерфейс естественно такой же, из-за чего визуальный эффект подвисает.
такrequestAnimationFrameПоявляется. Его определение заключается в том, что обратный вызов этого метода будет выполнен до того, как браузер отрисует в следующий раз. То, как браузер реализует этот метод, может гарантировать, что наша анимация не задерживается задачами с длинной очередью. Мы поговорим об этом далее.
// 例子,来自MDN
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
Для requestIdleCallback самое известное приложение должно быть react. React использует requestIdleCallback для планирования задач сравнения, что позволяет избежать проблемы, связанной с тем, что одна задача сравнения занимает слишком много времени и приводит к зависанию интерфейса. Обратный вызов этого метода будет вызываться в событии Цикл вызывается, когда он бездействует, и предоставляет время простоя, которое браузер может использовать в следующий раз (т. е. время, которое у него есть до рендеринга следующего кадра).
Event Loop
Вот как цикл обработки событий описан в стандарте HTML:
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop.
Каждый из наших интерфейсов браузера имеет соответствующий цикл обработки событий. Внимательные студенты могут заметить, что это циклы событий. Каждый из наших интерфейсов имеет не один цикл событий, а несколько циклов событий, которые управляются разными циклами событий. Направления полностью отличается:
- window event loop(это то, о чем мы обычно говорим)
- рабочий цикл событий (каждый рабочий поток имеет связанный с ним цикл событий)
- цикл событий worklet (woklet — это поток, который может получить доступ к механизму рендеринга, мы обычно его не используем)
Цикл событий, о котором мы поговорим далее, по умолчанию относится к циклу событий окна. У нас не так много приложений для последнего. Рабочий процесс можно рассматривать как среду выполнения JavaScript, которая не может получить доступ к DOM, в то время как ворклет все еще находится в Этап черновика и в основном используется в приложениях, требующих сверхвысокой производительности Сценарий (где ворклет запускает машинный код вместо JavaScript).
Так называемый Event Loop — это наш интерфейс от создания до уничтожения, некоторые шаги, которые постоянно выполняются в браузере, и некоторые неотъемлемые свойства, хранящиеся для выполнения этих шагов.
Каждый цикл событий содержит следующие свойства:
- Несколько очередей задач (очередь задач — это набор, а не очередь), задачи часто называют макрозадачами, а конкретные задачи могут быть следующими: события, синтаксический анализ, обратные вызовы.
- Очередь mircotask, то есть очередь микрозадач
Вопрос 1. Почему существует несколько очередей задач? Потому что браузеры могут назначать разные приоритеты разным очередям для определения приоритетов определенных типов задач.
Вопрос 2: Почему очередь задач — это не очередь, а коллекция? Потому что браузер всегда будет выбирать исполняемые задачи для выполнения, а не на основании времени входа в очередь.
Следующие шаги будут выполняться в течение цикла событий:
- Выберите очередь задач с исполняемыми задачами и выполните в ней самую старую задачу
- Выполнять микрозадачу до тех пор, пока очередь микрозадач не опустеет.
- обновить рендеринг, определить, есть ли возможность рендеринга, возможность рендеринга определяется в соответствии с физической средой (в зависимости от производительности машины), если есть возможность рендеринга, браузер выполнит работу рендеринга
- Если нет следующей задачи/микрозадачи для выполнения, цикл обработки событий будет считать время простоя.
Видно, что браузер не выполняет работу по рисованию каждый тик, а принимает решение в соответствии с реальной ситуацией в физической среде.
Например: вставка элемента p в задачу, после завершения задачи, не означает, что галочка отрисует соответствующий элемент на интерфейсе.
Зачем нам нужны микрозадачи, когда у нас есть макрозадачи?
Мы привыкли понимать макрозадачи и микрозадачи как форму асинхронного выполнения в JavaScript.
По сути, асинхронной является только макрозадача, а микрозадача — это перераспределение порядка выполнения кода макрозадачи, после выполнения макрозадачи всегда будут выполняться все микрозадачи, в этом смысле микрозадача блокирует основной поток.Вы постоянно создаете новые микрозадачи в микрозадаче, и нет сомнений, что в интерфейсе появится анабиоз. Смысл всех микрозадач в том, чтобы выполнить некоторый код, который всегда хочет быть выполненным после завершения задачи.
В настоящее время многие мелкие партнеры могут думать о Promise.Я лично думаю, что функция инвертирования управления Promise является причиной, почему он сияет (большую часть времени нам нравится синтаксис Promise и цепные вызовы; на самом деле нам все равно является ли это микрозадачей) , а не потому, что Promise.then является микрозадачей.
Здесь есть несколько странных моментов: спецификация Promise должна быть написана на ECMAScript, который не должен иметь ничего общего с HTML Standard, но из-за специфики Promise браузеры в основном реализуют Promise согласно спецификации HTML Standard. Обещание. Затем зарегистрированное не называется микрозадачей, а называется работой.
requestAnimationFrame
После разговора о цикле событий кажется, что фронтенд-разработчики вообще не могут понять суть перед рендерингом.Чтобы решить эту проблему, w3c определяет метод requestAnimationFrame, и обратный вызов этого метода будет перед следующей отрисовкой браузера. .
вызов requestAnimationFrame, который поместит обратный вызов вanimation frame request callback list, а непустой список обратного вызова запроса кадра анимации заставит браузер периодически добавлять задачу в цикл событий для выполнения обратного вызова, зарегистрированного requestAnimationFrame.Цикл здесь не указан, но мы можем легко предположить и просто в событии цикл, связанный с возможностью рендеринга.
До сих пор мы так и не увидели превосходства использования requestAnimationFrame над setinterval для создания JS-анимаций.Все периодически подталкивают задачи в цикл обработки событий.Почему requestAnimationFrame должен быть более стабильным?
Ответ скрыт в несоответствии приоритетов между несколькими очередями задач в цикле событий.Каждая задача имеет атрибут источника задачи, который определяет, к какой очереди задач принадлежит задача, а очередь задач имеет разные приоритеты выполнения, которые, очевидно, определяются кадр анимации.Приоритет задачи, созданной, когда список обратного вызова запросов не пуст, выше, чем у таймера.
Все функции обратного вызова в списке обратных вызовов запроса кадра анимации будут выполняться в одной задаче, что означает, что несколько синхронных вызовов requestAnimationFrame будут последовательно выполняться в одной задаче до следующего рендеринга.
requestIdleCallback
Прежде чем говорить о requestIdleCallback, давайте вспомним недавно запущенную архитектуру React 16, основанную на волокне.
Прежде всего, давайте посмотрим на проблемы с согласователем стека, который все еще является примером JS-анимации.Если мы используем requestAnimationFrame для модуляции нашей анимации, если нет длинной задачи, количество кадров анимации будет гарантировано; но если время выполнения задачи превышает 50 мс, то никто не может гарантировать, что интерфейс не зависнет, это проблема стек-реконсилера, и есть проблема, что один дифф занимает слишком много времени.
React представил волокно для решения этой проблемы, улучшения плавности анимации, разделения задач на несколько кадров и обеспечения того, чтобы подзадачи не казались длинными задачами.Основная возможность волокна — requestIdleCallback.
Вызов метода requestIdleCallback заставит браузер вызывать функцию обратного вызова этого метода в периоды простоя.
Вернемся к потоку выполнения цикла событий, мы можем знать, что время простоя будет рассчитываться, когда в цикле событий нет задач для выполнения.Есть два случая для расчета времени простоя.
Когда есть кадры, которые нужно рендерить непрерывно, время простоя будет равно частоте кадров минус время выполнения задачи минус время выполнения отрисовки. Когда в течение некоторого времени не выполняется рисование и выполнение задач, время простоя будет максимально возможным, но не превысит 50 мс.Волшебное число 50 мс появилось в результате анализа больших данных.Некоторые исследования показывают, что изображения со скоростью более 50 мс/кадр заставляют людей чувствовать себя застрявшими, поэтому мы часто просим, чтобы задача не была слишком длинной, что и является причиной.
Точно так же обратный вызов, который вызывает метод requestIdleCallback, не будет напрямую попадать в очередь задач, а будет вычислять период простоя перед окончанием каждого раунда цикла событий.Если период простоя больше 0, задача будет помещена в очередь.
Совет: Время простоя связано не только с частотой рендеринга, но также имеет определенную связь с таймером, который должен выполняться в последнее время.Период простоя всегда будет меньше, чем время, которое должно выполняться следующим таймером.
Синхронно вызывать requestIdleCallback несколько раз. Выполнение обратного вызова этого метода может быть распределено по разным кадрам. После выполнения каждого обратного вызова браузер будет проверять, осталось ли какое-либо оставшееся время простоя. Если нет, управление выполнением будет возвращено в цикл событий ., если есть, он продолжит выполнение следующего обратного вызова, похоже ли это на планирование реактивного волокна.
Суммировать
Цель этой статьи и предыдущей статьи об этой системе — выяснить жизненный цикл и рабочий процесс всего интерфейса, а также понять прямую связь между рисованием и выполнением скрипта.
Я считаю, что разработчики React не могут спроектировать архитектуру, подобную Fibre, не прочитав спецификацию, которая обеспечивает теоретическую основу для высокопроизводительной веб-разработки.
Предыдущий:Серия HTML Standard: как браузеры анализируют страницы и скрипты