Принципы работы современных браузеров
Вступление
Это подробное руководство по внутренним операциям WebKit и Gecko – результат обширного исследования, проведенного израильским разработчиком Тали Гарсиэль (Tali Garsiel). За несколько лет она изучила все опубликованные данные о внутреннем устройстве браузеров и потратила много времени на чтение исходного кода веб-браузеров. Она написала:
Веб-разработчикам знание внутренних процессов браузера помогает принимать более обоснованные решения и понимать, почему рекомендации по разработке именно такие, какие они есть. Хотя это довольно длинный документ, мы рекомендуем вам потратить некоторое время на его изучение. сервисы Google.
Пол Айриш (Paul Irish), Chrome Developer Relations
Введение
Веб-браузеры – это наиболее широко используемое программное обеспечение. В этом руководстве я объясню, как они работают за кулисами. Мы посмотрим, что произойдет, когда вы введете google.com в адресной строке и на экране браузера появится страница Google.
Браузеры, о которых мы поговорим
В настоящее время на компьютерах используются пять основных браузеров: Chrome, Internet Explorer, Firefox, Safari и Opera. На мобильных устройствах основными браузерами являются Android Browser, iPhone, Opera Mini, Opera Mobile, UC Browser, Nokia S40/S60 и Chrome. Все они, кроме браузеров Opera, основаны на WebKit. Я приведу примеры из браузеров с открытым исходным кодом Firefox и Chrome, а также Safari (который частично является ПО с открытым исходным кодом). По статистике StatCounter (на июнь 2013 г.), на долю Chrome, Firefox и Safari приходится около 71% мирового рынка браузеров для компьютеров. На мобильных устройствах около 54% трафика приходится на браузеры Android, iPhone и Chrome.
Основная функция браузера
Основная функция браузера – показывать выбранный вами веб-ресурс, запрашивая его с сервера и отображая в окне браузера. Обычно это HTML-документ, но может быть и PDF-файл, изображение или другой тип контента. Местоположение ресурса указывается пользователем с помощью URI (унифицированного идентификатора ресурса).
То, как браузер интерпретирует и отображает HTML-файлы, указано в спецификациях HTML и CSS. Эти спецификации поддерживаются организацией W3C (World Wide Web Consortium), которая является организацией по стандартизации для Интернета. В течение многих лет браузеры соответствовали только части спецификаций и разрабатывали собственные расширения. Это приводило к серьезным проблемам совместимости для веб-разработчиков. Сегодня большинство браузеров в той или иной степени соответствуют спецификациям.
Пользовательские интерфейсы браузеров имеют много общего. К распространенным элементам пользовательского интерфейса относятся:
- Адресная строка для вставки URI
- Кнопки "Назад" и "Вперед"
- Настройки закладок
- Кнопки обновления и остановки для обновления или остановки загрузки текущих документов
- кнопка "Главная страница";
Как ни странно, пользовательский интерфейс браузера не описан ни в одной официальной спецификации. Он просто основан на передовых практиках, сформированных за годы опыта, и на том, что браузеры имитируют друг друга. В спецификации HTML5 не определены элементы интерфейса, которые должны быть в браузере, но перечислены некоторые распространенные элементы. К ним относятся адресная строка, строка состояния и панель инструментов. Конечно, есть функции, уникальные для конкретного браузера, например менеджер загрузок Firefox.
Высокоуровневая инфраструктура
Основные компоненты браузера:
- Пользовательский интерфейс. Включает адресную строку, кнопки "Назад" и "Вперед", меню закладок и т. д. Это все элементы браузера, кроме окна, в котором отображается запрошенная страница.
- Ядро браузера – управляет действиями между интерфейсом и ядром рендеринга.
- Механизм визуализации отвечает за показ запрошенного контента. Например, если запрашивается HTML-контент, механизм визуализации анализирует HTML и CSS и показывает обработанный контент на экране.
- Сеть. Для сетевых вызовов, таких как HTTP-запросы, используются разные реализации для разных платформ за независимым от платформы интерфейсом.
- Бэкенд пользовательского интерфейса используется для отрисовки базовых виджетов, таких как поля со списком и окна. Этот бэкенд предоставляет общий интерфейс, не зависящий от платформы. Для этого используются методы пользовательского интерфейса операционной системы.
- Интерпретатор JavaScript. Используется для анализа и выполнения кода JavaScript.
- Хранение данных. Это уровень сохраняемости. Браузеру может потребоваться сохранить на устройстве различные данные, например файлы cookie. Браузеры также поддерживают такие механизмы хранения, как localStorage, IndexedDB, WebSQL и FileSystem.
Важно отметить, что такие браузеры, как Chrome, запускают несколько экземпляров механизма рендеринга – по одному для каждой вкладки. Каждая вкладка работает в отдельном процессе.
Движки рендеринга
Задачей движка рендеринга является… рендеринг, то есть отображение запрошенного контента на экране браузера.
По умолчанию движок рендеринга может показывать документы HTML и XML, а также изображения. С помощью плагинов и расширений браузер может показывать и другие типы данных, например документы PDF. Однако в этом разделе мы сосредоточимся на основном сценарии использования: показе HTML-кода и изображений, отформатированных с помощью CSS.
В разных браузерах используются разные механизмы визуализации: в Internet Explorer – Trident, в Firefox – Gecko, в Safari – WebKit. В Chrome и Opera (начиная с версии 15) используется Blink – форк WebKit.
WebKit – это механизм визуализации с открытым исходным кодом, который изначально был разработан для платформы Linux, а затем модифицирован компанией Apple для поддержки Mac и Windows.
Основной поток
Движок рендеринга начинает получать содержимое запрошенного документа от сетевого уровня. Обычно это делается фрагментами по 8 кБ.
После этого движок рендеринга выполняет следующие действия:
Движок визуализации начинает анализировать HTML-документ и преобразовывать элементы в узлы DOM в дереве, которое называется деревом контента. Движок проанализирует данные о стилях как во внешних файлах CSS, так и в элементах стилей. Информация о стилях вместе с визуальными инструкциями в HTML используется для создания другого дерева – дерева визуализации.
Дерево визуализации содержит прямоугольники с визуальными атрибутами, такими как цвет и размеры. Прямоугольники расположены в правильном порядке для отображения на экране.
После создания дерева визуализации начинается процесс макета. Это означает, что каждому узлу задаются точные координаты, в которых он должен появиться на экране. Следующий этап – отрисовка. Браузер проходит по дереву визуализации и отрисовывает каждый узел с помощью фонового слоя пользовательского интерфейса.
Важно понимать, что это постепенный процесс. Для удобства пользователей механизм визуализации пытается как можно быстрее показать контент на экране. Он не будет ждать, пока весь HTML-код будет проанализирован, прежде чем начать построение и компоновку дерева рендеринга. Части контента будут обработаны и показаны, а остальной контент будет поступать из сети и обрабатываться.
Примеры основного процесса
Из рисунков 3 и 4 видно, что хотя в WebKit и Gecko используется немного разная терминология, процесс в основном один и тот же.
В Gecko дерево визуально отформатированных элементов называется деревом фреймов. Каждый элемент – это фрейм. В WebKit используется термин "дерево рендеринга", состоящее из "объектов рендеринга". В WebKit для размещения элементов используется термин "макет", а в Gecko – "перекомпоновка". "Прикрепление" – это термин WebKit, обозначающий соединение узлов DOM и визуальной информации для создания дерева визуализации. Незначительное несемантическое различие заключается в том, что у Gecko есть дополнительный уровень между HTML и деревом DOM. Он называется "приемником контента" и используется для создания элементов DOM. Мы рассмотрим каждый этап:
Синтаксический анализ – общие сведения
Поскольку синтаксический анализ – это очень важный процесс в механизме визуализации, мы рассмотрим его более подробно. Давайте начнем с небольшого введения в синтаксический анализ.
Парсинг документа означает его перевод в структуру, которую может использовать код. Результатом синтаксического анализа обычно является дерево узлов, представляющих структуру документа. Это называется деревом синтаксического анализа или синтаксическим деревом.
Например, при синтаксическом анализе выражения 2 + 3 - 1 может быть возвращено следующее дерево:
Грамматика.
Синтаксический анализ выполняется на основе правил синтаксиса, которым подчиняется документ, то есть языка или формата, на котором он был написан. Каждый формат, который вы можете проанализировать, должен иметь детерминированную грамматику, состоящую из словарного запаса и синтаксических правил. Она называется контекстно-свободной грамматикой. Человеческие языки не являются такими языками и поэтому не могут быть проанализированы с помощью обычных методов синтаксического анализа.
Комбинация синтаксического и лексического анализаторов
Синтаксический анализ можно разделить на два подпроцесса: лексический анализ и синтаксический анализ.
Лексический анализ – это процесс разбиения входных данных на токены. Токены – это словарный запас языка, то есть набор допустимых строительных блоков. В человеческом языке он будет состоять из всех слов, которые есть в словаре этого языка.
Синтаксический анализ – это применение правил синтаксиса языка.
Обычно синтаксический анализатор состоит из двух компонентов: лексического анализатора (иногда его называют токенизатором), который разбивает входные данные на допустимые токены, и синтаксического анализатора, который строит дерево синтаксического анализа, анализируя структуру документа в соответствии с правилами синтаксиса языка.
Лексический анализатор умеет удалять ненужные символы, такие как пробелы и разрывы строк.
Процесс синтаксического анализа является итеративным. Обычно синтаксический анализатор запрашивает у лексического анализатора новый токен и пытается сопоставить его с одним из синтаксических правил. Если соответствие найдено, узел, соответствующий токену, добавляется в дерево синтаксического анализа, а анализатор запрашивает следующий токен.
Если ни одно правило не подходит, синтаксический анализатор сохранит токен и продолжит запрашивать токены, пока не найдет правило, соответствующее всем сохраненным токенам. Если правило не найдено, синтаксический анализатор выдаст исключение. Это означает, что документ недействителен и содержит синтаксические ошибки.
Перевод
Во многих случаях дерево синтаксического анализа не является конечным продуктом. Синтаксический анализ часто используется при переводе: преобразовании входного документа в другой формат. Пример – компиляция. Компилятор, который компилирует исходный код в машинный код, сначала анализирует его в дерево синтаксического анализа, а затем преобразует дерево в документ машинного кода.
Пример синтаксического анализа
На рисунке 5 мы построили дерево синтаксического анализа из математического выражения. Давайте попробуем определить простой математический язык и посмотрим на процесс синтаксического анализа.
Синтаксис:
- Синтаксис языка состоит из выражений, терминов и операций.
- В нашем языке может быть любое количество выражений.
- Выражение состоит из термина, за которым следует операция, а затем другой термин.
- Операция – это токен плюса или минуса.
- Терм – это целочисленный токен или выражение.
Давайте проанализируем входные данные 2 + 3 - 1.
Первая подстрока, соответствующая правилу, – 2. Согласно правилу 5 это термин.
Второе совпадение – 2 + 3. Оно соответствует третьему правилу: термин, за которым следует операция, за которой следует другой термин.
Следующее совпадение будет найдено только в конце входных данных.
2 + 3 - 1 – это выражение, потому что мы уже знаем, что 2 + 3 – это терм, поэтому у нас есть терм, за которым следует операция, за которой следует другой терм.
2 + + не соответствует ни одному правилу и поэтому является недопустимым вводом.
Формальные определения для лексики и синтаксиса
Словарь обычно выражается регулярными выражениями.
Например, язык будет определен следующим образом:
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
Как видите, целые числа определяются регулярным выражением.
Синтаксис обычно определяется в формате BNF. Язык будет определяться следующим образом:
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
Мы сказали, что язык может быть проанализирован обычными синтаксическими анализаторами, если его грамматика является контекстно-свободной. Интуитивное определение контекстно-свободной грамматики — это грамматика, которая может быть полностью выражена в BNF. Формальное определение можно найти в статье Википедии о контекстно-свободной грамматике.
Типы синтаксических анализаторов
Существует два типа синтаксических анализаторов: нисходящие и восходящие. Интуитивно понятное объяснение заключается в том, что нисходящие синтаксические анализаторы исследуют высокоуровневую структуру синтаксиса и пытаются найти совпадение правил. Восходящие синтаксические анализаторы начинают с входных данных и постепенно преобразуют их в синтаксические правила, начиная с правил низкого уровня и заканчивая правилами высокого уровня.
Давайте посмотрим, как два типа синтаксических анализаторов будут анализировать наш пример.
Синтаксический анализатор, работающий по нисходящему принципу, начнет с правила более высокого уровня: он определит 2 + 3 как выражение. Затем оно определит 2 + 3 - 1 как выражение (процесс определения выражения развивается, соответствуя другим правилам, но начальной точкой является правило самого высокого уровня).
Синтаксический анализатор снизу вверх будет сканировать входные данные, пока не будет найдено совпадение с правилом. Затем он заменит совпадающие входные данные правилом. Это будет продолжаться до конца ввода. Частично совпадающее выражение помещается в стек синтаксического анализатора.
Этот тип восходящего синтаксического анализатора называется синтаксическим анализатором сдвига-свертки, поскольку входные данные сдвигаются вправо (представьте себе указатель, указывающий сначала на начало ввода и перемещающийся вправо) и постепенно сводятся к синтаксическим правилам.
Автоматическое создание синтаксических анализаторов
Существуют инструменты, которые могут генерировать синтаксический анализатор. Вы передаете им грамматику вашего языка - его словарный запас и правила синтаксиса - и они генерируют рабочий синтаксический анализатор. Создание синтаксического анализатора требует глубокого понимания синтаксического анализа, и создать оптимизированный синтаксический анализатор вручную непросто, поэтому генераторы синтаксических анализаторов могут быть очень полезны.
WebKit использует два известных генератора синтаксических анализаторов: Flex для создания лексического анализатора и Bison для создания синтаксического анализатора (вы могли встречать их под названиями Lex и Yacc). Входные данные Flex – это файл, содержащий определения токенов в виде регулярных выражений. Входными данными для Bison являются правила синтаксиса языка в формате BNF.
Синтаксический анализатор HTML
Задача синтаксического анализатора HTML – преобразовать HTML-разметку в дерево синтаксического анализа.
Грамматика HTML
Словарь и синтаксис HTML определены в спецификациях, созданных организацией W3C.
Как мы видели во введении в синтаксический анализ, синтаксис грамматики можно формально определить с помощью таких форматов, как BNF.
К сожалению, все традиционные темы синтаксического анализа не применимы к HTML (я упомянул их не просто так – они будут использоваться при синтаксическом анализе CSS и JavaScript). HTML нелегко определить с помощью контекстно-свободной грамматики, которая необходима синтаксическим анализаторам.
Существует формальный формат для определения HTML – DTD (определение типа документа), но это не контекстно-свободная грамматика.
На первый взгляд это может показаться странным, ведь HTML и XML очень похожи. Существует множество XML-парсеров. Существует XML-вариация HTML – XHTML. В чем же разница?
Разница в том, что HTML-код более "снисходителен": он позволяет опускать некоторые теги (которые затем добавляются неявно), а иногда и начальные или конечные теги и т. д. В целом синтаксис HTML более гибкий, чем у XML.
Эта, казалось бы, незначительная деталь имеет огромное значение. С одной стороны, это основная причина популярности HTML: он прощает ошибки и облегчает жизнь веб-автору. С другой стороны, это затрудняет написание формальной грамматики. Таким образом, HTML не может быть легко проанализирован обычными парсерами, поскольку его грамматика не является контекстно-свободной. HTML-код не может быть обработан синтаксическими анализаторами XML.
HTML DTD
Определение HTML в формате DTD. Этот формат используется для определения языков семейства SGML. В формате содержатся определения всех допустимых элементов, их атрибутов и иерархии. Как мы видели ранее, HTML DTD не формирует контекстно-свободную грамматику.
Существует несколько вариантов DTD. Строгий режим соответствует только спецификациям, а другие режимы поддерживают разметку, которая использовалась браузерами в прошлом. Это необходимо для обратной совместимости с более старым контентом. Текущая версия строгого DTD доступна по адресу www.w3.org/TR/html4/strict.dtd.
DOM
Выходное дерево (дерево синтаксического анализа) – это дерево элементов DOM и узлов атрибутов. DOM – это объектная модель документа. Это объектное представление HTML-документа и интерфейс элементов HTML для внешнего мира, например JavaScript.
Корнем дерева является объект Document.
DOM имеет почти однозначное отношение к разметке. Пример:
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
Эта разметка будет преобразована в следующее дерево DOM:
Как и HTML, DOM определяется организацией W3C. Подробнее: www.w3.org/DOM/DOMTR. Это общая спецификация для работы с документами. Специальный модуль описывает элементы HTML. Определения HTML можно найти на странице www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html.
Когда я говорю, что дерево содержит узлы DOM, я имею в виду, что дерево состоит из элементов, которые реализуют один из интерфейсов DOM. Браузеры используют конкретные реализации, у которых есть другие атрибуты, используемые браузером внутри.
Алгоритм синтаксического анализа
Как мы видели в предыдущих разделах, HTML не может быть проанализирован с использованием обычных нисходящих или восходящих синтаксических анализаторов.
Причины:
- Прощающий характер языка.
- Браузеры традиционно допускают ошибки, чтобы поддерживать известные случаи недействительного HTML.
- Процесс синтаксического анализа является реентерабельным. В других языках исходный код не меняется во время синтаксического анализа, но в HTML динамический код (например, элементы скрипта, содержащие вызовы
document.write()) может добавлять дополнительные токены, поэтому процесс синтаксического анализа фактически изменяет входные данные.
Поскольку стандартные методы синтаксического анализа не подходят, браузеры создают собственные синтаксические анализаторы для HTML.
Алгоритм синтаксического анализа подробно описан в спецификации HTML5. Алгоритм состоит из двух этапов: токенизации и построения дерева.
Токенизация – это лексический анализ, разбиение входных данных на токены. К токенам HTML относятся начальные и конечные теги, а также названия и значения атрибутов.
Токенизатор распознает токен, передает его конструктору дерева и использует следующий символ для распознавания следующего токена и так далее до конца ввода.
Алгоритм токенизации
Результатом работы алгоритма является токен HTML. Алгоритм представлен в виде конечного автомата. Каждое состояние использует один или несколько символов входного потока и обновляет следующее состояние в соответствии с этими символами. Решение зависит от текущего состояния токенизации и состояния построения дерева. Это означает, что один и тот же символ может привести к разным результатам в зависимости от текущего состояния. Алгоритм слишком сложен, чтобы описывать его полностью, поэтому давайте рассмотрим простой пример, который поможет нам понять принцип.
Простой пример – токенизация следующего HTML-кода:
<html>
<body>
Hello world
</body>
</html>
Начальное состояние – "Состояние данных".
Когда встречается символ <, состояние меняется на Tag open state (Состояние открытого тега).
При обработке символа a-z создается токен начального тега, а состояние меняется на "Состояние названия тега".
Это состояние сохраняется до тех пор, пока не будет обработан символ >. Каждый символ добавляется к новому названию токена. В нашем случае созданный токен – это токен html.
Когда достигается тег >, текущий токен передается, а состояние снова меняется на Состояние данных.
Тег <body> будет обрабатываться так же.
Пока что были созданы теги html и body. Мы вернулись к состоянию данных.
Обработка символа H в Hello world приведет к созданию и отправке токена символа. Это будет продолжаться, пока не будет достигнут символ < в </body>. Мы создадим токен для каждого символа в Hello world.
Мы вернулись к открытому тегу.
При обработке следующего входного символа / будет создан токен end tag token и произойдет переход в состояние названия тега. Мы остаемся в этом состоянии, пока не достигнем >.Затем будет создан новый токен тега, и мы вернемся в состояние данных.
Входные данные </html> будут обрабатываться так же, как и в предыдущем случае.
Алгоритм построения дерева
При создании синтаксического анализатора создается объект Document. На этапе построения дерева DOM-дерево с документом в корне будет изменено, и в него будут добавлены элементы. Каждый узел, созданный токенизатором, обрабатывается конструктором дерева. Для каждого токена в спецификации указано, какой элемент DOM ему соответствует и будет создан для него. Элемент добавляется в дерево DOM, а также в стек открытых элементов. Этот стек используется для исправления несоответствий вложенности и незакрытых тегов. Алгоритм также описывается как конечный автомат. Эти состояния называются режимами вставки.
Давайте посмотрим, как строится дерево для примера входных данных:
<html>
<body>
Hello world
</body>
</html>
Входными данными для этапа построения дерева является последовательность токенов, полученная на этапе токенизации. Первый режим – начальный. Получение токена html приведет к переходу в режим before html и повторной обработке токена в этом режиме. Это приведет к созданию элемента HTMLHtmlElement, который будет добавлен к корневому объекту Document.
Статус изменится на "before head". Затем принимается токен body. HTMLHeadElement будет создан неявно, хотя у нас нет токена head, и он будет добавлен в дерево.
Теперь перейдем к режиму В теге head, а затем – к режиму После тега head. Токен body обрабатывается повторно, создается и вставляется HTMLBodyElement, а режим переключается на "in body".
Токены символов строки "Hello world" получены. Первый символ приведет к созданию и вставке узла Text, а остальные символы будут добавлены в этот узел.
Получение конечного токена body приведет к переходу в режим "after body". Теперь мы получим конечный тег html, который переведет нас в режим "после после body". Получение токена конца файла завершит синтаксический анализ.
Действия после завершения синтаксического анализа
На этом этапе браузер помечает документ как интерактивный и начинает анализировать скрипты, которые находятся в режиме отложенного выполнения (они должны выполняться после анализа документа). После этого документу будет присвоен статус "Завершено" и будет запущено событие "Загрузка".
Полные алгоритмы токенизации и построения дерева приведены в спецификации HTML5.
Допустимые ошибки браузеров
На HTML-странице никогда не возникает ошибка "Недопустимый синтаксис". Браузеры исправляют недопустимый контент и продолжают работу.
Рассмотрим следующий HTML-код:
<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
Я нарушил множество правил (mytag – нестандартный тег, неправильное вложение элементов p и div и т. д.), но браузер все равно показывает страницу правильно и не выдает ошибок. Поэтому большая часть кода синтаксического анализатора исправляет ошибки автора HTML.
Обработка ошибок в браузерах достаточно согласована, но, как ни странно, она не была частью спецификаций HTML. Как и закладки или кнопки "Назад" и "Вперед", это просто функция, которая со временем появилась в браузерах. Существуют известные недопустимые конструкции HTML, которые повторяются на многих сайтах, и браузеры пытаются исправить их так, чтобы они соответствовали другим браузерам.
Некоторые из этих требований определены в спецификации HTML5. (WebKit хорошо резюмирует это в комментарии в начале класса синтаксического анализатора HTML.)
Синтаксический анализатор обрабатывает токенизированные входные данные и строит дерево документа. Если документ правильно оформлен, его синтаксический анализ не представляет сложности.
К сожалению, нам приходится обрабатывать множество HTML-документов, которые не соответствуют стандартам, поэтому синтаксический анализатор должен быть устойчив к ошибкам.
Мы должны позаботиться по крайней мере о следующих условиях ошибки:
- Добавляемый элемент явно запрещен внутри внешнего тега. В этом случае нужно закрыть все теги до того, который запрещает элемент, и добавить его после.
- Мы не можем добавить элемент напрямую. Возможно, автор документа забыл добавить промежуточный тег или этот тег необязателен. Это может произойти со следующими тегами: HTML HEAD BODY TBODY TR TD LI (возможно, я что-то забыл).
- Мы хотим добавить блочный элемент внутрь встроенного. Закройте все встроенные элементы до следующего блочного элемента более высокого уровня.
- Если это не поможет, закрывайте элементы, пока не появится возможность добавить элемент, или игнорируйте тег.
Рассмотрим несколько примеров допустимых ошибок WebKit:
</br> вместо <br>
На некоторых сайтах вместо <br> используется </br>. Для совместимости с IE и Firefox WebKit обрабатывает это как <br>.
Код:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
Обратите внимание, что обработка ошибок выполняется внутри системы и не видна пользователю.
Бесхозный стол
Бесхозная таблица – это таблица, которая находится внутри другой таблицы, но не внутри ячейки таблицы.
Пример:
<table>
<table>
<tr><td>inner table</td></tr>
</table>
<tr><td>outer table</td></tr>
</table>
WebKit изменит иерархию на две таблицы одного уровня:
<table>
<tr><td>outer table</td></tr>
</table>
<table>
<tr><td>inner table</td></tr>
</table>
Код:
if (m_inStrayTableContent && localName == tableTag)
popBlock(tableTag);
WebKit использует стек для содержимого текущего элемента: он извлекает внутреннюю таблицу из стека внешней таблицы. Теперь таблицы будут находиться на одном уровне.
Вложенные элементы формы
Если пользователь помещает форму внутрь другой формы, вторая форма игнорируется.
Код:
if (!m_currentFormElement) {
m_currentFormElement = new HTMLFormElement(formTag, m_document);
}
Слишком глубокая иерархия тегов.
Комментарий говорит сам за себя.
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{
unsigned i = 0;
for (HTMLStackElem* curr = m_blockStack;
i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
curr = curr->next, i++) { }
return i != cMaxRedundantTagDepth;
}
Неправильно размещенные конечные теги html или body
Комментарий говорит сам за себя.
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;
Поэтому веб-мастерам следует быть осторожными и писать корректный HTML-код, если они не хотят, чтобы их сайт стал примером в фрагменте кода WebKit, отвечающем за обработку ошибок.
Синтаксический анализ CSS
Помните концепции синтаксического анализа из введения? В отличие от HTML, CSS – это контекстно-свободная грамматика, которую можно анализировать с помощью синтаксических анализаторов, описанных во введении. Спецификация CSS определяет лексическую и синтаксическую грамматику CSS.
Рассмотрим несколько примеров.
Лексическая грамматика (словарь) определяется регулярными выражениями для каждого токена:
comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num [0-9]+|[0-9]*"."[0-9]+
nonascii [\200-\377]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z0-9-]|{nonascii}|{escape}
name {nmchar}+
ident {nmstart}{nmchar}*
"ident" – это сокращение от identifier (идентификатор), например название класса. "name" – идентификатор элемента (на который ссылается символ #).
Синтаксис описан в форме Бэкуса – Наура.
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
selector
: simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
;
simple_selector
: element_name [ HASH | class | attrib | pseudo ]*
| [ HASH | class | attrib | pseudo ]+
;
class
: '.' IDENT
;
element_name
: IDENT | '*'
;
attrib
: '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
[ IDENT | STRING ] S* ] ']'
;
pseudo
: ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
;
Значение.
Набор правил имеет следующую структуру:
div.error, a.error {
color:red;
font-weight:bold;
}
div.error и a.error – селекторы. В фигурных скобках указаны правила, которые применяются этим набором правил.
Эта структура формально определена в следующем определении:
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
Это означает, что набор правил – это селектор или несколько селекторов, разделенных запятой и пробелами (S означает пробел). Набор правил содержит фигурные скобки, внутри которых находится объявление или несколько объявлений, разделенных точкой с запятой. "declaration" и "selector" будут определены в следующих определениях BNF.
Синтаксический анализатор WebKit CSS
WebKit использует генераторы синтаксических анализаторов Flex и Bison, чтобы автоматически создавать синтаксические анализаторы из файлов грамматики CSS. Как вы помните из введения в синтаксический анализатор, Bison создает восходящий синтаксический анализатор сдвига-свертки. В Firefox используется нисходящий синтаксический анализатор, написанный вручную. В обоих случаях каждый файл CSS анализируется в объект StyleSheet. Каждый объект содержит правила CSS. Объекты правил CSS содержат объекты селекторов и объявлений, а также другие объекты, соответствующие грамматике CSS.
Порядок обработки скриптов и таблиц стилей
Скрипты
Модель интернета синхронна. Авторы ожидают, что скрипты будут проанализированы и выполнены сразу же, как только синтаксический анализатор дойдет до тега <script>.
Синтаксический анализ документа приостанавливается до выполнения скрипта.
Если скрипт внешний, то сначала нужно получить ресурс из сети. Это также делается синхронно, и синтаксический анализ приостанавливается до тех пор, пока ресурс не будет получен.
Эта модель использовалась много лет и также указана в спецификациях HTML4 и HTML5.
Авторы могут добавить в скрипт атрибут defer, чтобы он не останавливал синтаксический анализ документа и выполнялся после его завершения. В HTML5 можно пометить скрипт как асинхронный, чтобы он обрабатывался и выполнялся в отдельном потоке.
Спекулятивный синтаксический анализ
Такая оптимизация выполняется и в WebKit, и в Firefox. Пока выполняются скрипты, другой поток анализирует остальную часть документа и определяет, какие ещё ресурсы нужно загрузить из сети, а затем загружает их. Это позволяет загружать ресурсы по параллельным подключениям и повышает общую скорость. Примечание. Спекулятивный синтаксический анализатор обрабатывает только ссылки на внешние ресурсы, такие как внешние скрипты, таблицы стилей и изображения. Он не изменяет дерево DOM – это делает основной синтаксический анализатор.
Таблицы стилей
Таблицы стилей, напротив, имеют другую модель. Поскольку таблицы стилей не меняют дерево DOM, нет причин ждать их загрузки и останавливать синтаксический анализ документа. Однако существует проблема, связанная со скриптами, которые запрашивают информацию о стиле на этапе синтаксического анализа документа. Если стиль ещё не загружен и не проанализирован, скрипт получит неверные ответы, что, по-видимому, и вызвало множество проблем. Это кажется пограничным случаем, но на самом деле встречается довольно часто. Firefox блокирует все скрипты, если таблица стилей ещё загружается и обрабатывается. WebKit блокирует скрипты, только когда они пытаются получить доступ к определенным свойствам стилей, на которые могут повлиять незагруженные таблицы стилей.
Создание дерева визуализации
Пока строится дерево DOM, браузер строит другое дерево – дерево рендеринга. В этом дереве визуальные элементы представлены в том порядке, в котором они будут показываться. Это визуальное представление документа. Это дерево позволяет отрисовывать контент в правильном порядке.
В Firefox элементы дерева визуализации называются фреймами. В WebKit используется термин "объект отрисовки".
Рендерер знает, как разместить и нарисовать себя и свои дочерние элементы.
Класс RenderObject в WebKit, базовый класс для средств визуализации, имеет следующее определение:
class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node; //the DOM node
RenderStyle* style; // the computed style
RenderLayer* containgLayer; //the containing z-index layer
}
Каждый отрисовщик представляет собой прямоугольную область, которая обычно соответствует блоку CSS узла, как описано в спецификации CSS2. Он включает геометрическую информацию, такую как ширина, высота и положение.
Тип блока зависит от значения display атрибута style, относящегося к узлу (см. раздел Вычисление стиля). Ниже приведен код WebKit, который определяет, какой тип визуализатора должен быть создан для узла DOM в соответствии с атрибутом display:
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch (style->display()) {
case NONE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}
Также учитывается тип элемента: например, у элементов управления формой и таблиц есть специальные рамки.
В WebKit, если элемент хочет создать специальный рендерер, он переопределит метод createRenderer().
Объекты отрисовки указывают на объекты стиля, которые содержат негеометрическую информацию.
Связь дерева рендеринга с деревом DOM
Отрисовщики соответствуют элементам DOM, но не один к одному. Невизуальные элементы DOM не будут вставлены в дерево рендеринга. Пример – элемент head. Элементы, для которых задано значение display: none, не будут включены в дерево, а элементы со значением visibility: hidden – будут.
Элементы DOM, которые соответствуют нескольким визуальным объектам. Обычно это элементы со сложной структурой, которые нельзя описать одним прямоугольником. Например, у элемента select есть три средства визуализации: для области отображения, для раскрывающегося списка и для кнопки. Если текст разбит на несколько строк из-за недостаточной ширины, новые строки будут добавлены как дополнительные средства визуализации.
Другой пример использования нескольких обработчиков – поврежденный HTML-код. Согласно спецификации CSS, строчный элемент должен содержать только блочные или только строчные элементы. В случае смешанного контента будут созданы анонимные средства визуализации блоков, чтобы обернуть встроенные элементы.
Некоторые объекты рендеринга соответствуют узлу DOM, но не в том же месте дерева. Плавающие и абсолютно позиционированные элементы находятся вне потока, размещаются в другой части дерева и сопоставляются с реальным фреймом. Вместо них вы увидите пустые кадры.
Процесс создания дерева
В Firefox презентация регистрируется как прослушиватель обновлений DOM.
Презентация делегирует создание фрейма объекту FrameConstructor, а конструктор разрешает стиль (см. Вычисление стиля) и создает фрейм.
В WebKit процесс разрешения стиля и создания средства визуализации называется "прикреплением". У каждого узла DOM есть метод attach. Прикрепление выполняется синхронно. При вставке узла в дерево DOM вызывается метод attach нового узла.
Обработка тегов html и body приводит к построению корня дерева рендеринга.
Корневой объект рендеринга соответствует тому, что в спецификации CSS называется содержащим блоком: самому верхнему блоку, который содержит все остальные блоки. Его размеры соответствуют области просмотра, то есть размерам области отображения окна браузера.
В Firefox это ViewPortFrame, а в WebKit – RenderView.
Это объект рендеринга, на который указывает документ.
Остальная часть дерева создается как вставка узлов DOM.
Подробную информацию вы найдете в спецификации CSS2, посвященной модели обработки.
Вычисление стиля
Для построения дерева рендеринга необходимо рассчитать визуальные свойства каждого объекта рендеринга. Для этого вычисляются свойства стиля каждого элемента.
Стиль включает таблицы стилей различного происхождения, встроенные элементы стиля и визуальные свойства в HTML (например, свойство bgcolor).Последнее преобразуется в соответствующие свойства стиля CSS.
Таблицы стилей могут быть предоставлены браузером по умолчанию, автором страницы или пользователем браузера (браузеры позволяют задавать предпочитаемые стили). Например, в Firefox для этого нужно поместить таблицу стилей в папку Firefox Profile.
Вычисление стилей сопряжено с некоторыми трудностями:
- Данные о стилях – это очень большая конструкция, содержащая многочисленные свойства стилей, что может вызвать проблемы с памятью.
Поиск соответствующих правил для каждого элемента может вызвать проблемы с производительностью, если он не оптимизирован. Проверка всего списка правил для каждого элемента – это сложная задача. Селекторы могут иметь сложную структуру, из-за которой процесс сопоставления может начаться по кажущемуся многообещающим пути, который оказывается бесполезным, и приходится пробовать другой путь.
Например, этот составной селектор:
div div div div{ ... }Правила применяются к элементу
<div>, который является потомком трех элементов div. Предположим, вы хотите проверить, применяется ли правило к определенному элементу<div>. Вы выбираете определенный путь вверх по дереву для проверки. Возможно, вам придется пройти по дереву узлов вверх, чтобы узнать, что есть только два div и правило не применяется. Вам нужно будет попробовать другие пути в дереве.Применение правил включает довольно сложные каскадные правила, определяющие иерархию правил.
Давайте посмотрим, как браузеры справляются с этими проблемами:
Доступ к данным о стиле
Узлы WebKit ссылаются на объекты стилей (RenderStyle). При определенных условиях эти объекты могут использоваться узлами совместно. Узлы являются дочерними или родственными и:
- Элементы должны находиться в одном состоянии мыши (например, один не может быть в состоянии :hover, а другой – нет).
- Ни у одного элемента не должно быть идентификатора.
- Названия тегов должны совпадать.
- Атрибуты класса должны совпадать.
- Набор сопоставленных атрибутов должен быть идентичным.
- Статусы ссылок должны совпадать.
- Состояния фокуса должны совпадать.
- На элементы не должны влиять селекторы атрибутов. Это означает, что не должно быть совпадений с селекторами, в которых селектор атрибута используется в любой позиции.
- В элементах не должно быть атрибута встроенного стиля.
- Нельзя использовать селекторы родственных элементов. WebCore просто включает глобальный переключатель, когда обнаруживается любой родственный селектор, и отключает совместное использование стилей для всего документа, когда они присутствуют. Это относится к селектору + и селекторам :first-child и :last-child.
Дерево правил Firefox
В Firefox есть два дополнительных дерева, которые упрощают вычисление стилей: дерево правил и дерево контекста стилей. В WebKit также есть объекты стилей, но они не хранятся в дереве, подобном дереву контекста стилей, а только узел DOM указывает на соответствующий стиль.
Контексты стилей содержат конечные значения. Значения вычисляются путем применения всех соответствующих правил в правильном порядке и выполнения манипуляций, которые преобразуют их из логических в конкретные значения. Например, если логическое значение – это процент от экрана, оно будет рассчитано и преобразовано в абсолютные единицы. Идея с деревом правил действительно умная. Это позволяет передавать значения между узлами, чтобы не вычислять их заново. Это также экономит место.
Все сопоставленные правила хранятся в виде дерева. Нижние узлы в пути имеют более высокий приоритет. В дереве содержатся все пути для найденных совпадений с правилами. Правила сохраняются в ленивом режиме. Дерево не рассчитывается в начале для каждого узла, но всякий раз, когда необходимо вычислить стиль узла, вычисленные пути добавляются в дерево.
Идея состоит в том, чтобы рассматривать пути дерева как слова в лексиконе. Предположим, что мы уже вычислили следующее дерево правил:
Предположим, что нам нужно сопоставить правила для другого элемента в дереве контента и выяснить, какие правила совпадают (в правильном порядке) с B-E-I. Этот путь уже есть в дереве, поскольку мы уже вычислили путь A-B-E-I-L. Теперь у нас будет меньше работы.
Давайте посмотрим, как дерево помогает нам экономить время.
Разделение на структуры
Контексты стилей разделены на структуры. Эти структуры содержат информацию о стиле для определенной категории, например о границе или цвете. Все свойства в структуре либо наследуются, либо не наследуются. Унаследованные свойства – это свойства, которые наследуются от родительского элемента, если они не определены в дочернем. Ненаследуемые свойства (также называемые свойствами сброса) используют значения по умолчанию, если они не определены.
Дерево помогает нам, кэшируя целые структуры (содержащие вычисленные конечные значения) в дереве. Если нижний узел не предоставил определение для структуры, можно использовать кешированную структуру в верхнем узле.
Вычисление контекстов стиля с помощью дерева правил
При вычислении контекста стиля для определенного элемента мы сначала вычисляем путь в дереве правил или используем существующий. Затем мы начинаем применять правила в пути, чтобы заполнить структуры в нашем новом контексте стиля. Мы начинаем с нижнего узла пути – узла с наивысшим приоритетом (обычно это наиболее конкретный селектор) и проходим по дереву вверх, пока наша структура не будет заполнена. Если в узле правила нет спецификации для структуры, мы можем значительно оптимизировать процесс: мы поднимаемся по дереву, пока не найдем узел, который полностью ее определяет, и указываем на него. Это лучшая оптимизация – вся структура является общей. Это позволяет экономить память и ресурсы процессора.
Если мы находим частичные определения, мы поднимаемся по дереву, пока структура не будет заполнена.
Если мы не нашли определений для нашей структуры, то, если структура является "унаследованным" типом, мы указываем на структуру нашего родителя в дереве контекста. В этом случае нам также удалось поделиться структурами. Если это структура сброса, будут использоваться значения по умолчанию.
Если самый конкретный узел добавляет значения, то нам нужно выполнить дополнительные вычисления, чтобы преобразовать его в фактические значения. Затем мы кешируем результат в узле дерева, чтобы его могли использовать дочерние элементы.
Если у элемента есть родственный элемент, который указывает на тот же узел дерева, то они могут использовать один и тот же контекст стиля.
Рассмотрим пример. Предположим, у нас есть следующий HTML-код:
<html>
<body>
<div class="err" id="div1">
<p>
this is a <span class="big"> big error </span>
this is also a
<span class="big"> very big error</span> error
</p>
</div>
<div class="err" id="div2">another error</div>
</body>
</html>
и следующие правила:
div {margin: 5px; color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}
Для простоты предположим, что нам нужно заполнить только две структуры: color и margin. Структура цвета содержит только один элемент – цвет. Структура полей содержит четыре стороны.
Полученное дерево правил будет выглядеть следующим образом (узлы помечены названием узла: номером правила, на которое они указывают):
Дерево контекста будет выглядеть следующим образом (название узла: узел правила, на который он указывает):
Предположим, что мы анализируем HTML-код и доходим до второго тега <div>. Нам нужно создать контекст стиля для этого узла и заполнить его структуры стиля.
Мы сопоставим правила и обнаружим, что для <div> подходят правила 1, 2 и 6.
Это означает, что в дереве уже есть путь, который может использовать наш элемент, и нам нужно только добавить к нему ещё один узел для правила 6 (узел F в дереве правил).
Мы создадим контекст стиля и поместим его в дерево контекста. Новый контекст стиля будет указывать на узел F в дереве правил.
Теперь нужно заполнить структуры стилей. Начнем с заполнения структуры полей. Поскольку последний узел правила (F) не добавляет структуру полей, мы можем подниматься по дереву, пока не найдем кэшированную структуру, вычисленную при предыдущей вставке узла, и использовать ее. Мы найдем его в узле B, который является самым верхним узлом, в котором указаны правила наценки.
У нас есть определение для структуры цвета, поэтому мы не можем использовать кэшированную структуру. Поскольку у цвета только один атрибут, нам не нужно подниматься по дереву, чтобы заполнить другие атрибуты. Мы вычислим конечное значение (преобразуем строку в RGB и т. д.) и кешируем вычисленную структуру на этом узле.
Работать со вторым элементом <span> ещё проще. Мы сопоставим правила и придем к выводу, что оно указывает на правило G, как и предыдущий диапазон.
Поскольку у нас есть дочерние элементы, указывающие на один и тот же узел, мы можем использовать весь контекст стиля и просто указать на контекст предыдущего диапазона.
Для структур, содержащих правила, унаследованные от родительского элемента, кеширование выполняется в дереве контекста (свойство color фактически наследуется, но Firefox обрабатывает его как сброс и кеширует в дереве правил).
Например, если мы добавили правила для шрифтов в абзаце:
p {font-family: Verdana; font size: 10px; font-weight: bold}
Затем элемент абзаца, который является дочерним элементом div в дереве контекста, мог использовать ту же структуру шрифта, что и его родительский элемент. Это происходит, если для абзаца не заданы правила шрифта.
В WebKit, где нет дерева правил, сопоставленные объявления просматриваются четыре раза. Сначала применяются неважные свойства с высоким приоритетом (свойства, которые должны применяться первыми, поскольку от них зависят другие, например display), затем важные свойства с высоким приоритетом, затем неважные свойства с обычным приоритетом и, наконец, важные правила с обычным приоритетом. Это означает, что свойства, которые появляются несколько раз, будут разрешены в соответствии с правильным каскадным порядком. Последний побеждает.
Итак, совместное использование объектов стиля (полностью или некоторых структур внутри них) решает проблемы 1 и 3. Дерево правил Firefox также помогает применять свойства в правильном порядке.
Манипулирование правилами для легкого совпадения
Правила стилей могут поступать из нескольких источников:
- Правила CSS, которые могут находиться как во внешних таблицах стилей, так и в элементах style.
css p {color: blue} - Атрибуты встроенных стилей, например
html <p style="color: blue" />. - Визуальные HTML-атрибуты (сопоставляются с соответствующими правилами стиля).
html <p bgcolor="blue" />Последние два легко сопоставить с элементом, поскольку он владеет атрибутами стиля, а HTML-атрибуты можно сопоставить, используя элемент в качестве ключа.
Как отмечалось в проблеме 2, сопоставление правил CSS может быть сложным. Чтобы решить эту проблему, правила можно изменить.
После синтаксического анализа таблицы стилей правила добавляются в одну из нескольких хеш-таблиц в соответствии с селектором. Есть карты по идентификатору, по названию класса, по названию тега и общая карта для всего, что не подходит под эти категории. Если селектор является идентификатором, правило будет добавлено в карту идентификаторов, если классом – в карту классов и т. д.
Это упрощает сопоставление правил. Нет необходимости просматривать каждое объявление: мы можем извлечь соответствующие правила для элемента из карт. Такая оптимизация позволяет исключить более 95% правил, поэтому их не нужно учитывать в процессе сопоставления(4.1).
Рассмотрим следующий пример:
p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}
Первое правило будет вставлено в карту классов. Второй – в сопоставление идентификаторов, а третий – в сопоставление тегов.
Для следующего фрагмента HTML:
<p class="error">an error occurred</p>
<div id=" messageDiv">this is a message</div>
Сначала мы попытаемся найти правила для элемента p. В карте классов будет ключ error, под которым находится правило для p.error. Элемент div будет иметь соответствующие правила в карте идентификаторов (ключом является идентификатор) и карте тегов. Остается только определить, какие из извлеченных правил действительно соответствуют запросу.
Например, если правило для div было следующим:
table div {margin: 5px}
Он все равно будет извлечен из карты тегов, поскольку ключ – это крайний правый селектор, но он не будет соответствовать нашему элементу div, у которого нет предка table.
Это делают и WebKit, и Firefox.
Порядок каскадирования таблиц стилей
Объект стиля имеет свойства, соответствующие каждому визуальному атрибуту (все атрибуты CSS, но более общие). Если свойство не определено ни одним из сопоставленных правил, то некоторые свойства могут быть унаследованы от объекта стиля родительского элемента. Для других свойств используются значения по умолчанию.
Проблема возникает, когда определений больше одного. В этом случае на помощь приходит каскадный порядок.
Объявление свойства стиля может появляться в нескольких таблицах стилей и несколько раз в одной таблице стилей. Это означает, что порядок применения правил очень важен. Это называется каскадным порядком. Согласно спецификации CSS2, каскадный порядок (от низкого к высокому) выглядит следующим образом:
- Декларации браузера
- Обычные декларации пользователя
- Создание обычных деклараций
- Важные заявления автора
- Важные декларации пользователя
Объявления браузера наименее важны, и пользователь переопределяет автора, только если объявление было помечено как важное. Декларации с одинаковым порядком будут отсортированы по специфичности, а затем по порядку, в котором они указаны. Визуальные атрибуты HTML преобразуются в соответствующие объявления CSS . Они обрабатываются как правила автора с низким приоритетом.
Специфика
Специфичность селектора определяется спецификацией CSS2 следующим образом:
- 1, если объявление относится к атрибуту style, а не к правилу с селектором; в противном случае – 0 (= a).
- количество атрибутов ID в селекторе (= b);
- количество других атрибутов и псевдоклассов в селекторе (= c);
- Подсчитайте количество названий элементов и псевдоэлементов в селекторе (= d).
Объединение четырех чисел a-b-c-d (в системе счисления с большим основанием) дает специфичность.
Основание системы счисления, которое вам нужно использовать, определяется наибольшим количеством в одной из категорий.
Например, если a=14, можно использовать шестнадцатеричную систему счисления. В маловероятном случае, когда a=17, вам понадобится 17-значное числовое основание. Последняя ситуация может произойти с селектором, подобным этому: html body div div p… (17 тегов в селекторе… маловероятно).
Ниже приведено несколько примеров.
* {} /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
li {} /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
li:first-line {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul li {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul ol+li {} /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
h1 + *[rel=up]{} /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
ul ol li.red {} /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
li.red.level {} /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
#x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
style="" /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */
Сортировка правил
После этого правила сортируются в соответствии с каскадными правилами.
WebKit использует пузырьковую сортировку для небольших списков и сортировку слиянием для больших.
WebKit реализует сортировку, переопределяя оператор > для правил:
static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
int spec1 = r1.selector()->specificity();
int spec2 = r2.selector()->specificity();
return (spec1 == spec2) ? r1.position() > r2.position() : spec1 > spec2;
}
Постепенный процесс
WebKit использует флаг, который отмечает, загружены ли все таблицы стилей верхнего уровня (включая @imports). Если стиль не загружен полностью при прикреплении, используются заполнители, которые помечаются в документе и пересчитываются после загрузки таблиц стилей.
Макет
Когда средство визуализации создано и добавлено в дерево, у него нет положения и размера. Вычисление этих значений называется макетом или перекомпоновкой.
В HTML используется модель макета на основе потока, то есть в большинстве случаев геометрию можно рассчитать за один проход. Элементы, расположенные ниже по потоку, обычно не влияют на геометрию элементов, расположенных выше по потоку, поэтому макет может обрабатываться слева направо и сверху вниз по документу. Однако есть исключения. Например, для таблиц HTML может потребоваться несколько проходов.
Система координат определяется относительно корневого фрейма. Используются координаты верхнего и левого краев.
Макет – это рекурсивный процесс. Оно начинается с корневого средства визуализации, которое соответствует элементу <html> в HTML-документе. Макет рекурсивно проходит через часть или всю иерархию фреймов, вычисляя геометрическую информацию для каждого средства визуализации, которому она требуется.
Позиция корневого рендерера – 0,0, а его размеры – это область просмотра, то есть видимая часть окна браузера.
У всех средств визуализации есть метод layout или reflow, и каждое из них вызывает метод layout своих дочерних элементов, которым требуется макет.
Система грязных битов
Чтобы не выполнять полную компоновку при каждом небольшом изменении, браузеры используют систему "грязных битов". Измененный или добавленный рендерер помечает себя и свои дочерние элементы как "грязные", то есть требующие макета.
Есть два флага: dirty (грязный) и children are dirty (дочерние элементы грязные). Второй означает, что хотя сам рендерер может быть в порядке, у него есть по крайней мере один дочерний элемент, которому нужен макет.
Глобальный и инкрементный макет
Макет может быть запущен на всем дереве рендеринга – это "глобальный" макет. Это может произойти по следующим причинам:
- Глобальное изменение стиля, которое влияет на все средства визуализации, например изменение размера шрифта.
- изменение размера экрана;
Макет может быть инкрементным, будут размещены только грязные средства визуализации (это может привести к некоторым повреждениям, которые потребуют дополнительных макетов).
Постепенный макет запускается (асинхронно), когда средства визуализации загрязнены. Например, когда в дерево рендеринга добавляются новые средства визуализации после того, как из сети поступил дополнительный контент и был добавлен в дерево DOM.
Асинхронный и синхронный макеты
Постепенная компоновка выполняется асинхронно. Firefox ставит в очередь команды перекомпоновки для инкрементных макетов, а планировщик запускает пакетное выполнение этих команд. В WebKit также есть таймер, который выполняет инкрементную компоновку: дерево обходится, и компонуются "грязные" средства визуализации.
Скрипты, запрашивающие информацию о стиле, например offsetHeight, могут синхронно запускать инкрементный макет.
Глобальный макет обычно запускается синхронно.
Иногда макет запускается как обратный вызов после первоначального макета, поскольку некоторые атрибуты, например положение прокрутки, изменились.
Оптимизация
Когда макет активируется изменением размера или положения отрисовщика(но не размера), размеры отрисовщиков берутся из кеша, а не рассчитываются заново…
В некоторых случаях изменяется только поддерево, и макет не начинается с корня. Это может произойти в тех случаях, когда изменение является локальным и не влияет на его окружение, например, текст, вставленный в текстовые поля (в противном случае каждое нажатие клавиши запускало бы макет, начиная с корня).
Процесс макетирования
Обычно макет имеет следующую структуру:
- Родительский объект визуализации определяет собственную ширину.
- Родитель подходит к детям и:
- Разместите дочерний рендерер (задайте его координаты x и y).
- При необходимости вызывает дочерний макет (если он недействителен, если мы находимся в глобальном макете или по какой-либо другой причине), который вычисляет высоту дочернего элемента.
- Родительский элемент использует суммарную высоту дочерних элементов, а также высоту полей и отступов, чтобы задать собственную высоту. Она будет использоваться родительским элементом родительского визуализатора.
- Устанавливает для бита изменения значение false.
Firefox использует объект "state" (nsHTMLReflowState) в качестве параметра для макета (называемого "reflow"). В частности, состояние включает ширину родительского элемента.
Выходные данные макета Firefox – это объект "metrics" (nsHTMLReflowMetrics). Он будет содержать высоту, рассчитанную средством визуализации.
Расчет ширины
Ширина средства визуализации рассчитывается с учетом ширины блока контейнера, свойства width стиля средства визуализации, полей и границ.
Например, ширина следующего элемента div:
<div style="width: 30%"/>
WebKit рассчитает это следующим образом(метод calcWidth класса RenderBox):
- Ширина контейнера – это максимальное значение из availableWidth и 0. В этом случае availableWidth – это contentWidth, которое рассчитывается следующим образом:
clientWidth() - paddingLeft() - paddingRight()
clientWidth и clientHeight представляют внутреннюю часть объекта, исключая границу и полосу прокрутки.
Ширина элемента задается атрибутом стиля width. Оно будет рассчитано как абсолютное значение путем вычисления процента от ширины контейнера.
Горизонтальные границы и поля добавлены.
До сих пор мы рассчитывали предпочтительную ширину. Теперь будут рассчитаны минимальная и максимальная ширина.
Если предпочтительная ширина больше максимальной, используется максимальная ширина. Если она меньше минимальной ширины (наименьшей неразрывной единицы), используется минимальная ширина.
Значения кешируются на случай, если потребуется макет, но ширина не меняется.
Перенос строк
Когда средство визуализации в середине макета решает, что ему нужно прерваться, оно останавливается и сообщает родительскому элементу макета, что его нужно прервать. Родительский элемент создает дополнительные средства визуализации и вызывает для них макет.
Картина
На этапе отрисовки выполняется обход дерева визуализации и вызывается метод paint() визуализатора, чтобы показать контент на экране. Для отрисовки используется компонент инфраструктуры пользовательского интерфейса.
Глобальные и дополнительные
Как и макет, отрисовка может быть глобальной (отрисовывается все дерево) или инкрементной. При инкрементной отрисовке некоторые средства визуализации изменяются таким образом, что это не влияет на все дерево. Измененный обработчик аннулирует свой прямоугольник на экране. В результате ОС воспринимает ее как "грязную область" и генерирует событие "рисование". ОС делает это разумно и объединяет несколько регионов в один. В Chrome это сложнее, потому что рендерер находится в другом процессе, а не в основном. Chrome в некоторой степени имитирует поведение ОС. Презентация прослушивает эти события и делегирует сообщение корню рендеринга. Дерево просматривается до тех пор, пока не будет достигнут нужный обработчик. Оно будет перерисовано (и обычно его дочерние элементы тоже).
Порядок отрисовки
Порядок отрисовки определяется в CSS2. Это порядок, в котором элементы размещаются в контекстах наложения. Этот порядок влияет на отрисовку, поскольку стеки отрисовываются от заднего плана к переднему. Порядок наложения отрисовщика блоков:
- цвет фона
- фоновое изображение
- граница
- дети
- outline
Список отображения Firefox
Firefox проходит по дереву рендеринга и создает список отображения для отрисованного прямоугольника. Он содержит средства визуализации, относящиеся к прямоугольнику, в правильном порядке рисования (фоны средств визуализации, затем границы и т. д.).
Таким образом, дерево нужно обойти только один раз для перерисовки вместо нескольких раз – отрисовка всех фонов, затем всех изображений, затем всех границ и т. д.
Firefox оптимизирует процесс, не добавляя элементы, которые будут скрыты, например элементы, полностью находящиеся под другими непрозрачными элементами.
Хранилище прямоугольников WebKit
Перед перерисовкой WebKit сохраняет старый прямоугольник в виде растрового изображения. Затем он отрисовывает только разницу между новым и старым прямоугольниками.
Динамические изменения
Браузеры стараются выполнять минимально возможные действия в ответ на изменения. Поэтому изменение цвета элемента приведет только к его перерисовке. Изменение положения элемента приведет к изменению макета и перерисовке элемента, его дочерних элементов и, возможно, элементов того же уровня. Добавление узла DOM приведет к изменению макета и перерисовке узла. Значительные изменения, например увеличение размера шрифта элемента html, приведут к аннулированию кеша, изменению макета и перерисовке всего дерева.
Потоки механизма отрисовки
Движок рендеринга однопоточный. Почти все, кроме сетевых операций, происходит в одном потоке. В Firefox и Safari это основной поток браузера. В Chrome это основной поток процесса вкладки.
Сетевые операции могут выполняться в нескольких параллельных потоках. Количество параллельных подключений ограничено (обычно от 2 до 6).
Цикл событий
Основной поток браузера – это цикл событий. Это бесконечный цикл, который поддерживает процесс. Он ожидает событий (например, макета и отрисовки) и обрабатывает их. Вот код Firefox для основного цикла событий:
while (!mExiting)
NS_ProcessNextEvent(thread);
Визуальная модель CSS2
Холст
Согласно спецификации CSS2, термин "холст" описывает "пространство, в котором отображается структура форматирования", то есть где браузер отрисовывает контент.
Холст бесконечен в каждом измерении пространства, но браузеры выбирают начальную ширину на основе размеров области просмотра.
Согласно www.w3.org/TR/CSS2/zindex.html, холст прозрачен, если он находится внутри другого холста, и имеет цвет, определенный браузером, если это не так.
Блочная модель CSS
Блочная модель CSS описывает прямоугольные блоки, которые генерируются для элементов в дереве документа и размещаются в соответствии с моделью визуального форматирования.
Каждый блок имеет область содержимого (например, текст, изображение и т. д.) и необязательные окружающие области заполнения, границы и поля.
Каждый узел генерирует от 0 до n таких блоков.
У всех элементов есть свойство display, которое определяет тип создаваемого блока.
Примеры:
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
По умолчанию используется значение inline, но в таблице стилей браузера могут быть заданы другие значения по умолчанию. Например, по умолчанию для элемента div используется блочный тип отображения.
Пример таблицы стилей по умолчанию можно найти на странице www.w3.org/TR/CSS2/sample.html.
Схема позиционирования
Существует три схемы:
- Обычное: объект размещается в соответствии с его положением в документе. Это означает, что его место в дереве рендеринга аналогично его месту в дереве DOM и расположено в соответствии с типом и размерами его блока.
- Float: объект сначала размещается как обычный поток, а затем перемещается как можно дальше влево или вправо.
- Absolute: объект помещается в дерево рендеринга в другое место, чем в дереве DOM.
Схема позиционирования задается свойством position и атрибутом float.
- static и relative вызывают нормальный поток
- absolute и fixed – абсолютное позиционирование;
При статическом позиционировании позиция не задается и используется позиционирование по умолчанию. В других схемах автор указывает позицию: вверху, внизу, слева, справа.
Макет блока определяется следующими факторами:
- Тип поля
- Размеры коробки
- Схема позиционирования
- Внешняя информация, например размер изображения и размер экрана.
Типы полей
Блочный бокс: формирует блок – имеет собственный прямоугольник в окне браузера.
Встроенный блок: не имеет собственного блока, но находится внутри содержащего блока.
Блоки располагаются вертикально один за другим. Встроенные элементы форматируются по горизонтали.
Строчные блоки помещаются внутрь строк или "строчных блоков". Высота строк не меньше высоты самого высокого поля, но может быть больше, если поля выровнены по базовой линии (то есть нижняя часть элемента выровнена по точке другого поля, отличной от нижней). Если ширины контейнера недостаточно, встроенные элементы будут размещены на нескольких строках. Обычно это происходит в абзаце.
Позиционирование
Член семьи
Относительное позиционирование – элемент размещается как обычно, а затем сдвигается на заданное расстояние.
Числа с плавающей запятой
Плавающий блок сдвигается влево или вправо от строки. Интересная особенность заключается в том, что другие блоки обтекают его. HTML-код:
<p>
<img style="float: right" src="images/image.gif" width="100" height="100">
Lorem ipsum dolor sit amet, consectetuer...
</p>
Пример:
Абсолютное и фиксированное позиционирование
Макет определяется точно, независимо от обычного потока. Элемент не участвует в обычном потоке. Размеры указываются относительно контейнера. В случае фиксированного позиционирования контейнером является область просмотра.
Многослойное представление
Это определяется свойством z-index CSS. Это третье измерение поля – его положение по оси Z.
Блоки делятся на стопки (называемые контекстами наложения). В каждом наборе сначала отрисовываются задние элементы, а затем передние, которые находятся ближе к пользователю. Если элементы перекрывают друг друга, то верхний элемент будет скрывать нижний.
Стеки упорядочиваются по свойству z-index. Блоки со свойством z-index формируют локальный стек. Область просмотра содержит внешний стек.
Пример:
<style type="text/css">
div {
position: absolute;
left: 2in;
top: 2in;
}
</style>
<p>
<div
style="z-index: 3;background-color:red; width: 1in; height: 1in; ">
</div>
<div
style="z-index: 1;background-color:green;width: 2in; height: 2in;">
</div>
</p>
В результате этого:
Хотя красный div предшествует зеленому в разметке и был бы нарисован раньше в обычном потоке, свойство z-index выше, поэтому оно находится дальше в стеке, удерживаемом корневым полем.
Ресурсы
Архитектура браузера
- Grosskurth, Alan. Эталонная архитектура для веб-браузеров (PDF)
- Gupta, Vineet. Как работают браузеры. Часть 1. Архитектура
Синтаксический анализ
- Ахо, Сети, Ульман, "Компиляторы: принципы, технологии и инструменты" (также известная как "Книга дракона"), Addison-Wesley, 1986 г.
- Рик Джеллифф (Rick Jelliffe). The Bold and the Beautiful: two new drafts for HTML 5.
Firefox
- L. Дэвид Барон (David Baron), Faster HTML and CSS: Layout Engine Internals for Web Developers (Ускорение HTML и CSS: внутренние механизмы движка макета для веб-разработчиков).
- L. Дэвид Барон (David Baron), Faster HTML and CSS: Layout Engine Internals for веб-разработчиков (видеозапись технической презентации Google)
- L. Дэвид Барон (David Baron), Mozilla's Layout Engine
- L. Дэвид Барон, Mozilla Style System Documentation
- Крис Уотерсон, Notes on HTML Reflow
- Обзор Gecko, Крис Уотерсон (Chris Waterson)
- Александр Ларссон, Жизнь HTTP-запроса HTML
WebKit
- Дэвид Хайатт, Implementing CSS(part 1)
- Дэвид Хаятт, An Overview of WebCore
- Дэвид Хайатт (David Hyatt), WebCore Rendering
- Дэвид Хайатт (David Hyatt), Проблема FOUC
Спецификации W3C
Инструкции по сборке браузеров
Переводы
Эта страница была переведена на японский язык дважды:
- How Browsers Work - Behind the Scenes of Modern Web Browsers (ja) by @kosei
- ブラウザってどうやって動いてるの?(モダンWEBブラウザシーンの裏側 by @ikeike443 and @kiyoto01.
Вы можете посмотреть переводы на корейский и турецкий языки, размещенные на внешних ресурсах.
Спасибо!