Как работает браузер или что происходит за кулисами современных браузеров.

Перевод статьи:  How Browsers Work: Behind the scenes of modern web browsers.
Авторы:  Tali Garsiel и Paul Irish.

Иногда на своем блоге я размещаю переводы отдельных статей и документов, которые были опубликованы не один год тому назад и даже те, перевод которых уже сделан. Причина тому их важность и неизменная актуальность. Я к тому же имею привычку всегда знакомиться с документами, стандартами и публикациями в их оригинальном варианте. Это одна из таких статей, оригинал которой был опубликован в августе 2011 года и поэтому все используемые в ней ссылки на даты и привязки ко времени следует воспринимать соответственно. Статья действительно до сих пор актуальна и очень познавательна.

Предисловие.

Представленное здесь исчерпывающее руководство для начинающих по внутреннему устройству и функционалу движков WebKit и Gecko является результатом важнейшего исследования, проведенного израильским разработчиком Тали Гарсиель. На протяжении нескольких лет она просматривала все публикуемые данные касательно внутреннего устройства браузеров (взгляните на список источников) и провела уйму времени, анализируя исходный код браузеров. Вот, что она написала:

Во времена 90%-го господства IE мы вынуждены были воспринимать браузер как «черный ящик», но теперь, когда браузеры с открытым исходным кодом составляют более половины от всех используемых, самое время заглянуть «под капот машины» и узнать, что находится внутри веб-браузера. Итак, что же скрывается за миллионами строк C++ кода….

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

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

Пол Айриш — департамент по связям с разработчиками Chrome

Введение.

Веб-браузеры относятся к наиболее широко распространенному программному обеспечению. В этом пособии я расскажу вам, что делается за «кулисами» этих приложений. Вы узнаете, что происходит после ввода google.com в адресную строку браузера до того как вы увидите в его окне главную страницу Google.

  1. Введение
    1. Браузеры, о которых пойдет речь
    2. Основное функциональное назначение браузера
    3. Структура высокого уровня браузера.
  2. Механизм рендеринга
    1. Модули рендеринга
    2. Ход главного процесса
    3. Примеры основного потока
  3. Парсинг — общие понятия
    1. Грамматика
    2. Комбинация парсера и лексера
    3. Трансляция
    4. Пример парсинга
    5. Формальные определения словаря и синтаксиса
    6. Типы синтаксических анализаторов
    7. Автоматическая генерация парсеров
  4. HTML парсер
    1. Определение HTML грамматики
    2. Не бесконтекстная грамматика
    3. HTML DTD
    4. DOM
    5. Алгоритм парсинга
    6. Алгоритм лексического анализа
    7. Алгоритм построения дерева
    8. Что происходит по окончании процесса парсинга
    9. Толерантность браузеров к ошибкам
  5. Парсинг CSS кода.
    1. Webkit CSS парсер
  6. Порядок обработки скриптов и таблиц стилей
    1. Скрипты
    2. Спекулятивный парсинг
    3. Таблицы стилей
  7. Создание дерева визуализации
    1. Взаимосвязь дерева визуализации и DOM дерева
    2. Процесс формирования дерева
    3. Вычисление стилей
      1. Совместное использование стилевых данных
      2. Дерево правил в Firefox
        1. Разделение стилевых данных на конструкции
        2. Расчет контекстов стилей с помощью дерева правил
      3. Манипулирование правилами для упрощения процесса их сопоставления
      4. Корректное применение правил согласно модели каскадности
        1. Каскадность таблиц стилей
        2. Специфичность
        3. Сортировка правил
    4. Последовательность процесса построения дерева визуализации
  8. Компоновка
    1. Система «битов очистки»
    2. Глобальная и инкрементная компоновка
    3. Асинхронная и синхронная компоновка
    4. Оптимизация компоновки
    5. Процесс компоновки
    6. Расчет ширины
    7. Переносы строк
  9. Отрисовка
    1. Глобальная и инкрементная отрисовка
    2. Последовательность отрисовки
    3. Список отображения в Firefox
    4. Хранение отрисованных прямоугольных областей в WebKit
  10. Динамичные изменения
  11. Потоки движка визуализации
    1. Цикл событий
  12. Визуальная модель форматирования стандарта CSS2
    1. Канва
    2. Модель CSS бокса
    3. Схемы позиционирования
    4. Типы боксов
    5. Позиционирование
      1. Относительное
      2. Плавающие элементы
      3. Абсолютное и фиксированное
    6. Слои представления документа
  13. Использованная литература

Браузеры о которых пойдет речь.

На сегодняшний день существует пять основных наиболее часто используемых на стационарных компьютерах браузеров: Internet Explorer, Firefox, Safari, Chrome и Opera. Что касается мобильных устройств, то здесь доминируют: браузер Android, iPhone, Opera Mini и Opera Mobile, UC Browser, браузеры Nokia S40/S60 и Chrome, каждый из которых за исключением Opera функционируют на движке WebKit. Примеры кода, которые я буду использовать в статье, взяты из браузеров с открытым кодом — Firefox, Chrome и Safari (последний лишь с частично открытым кодом). Согласно статистике по использованию браузеров, приведенной компанией StatCounter, на данный момент (июнь 2013) процент использования Firefox, Safari и Chrome вместе взятых на стационарных ПК составляет около 71% от общего количества браузеров для этого сегмента устройств. На мобильных устройствах преобладают: браузер Android, iPhone и Chrome, совокупный процент использования которых составляет 54%.

Основное функциональное назначение браузера.

Прежде всего, браузеры предназначены для представления выбранного вами веб-ресурса, путем его запроса у сервера и отображения в окне браузера. Ресурсом, как правило, является HTML документ, но это может быть изображение, файл PDF или другого формата. Адрес, по которому находится ресурс, определяется по введенному пользователем URI.

То, как браузер интерпретирует и отображает HTML файлы, определено в HTML и CSS спецификациях. Эти спецификации ведутся такой организацией как W3C (Word Wide Web Консорциум), которая занимается стандартизацией Web пространства. На протяжении многих лет браузеры лишь частично поддерживали спецификации и занимались разработкой собственных расширений. Это привело к тому, что веб-разработчики столкнулись с серьезными проблемами, связанными с совместимостью. На сегодняшний день большинство браузеров в большей или меньшей степени все-таки отвечают требованиям спецификаций.

Предоставляемые различными браузерами пользовательские интерфейсы в большей своей части очень схожи между собой. К наиболее распространенным элементам пользовательского интерфейса относятся:

  • Адресная строка для ввода URI.
  • Кнопки перехода "Вперед" и "Назад".
  • Возможности применения закладок.
  • Кнопки "Refresh" и "Stop", используемые для обновления и прекращения загрузки текущей страницы.
  • Кнопка "Home", после нажатия которой вы попадаете на определенную вами ранее домашнюю страницу.

Как ни странно, но ни в одной существующей формальной спецификации не затрагивается вопрос используемого в браузерах пользовательского интерфейса, он просто основан на лучших практических приемах, отточенных годами опыта и формировался в результате подражания друг друга браузерами. Новая HTML5 спецификация также не определяет какие элементы должен включать UI, однако она перечисляет некоторые наиболее распространенные из них. К их числу относится адресная строка, строка состояния и панель инструментов. Существуют, конечно же, особенности, характерные исключительно отдельным браузерам, к примеру, менеджер загрузок в Firefox.

Структура высокого уровня браузера.

Основные компоненты браузера (1.1):

  1. Пользовательский интерфейс — включает в себя адресную строку, навигационные кнопки "Вперед"/"Назад", меню закладок и т.д. К нему относится любая часть окна браузера за исключением его области, в которой отображается запрашиваемая вами страница.
  2. Движок браузера — отвечает за взаимодействие между UI и блоком визуализации.
  3. Модуль рендеринга — отвечает за отображение запрашиваемого контента. К примеру, если запрошенный пользователем контент представлен HTML документом, то данный компонент браузера производит синтаксический разбор HTML и CSS кода, а получаемый в результате этого анализа контент отображает на экране устройства.
  4. Сетевой механизм — используется для сетевых операций, таких как HTTP запросы. В нем предусмотрен кроссплатформенный интерфейс и низкоуровневые реализации для каждой из поддерживаемых платформ.
  5. Внутренний UI — применяется для отрисовки основных графических элементов и виджетов типа комбо-боксов и окон. Он предоставляет типичный, независимый от платформы интерфейс, а на низком уровне использует методы, предусмотренные операционной системой.
  6. JavaScript интерпретатор. Необходим для парсинга и выполнения JavaScript кода.
  7. Хранилище данных. Представляет собой персистентный уровень. Браузеру необходимо сохранять различного рода данные на жестком диске компьютера, такие, к примеру, как cookie. В последней HTML спецификации (HTML5) вводится новая технология "Web database", которая представляет собой полнофункциональный, хотя и облегченный вариант базы данных в самом браузере.

Основные компоненты браузера.

Изображение 1: Основные компоненты браузера.

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

Механизм рендеринга.

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

Базовый модуль рендеринга браузера способен отображать документы HTML и XML формата, а также изображения. Возможно представление и других форматов, но для этого необходим плагин или модуль расширения браузера. К примеру, для показа PDF документа необходим плагин PDF viewer. Однако в этом параграфе мы сфокусируемся на основном варианте использования: отображении HTML документов и изображений, отформатированных с помощью CSS.

Модули рендеринга.

Взятые мной в качестве примеров браузеры — Firefox, Chrome и Safari созданы на основе двух движков визуализации. Firefox использует Gecko — модуль собственной разработки компании Mozilla. А в Chrome, как и в Safari используется Webkit.

Webkit является движком рендеринга с отрытым кодом, который изначально разрабатывался для применения на платформе Linux и впоследствии был модифицирован компанией Apple с целью поддержки платформ Mac и Windows. Более подробную информацию по этому вопросу вы можете получить на официальном сайте Webkit.org.

Ход главного процесса.

Функционирование модуля рендеринга начинается с получения контента запрашиваемого документа, для чего задействуется компонент сетевого уровня. Контент передается по сетевому соединению порционно, как правило, пакетами размером 8 Кбайт.

Далее основной поток работы движка визуализации выглядит следующим образом:

Основной процесс движка браузера.

Изображение 2: Основной процесс движка рендеринга.

Модуль визуализации начнет парсинг полученного HTML документа и на основе используемых в нем тегов создаст DOM узлы дерева, называемого "content tree" («дерево контента»). Далее парсингу подвергаются данные стилей: как внешние CSS файлы, так и встроенные таблицы стилей и соответствующие атрибуты элементов. Стилевая информация совместно с предоставляемыми HTML разметкой визуальными инструкциями будут использованы при построении другого дерева — "render tree" («дерево визуализации»).

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

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

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

Примеры главного процесса.

Ход основного процесса движка Webkit.

Изображение 3: Ход основного процесса движка Webkit

Основной процесс движка Gecko.

Изображение 4: Основной процесс движка рендеринга Gecko компании Mozilla (см. список ресурсов 3.6).

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

В Gecko дерево визуально форматируемых элементов называется "frame tree" («дерево фреймов»), каждый элемент которого является фреймом. Webkit для этого использует термин "render tree" («дерево визуализации»), состоящее из "render objects" («объектов визуализации»). Процесс размещения элементов в Webkit называется "layout" («компоновка»), который в Gecko именуется "reflow" («реорганизация» потока). Процесс связывания DOM узлов с соответствующей им визуальной информацией, в ходе которого создается дерево визуализации, в Webkit называется "attachment" («привязка» стилей). Незначительное не семантическое расхождение также и в том, что в Gecko между HTML разметкой и деревом DOM документа предусмотрен дополнительный уровень, называемый "content sink" («сток контента»), который является неким конвейером по производству DOM элементов. Далее мы поговорим о каждом компоненте в отдельности.

*Примечание переводчика: Хочу обратить ваше внимание на термин «поток» или в оригинале "flow", применяемый в технической литературе. Дело в том, что HTML стандарт использует поточную модель компоновки документа, которая в большинстве случаев позволяет рассчитать геометрические данные компонентов страницы за один проход структуры документа. При таком подходе элементы, находящиеся ниже в потоке, как правило (хотя далеко не всегда), не влияют на геометрические данные элементов, которые расположены выше в потоке документа. То есть процесс формирования разметки страницы происходит поточным образом, начиная от верхнего левого угла, направо и далее построчно вниз. В противоположность этому методу можно назвать блочную модель компоновки XUL, согласно которой в ходе расчета геометрических характеристик определенного элемента, в первую очередь принимаются во внимание все ограничения и преимущества элементов, составляющих его окружение, лишь после этого рассчитывается геометрия самого элемента.

В Gecko процесс компоновки элементов страницы именуется "reflow"«реорганизация потока». Данный движок предусматривает пять разновидностей такой реорганизации, каждая из которых применяется в различных ситуациях. Но это уже тема для отдельной публикации. Вернемся к статье.

Парсинг — общие понятия.

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

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

Вот, например, парсинг такого выражения как 2 + 3 — 1 мог бы в результате дать следующее дерево:

Синтаксическое дерево математического выражения.

Изображение 5: Синтаксическое дерево математического выражения.

Грамматика.

Парсинг основывается на правилах синтаксиса, которым удовлетворяет анализируемый документ — язык или формат, с помощью которого он создан. Любой формат, который подлежит парсингу, должен иметь детерминированную грамматику, состоящую из необходимых синтаксических правил и словаря. В данном случае имеется в виду бесконтекстная грамматика. Естественные языки относятся к другому типу языков и по этой причине не поддаются парсингу с помощью стандартных методов синтаксического анализа.

Комбинация парсера и лексера.

Процесс парсинга может быт разделен на два подпроцесса — лексического и синтаксического анализа.

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

Синтаксический анализ контролирует выполнение синтаксических правил языка.

Анализаторы обычно разделяют возложенную на них работу между двумя компонентами — лексером (иногда называют токенайзером), который отвечает за разбивку входящего потока на допустимые лексемы и парсером, который отвечает за создание синтаксического дерева, путем анализа структуры документа с помощью синтаксических правил языка. Лексер обладает способностью распознавания посторонних символов, таких как пробелы, переводы строк и им подобные, которые необходимы лишь для повышения читабельности исходного кода, поэтому отфильтровываются в ходе лексического анализа.

От исходного документа до синтаксического дерева.

Изображение 6: От исходного документа до синтаксического дерева.

Парсинг является циклическим процессом, в ходе которого парсер обычно запрашивает у лексера новую лексему, после чего делает попытку применения к ней одного из синтаксических правил. Если правило будет подобрано, то в синтаксическое дерево добавляется соответствующий обрабатываемой лексеме узел, после чего парсер выполняет запрос новой лексемы.

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

Трансляция.

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

Ход компиляции.

Изображение 7: Ход компиляции.

Пример парсинга.

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

Словарь:

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

Синтаксис:

  1. Элементами синтаксиса определяемого нами языка являются: выражения, термы и операторы.
  2. Синтаксис нашего языка допускает использование любого количества выражений.
  3. Выражение определено в формате: терм, следующий за ним оператор, после которого находится еще один терм.
  4. Оператором является одни из литералов: плюс (+) или минус ().
  5. В качестве терма может выступать числовой литерал, либо выражение.

Теперь давайте проведем анализ входных данных, то есть математического выражения из нашего примера 2 + 3 — 1.

Первая удовлетворяющая одному из наших синтаксических правил подстрока — это 2 и согласно правилу номер 5 она является термом. Второе соответствие синтаксису: 2 + 3, которое отвечает требованиям третьего правила, и в этом случае мы уже имеем дело с выражением, то есть терм, за которым следует операнд и еще один терм. Следующее соответствие будет достигнуто лишь при полном прочтении входных данных. Имеем 2 + 3 — 1, определяемое как выражение, поскольку если 2 + 3 выражение, которое согласно пятому правилу может быть термом, а за ним следует оператор и еще один замыкающий терм 1. Если взять, допустим, математическое выражение 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 (в ходе процесса идентификации выражения проверяется соответствие и другим правилам, но порядок такого анализа начинается с правила самого высокого уровня или другими словами с самого сложного правила, включающего максимальное число лексем).

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

Стек Ввод
2 + 3 — 1
терм (2) + 3 — 1
терм оператор (2 +) 3 — 1
выражение (2 + 3) — 1
выражение оператор (2 + 3 —) 1
выражение (2 + 3 — 1)   

Этот тип восходящего парсера работает в режиме «сдвиг — свёртка». Такой термин объясняется тем, что анализатор обрабатывает входящий поток, сдвигаясь вправо (вообразите себе курсор, который начиная с крайне левого начального положения входной строки, перемещается в правом направлении к ее концу), постепенно сворачивает его, заменяя уже обработанные фрагменты соответствующими синтаксическими правилами.

Автоматическая генерация парсеров.

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

Движок Webkit использует два хорошо известных генератора парсеров: Flex для создания лексера и Bison, создающий парсер (вы, вероятно, уже сталкивались с этими средствами под другими именами — LexYacc). В качестве исходных данных для Flex используется файл, содержащий определенные в терминах регулярных выражений лексемы. Генератор Bison использует представленные в BNF формате синтаксические правила языка.

HTML парсер.

Задачей HTML парсера является анализ HTML разметки с последующим ее преобразованием в синтаксическое дерево.

Определение HTML грамматики.

Словарь и синтаксис языка HTML определен в спецификациях, разработанных W3C консорциумом.

Не бесконтекстная грамматика.

Как уже говорилось ранее, в предыдущем, вводном разделе о парсинге, грамматический синтаксис может быть формально определен с использованием BNF формата.

К сожалению, принципы, которыми руководствуются все традиционные парсеры, не совместимы с HTML (но я все-таки не зря коснулась этого вопроса, поскольку дальше мы будем рассматривать основы парсинга CSS и JavaScript кода). Язык HTML не может быть вот так просто определен с помощью бесконтекстной грамматики, которая необходима для нормальной работы парсеров.

И все же существует официальный формат определения HTML — DTD (Document Type Definition), но это совсем не бесконтекстная грамматика.

На первый взгляд это может показаться странным, ведь HTML довольно схож с XML, для которого существует множество парсеров. Более того, предусмотрена даже XML-разновидность HTML языка — XHTML, так что же делает их такими разными при очевидном сходстве?

Различие заключается в том, что в HTML используется более «щадящий» подход, позволяющий вам опускать определенные теги, которые впоследствии добавляются неявным образом (т.е. автоматически). Иногда допускается пропуск как открывающих, так и закрывающих тегов, а также другие вольности. Одним словом, мы имеем дело со «снисходительным» синтаксисом, который является противоположностью требовательного и «непоколебимого» XML синтаксиса.

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

HTML DTD

HTML определен DTD форматом, который используется для описания языков SGML семейства. Данный формат включает определения всех допустимых элементов, их атрибутов и иерархии. Как уже было указано ранее, HTML DTD не использует бесконтекстную грамматику.

Существует несколько вариаций DTD формата, каждый из которых определяет соответствующий ему режим работы браузера. Строгий (strict) режим обеспечивает строгое соблюдение требований спецификаций, а остальные режимы предусматривают поддержку разметки, которая использовалась для создания документов в прошлом. Целью такого поведения браузеров является обратная совместимость с ранее созданным контеном. Документ, определяющий строгий формат, который используется на данный момент, находится здесь: www.w3.org/TR/html4/strict.dtd.

DOM

Как известно из предыдущего раздела, результирующим деревом является «синтаксическое дерево». Оно состоит из DOM элементов и узлов, представляющих атрибуты этих элементов. DOM — это аббревиатура определения модели объектного представления HTML документа — Document Object Model (Объектная модель документа). Эта модель является интерфейсом, позволяющим получить доступ к HTML элементам извне таким средствам как JavaScript.
Корень DOM дерева представлен объектом Document.

Структура DOM практически с точностью один к одному соответствует разметке. Вот, к примеру, этот фрагмент:

<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>

Будет транслирован в следующее DOM дерево:

DOM дерево разметки из примера.

Изображение 8: 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 языка не позволяет проанализировать созданные на его основе документы с помощью обычных нисходящих и восходящих парсеров.

И вот по каким причинам:

  1. Лояльность самого языка, допускающего наличие ошибок.
  2. Предусматриваемый браузерами дополнительный уровень толерантности к ошибкам, который объясняется довольно частыми случаями использования некорректного HTML кода.
  3. Реентерабельность процесса парсинга. Обычно исходные данные остаются неизменными на протяжении всего процесса парсинга, но в HTML встречаются теги скриптов, которые могут содержать инструкции типа document.write, приводящие к генерации дополнительных лексем, то есть процесс самого синтаксического анализа может стать причиной модифицирования входных данных.

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

Алгоритм парсинга подробно описан в HTML5 спецификации. Он состоит из двух этапов — процесса разбивки входного потока на лексемы и процесса формирования дерева.

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

Tокенайзер идентифицирует определенную лексему, передает ее блоку построения дерева, а затем принимает следующие входные символы, из которых будет сформирована другая лексема. Этот процесс продолжается до окончания входного потока.

Ход процесса HTML парсинга.

Изображение взято из HTML5 спецификации.

Изображение 9: Ход процесса HTML парсинга.

Алгоритм лексического анализа.

Результат работы этого алгоритма — HTML лексема. Алгоритм реализован в виде конечного автомата. При каждом шаге его выполнения принимается один или более символов входного потока с последующим изменением состояний автомата в зависимости от результатов анализа полученных символов. То есть если после анализа полученного символа или ряда символов токенайзер генерирует лексическую продукцию (лексему), то происходит переход в следующее состояние автомата. Решение, принимаемое анализатором в ходе этого процесса, зависит от текущего состояния автомата и состояния, в котором находится блок формирования дерева. Это значит, что получение токенайзером одного и того же символа может привести к переходу в различные допустимые алгоритмом состояния, все зависит от текущего состояния системы. Сам алгоритм очень сложен чтобы рассматривать его полностью, поэтому давайте ограничимся разбором простейшего примера, который поможет нам понять сам принцип.

Базовый пример лексического анализа следующего фрагмента HTML кода:

<html>
<body>
Hello world
</body>
</html>

Исходным состоянием алгоритма является "Ввод данных". Когда анализатор встречает символ <, то это приводит к смене состояния автомата на "Открытие тега". Если в этом состоянии происходит поступление символов a-z, что означает начало формирования лексемы открывающего тега, то состояние автомата меняется на "Ввод имени тега", которое длится до поступления на вход анализатора символа >. Находясь в этом состоянии все принимаемые символы, последовательно добавляются к имени новой лексемы. В нашем случае получаем лексему html.

По достижении символа закрытия тега >, текущая лексема считается сформированной, и состояние автомата возвращается в его начальное состояние "Ввод данных". Следующий тег нашей разметки <body> будет обработан аналогичным образом в той же последовательности. То есть на данный момент имеем два сгенерированных токенайзером тега html и body, и находимся в состоянии "Ввод данных". Поступление буквенного символа Н (начало строки "Hello word") инициирует формирование и генерацию символьной лексемы. Так происходит до момента приема анализатором символа < (начало закрывающего тега </body>). То есть для каждого символа строки "Hello word" будет создана символьная лексема.

Сейчас автомат снова переходит в состояние "Открытие тега". Но теперь, поскольку следующим входным символом является /, то это приводит к началу процесса формирования лексемы закрывающего тега и переходу в состояние "Ввод имени тега". И опять же, как в предыдущем цикле алгоритма, мы находимся в этом состоянии до ввода символа закрытия тега >. После этого токенайзер генерирует лексему нового тега и возвращается в состояние "Ввод данных". Дальнейшая обработка оставшихся исходных данных, вплоть до тега </html> включительно, производится аналогичным образом.

Лексический анализ используемых в примере данных.

Изображение 10: Лексический анализ используемых в примере данных.

Алгоритм построения дерева.

При запуске парсера создается объект документа Document. Инициированное DOM дерево, корнем которого является объект Document, в ходе второго этапа парсинга — формирования дерева, будет модифицироваться путем добавления в него новых элементов. Каждый генерируемый токенайзером узел будет соответствующим образом обработан блоком построения дерева. Спецификацией строго определено, какой DOM элемент соответствует каждой лексеме, а значит для всех генерируемых анализатором лексем, будут созданы необходимые элементы. Помимо добавления такого элемента в DOM дерево, он также вносится в стек открытых элементов. Этот стек необходим для контроля корректной вложенности тегов, а также отслеживания незакрытых и несовпадающих тегов. Алгоритм формирования дерева тоже описан с использованием модели конечного автомата. Состояния, которого в этом случае именуются режимами вставки.

*Примечание переводчика: Для автомата предусмотрены несколько режимов работы, которые вкратце описаны ниже. Когда автомат находится в определенном режиме, применяются соответствующие этому режиму правила обработки любых поступающих на его вход лексем, с возможной генерацией и вставкой требуемых DOM элементов в дерево. Собственно отсюда и название термина «режимы вставки» (состояния автомата).

Давайте рассмотрим ход процесса построения дерева для нашего примера:

<html>
<body>
Hello world
</body>
</html>

Входными данными для этапа формирования дерева является поток генерируемых токенайзером лексем. Начальный режим вставки алгоритма — "initial mode" ("исходный"). При получении лексемы html автомат меняет свой режим на "before html" ("перед html") и снова возвращается к поступившей лексеме, после анализа которой создается DOM узел HTMLElement, добавляемый к корневому объекту Document.

По окончании выполнения описанных выше операций, режим вставки (состояние) автомата переключается в "before head" ("перед head"). Далее на вход поступает лексема body. Однако в этом состоянии алгоритм ожидает поступления лексемы head и если этого не происходит, то он автоматически генерирует узел HTMLHeadElement, несмотря на то, что в нашем примере соответствующий ему элемент не предусмотрен. Далее этот узел добавляется в структуру DOM дерева.

Теперь анализатор переключается в режим "in head" ("в пределах head") и поскольку явный элемент <head> в нашей разметке отсутствует, то анализатор сразу же переходит в режим "after head" ("после head"). Парсер снова возвращается к ожидающей обработки лексеме body и создает соответствующий ей DOM узел — HTMLBodyElement, помещая его в дерево. Теперь автомат переключается в состояние "in body" ("в пределах body").

Очередь доходит до символьных лексем, полученных в результате лексического анализа фразы "Hello word". При поступлении первой из них создается и вставляется в дерево текстовый узел Text, в состав которого добавляются все следующие за первой символьные лексемы данной фразы.

Получение закрывающего тега элемента body приводит к переходу автомата в режим вставки "after body" ("после body"). А по окончании обработки закрывающего тега </html> алгоритм переходит в позицию "after after body" ("после закрытия body"). Следующее поступление маркера окончания входного потока (EOF — End Of File) является условием завершения процесса парсинга документа.

*Примечание переводчика: Название последнего режима вставки автомата кажется несколько странным, поскольку для этого можно было выбрать более простой и понятный термин — "after html" ("после html"). Все дело в том, что в алгоритме построения DOM дерева помимо режима "in body" предусмотрен режим "in frameset" ("в пределах frameset"). В это состояние автомат переключается если в режиме "after head" поступает лексема фрейма frameset, что, как вы понимаете, приводит к образованию фреймовой структуры. В этом случае алгоритмом предусмотрено несколько иное поведение и правила обработки входных лексем, чем для режима "in body". Поэтому перед закрывающим тегом </html> и после него может быть два различных режима вставки (один при парсинге обычного документа, а другой для фреймов), которые также имеют некоторые различия. Последний режим вставки автомата нужен, собственно, лишь для фиксации алгоритма в состоянии предполагаемого окончания процесса формирования DOM дерева документа. Здесь допускаются лишь лексемы комментариев и ожидаемый маркер EOF.

Построение дерева используемого в примере фрагмента разметки.

Изображение 11: Построение дерева используемого в примере фрагмента разметки.

Что происходит по окончании процесса парсинга.

На этом этапе браузер устанавливает «интерактивный» статус документа и начинает анализ скриптов, находящихся в отложенном состоянии, то есть которые должны быть выполнены по окончании парсинга документа (*помеченные атрибутом deferred). Затем статус документа меняется на «завершённый» и возбуждается событие load.

Полная версия алгоритмов лексического анализа и формирования DOM дерева представлена в HTML5 спецификации.

Толерантность браузеров к ошибкам.

Вы никогда не дождетесь от браузера вывода сообщения о синтаксической ошибке. При обнаружении любой ошибки в процессе обработки контента он всегда найдет выход из ситуации и продолжит его обработку.

Вот подходящий пример некорректного HTML кода:

<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>

В таком маленьком фрагменте я умудрилась нарушить массу правил (тег <mytag> не предусмотрен стандартом, нарушена вложенность элементов p и div и т.д.), но, тем не менее, браузер все равно отображает его корректно без намека на единую ошибку. Это происходит потому, что львиная доля кода парсера нацелена на исправление ошибок HTML разметки, допускаемых авторами.

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

HTML5 спецификацией уже определены некоторые требования, касающиеся ошибок парсинга, которые прекрасно резюмированы в начале одного из представленных разработчиками Webkit пособий по HTML парсингу.

Парсер производит синтаксический анализ разбитого на лексические продукции входного потока, в результате которого создается дерево документа. Если документ синтаксически корректен, то процесс парсинга протекает без отклонений от его стандартного сценария.

Но, к сожалению, нам приходится обрабатывать множество HTML документов, которые содержат синтаксические ошибки, что вынуждает парсер быть толерантным к такого рода ошибкам.

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

  1. Новый элемент добавляется в место, находящееся в рамках открытого ранее в документе тега, который однозначно не допускает наличие такого элемента. В этом случае мы должны закрыть все открытые теги, вплоть до запрещающего внешнего тега включительно и лишь затем добавлять новый элемент.
  2. Нам запрещено вставлять необходимые элементы напрямую. Может случиться так, что человек, который создавал документ, просто забыл вставить некоторый промежуточный тег (или же этот промежуточный тег необязателен). Подобные случаи могут касаться тегов таких элементов как HTML, HEAD, BODY, TBODY, TR, TD и LI (Я ничего не забыла?).
  3. Нам нужно вставить блочный элемент внутрь строчного элемента. Для этого мы закрываем все строчные элементы вплоть до предшествующего блочного элемента, который находится на уровень выше по иерархии дерева документа.
  4. Если все вышеуказанные варианты неприменимы, то необходимо поочередно закрывать все предшествующие элементы до тех пор, пока мы не сможем вставить данный элемент. В противном случае тег элемента должен быть проигнорирован.

Давайте рассмотрим некоторые случаи толерантной обработки ошибок движком Webkit:

использование тега </br> вместо <br>
На отдельных сайтах в целях совместимости со старыми версиями таких браузеров как IE и Firefox вместо полагаемого тега <br> используется его самозакрывающийся вариант </br>, который не рекомендован к использованию спецификацией. Для обработки такой ситуации в движке Webkit предусмотрен следующий фрагмент кода:

if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
блуждающая таблица
Если в рамках области контента таблицы находится другая таблица, но при этом она не включена в ячейку внешней, родительской таблицы, тогда такая внутренняя таблица называется блуждающей. Это выглядит примерно вот так:

<table>
<table>
<tr><td>внутренняя таблица</td></tr>
</table>
<tr><td>внешняя таблица</td></tr>
</table>

Webkit изменит иерархию исходного документа таким образом, что в результате получатся две родственные таблицы одного уровня:

<table>
<tr><td>внутренняя таблица</td></tr>
</table>
<table>
<tr><td>внешняя таблица</td></tr>
</table>

Код, отвечающий за такое преобразование:

if (m_inStrayTableContent && localName == tableTag)
popBlock(tableTag);
вложенные элементы форм
В ситуации, когда пользователь помещает одну форму внутрь другой формы, вторая, внутренняя форма игнорируется. Такое поведение реализуется с помощью следующего кода:

if (!m_currentFormElement) {
m_currentFormElement = new HTMLFormElement(formTag, m_document);
}
слишком глубокая вложенность тегов
Приведенный ниже комментарий говорит сам за себя.

Подходящим примером для такого случая является сайт www.liceo.edu.mx, структура которого содержит невероятно глубокий уровень вложенности элементов, достигнув показателя 1500 тегов, причем все задействованные в такой конструкции теги принадлежат элементу <b>. Мы допускаем вложенность не более 20 однотипных тегов. Все последующие, превышающие этот порог теги будут проигнорированы.

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
Как и в предыдущем случае, краткий комментарий идеально объясняет ситуацию.

Поддержка чрезвычайно ошибочного html кода. Мы никогда не вставляем закрывающий тело документа body тег, так как в некоторых неграмотно составленных веб-страницах данный тег встречается раньше действительного окончания документа. Нам остается лишь надеется на вызов метода end(), с помощью которого мы закрываем все всё ещё открытые в конце документа теги.
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;

Парсинг 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 соответствует идентификатору элемента id (с помощью которого можно ссылаться на соответствующий элемент, добавив к идентификатору символ решетки #).

*Здесь автор статьи с целью упрощения представляемого материала лишь частично раскрыла структуру CSS синтаксиса. На самом деле его лексическая часть состоит из нескольких уровней. Дело в том, что перечисленные в предыдущем сниппете компоненты, по сути не являются лексемами — это лексические структуры более низкого уровня, так называемые макросы, которые представляются наборами элементарных символов, определяемыми с помощью регулярных выражений. А вот ниже приведен неполный список CSS лексем, некоторые из которых могут содержать в своем составе один или более приведенных выше макросов, заключаемых в фигурные скобки.

IDENT {ident}
ATKEYWORD @{ident}
NUMBER {num}
PERCENTAGE {num}%
DIMENSION {num}{ident}
URI url\({w}{string}{w}\)
|url\({w}([!#$%&*-\[\]-~]|{nonascii}|{escape})*{w}\)
UNICODE-RANGE u\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})?
CDO <!––
CDC ––>
: :
{ \{
} \}
COMMENT \/\*[^*]*\*+([^/*][^*]*\*+)*\/
S [ \t\r\n\f]+

То есть макросы — это стандартные предопределенные последовательности символов, которые используются при определении лексем.

Синтаксическая грамматика представлена в BNF формате (*Элементами синтаксиса являются так называемые продукции, которые определяются с помощью лексем).

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;
}
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
declaration
: property S* ':' S* value;
selector
: any+;

*Примечание переводчика: В упомянутых выше BNF определениях используются и другие предусмотренные CSS синтаксисом продукции. Чтобы читатель смог представить себе полную картину, привожу ниже полный список грамматических продукций CSS 2.1 синтаксиса, в его оригинальной форме:

stylesheet : [ CDO | CDC | S | statement ]*;
statement : ruleset | at–rule;
at–rule : ATKEYWORD S* any* [ block | ';' S* ];
block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*;
ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*;
selector : any+;
declaration : property S* ':' S* value;
property : IDENT;
value : [ any | block | ATKEYWORD S* ]+;
any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING
| DELIM | URI | HASH | UNICODE–RANGE | INCLUDES
| DASHMATCH | ':' | FUNCTION S* [any|unused]* ')'
| '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']'
] S*;
unused : block | ATKEYWORD S* | ';' S* | CDO S* | CDC S*;

Webkit CSS парсер.

Движок Webkit с целью автоматического создания синтаксических анализаторов применяет генераторы Flex и Bison, в качестве входных данных используя файлы с необходимой CSS грамматикой. Как вам уже должно быть известно из представленного выше ознакомительного материала касательно парсеров, Bison создает восходящий синтаксический анализатор, работающий в режиме «сдвиг — свёртка». Тогда как у Firefox для этого имеется нисходящий парсер собственной разработки. Однако, несмотря на то, что эти движки используют парсеры различных типов, результат их работы одинаков. В обоих случаях каждый CSS файл транслируется в объект StyleSheet. Любой такой объект содержит CSS правила, которые транслируются в соответствующие им объекты правил, а они в свою очередь содержат объекты селекторов и деклараций, а также другие соответствующие CSS грамматике объекты.

Разбор CSS кода.

Изображение 12: Разбор CSS кода.

Порядок обработки скриптов и таблиц стилей.

Скрипты.

Модель Web пространства подразумевает синхронность выполняемых в ней действий и операций. Авторы при создании своих документов рассчитывают на то, что находящиеся в них скрипты будут обработаны и выполнены непосредственно в тот момент, когда парсер встречает тег <script>. При этом процесс синтаксического анализа приостанавливается до тех пор, пока не будут закончены все действия, связанные с выполнением этих скриптов. Если используется внешний скрипт, то соответствующий ресурс сначала должен быть извлечен из сети, что также происходит синхронно, с остановкой процесса парсинга. Такая схема работы использовалась многие годы и все еще актуальна, так как она определена в четвертой и пятой HTML спецификациях. Для того, чтобы предотвратить прерывание парсинга, авторы могут использовать ключевое слово defer, тогда отмеченный таким образом скрипт
будет выполнен лишь по завершении процесса синтаксического анализа документа. Последняя версия стандарта — HTML5 добавляет новую возможность определения асинхронного скрипта, который будет анализироваться и выполняться отдельным независимым потоком.

Спекулятивный парсинг.

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

Таблицы стилей.

Что касается обработки таблиц стилей, то здесь несколько иной случай. Дело в том, что с концептуальной точки зрения нет никаких причин приостанавливать парсинг документа ожидая загрузки или завершения обработки таблиц стилей, поскольку они не влияют на построение DOM дерева. Однако, иногда в ходе парсинга документа возникают ситуации, когда для корректного выполнения скриптов могут понадобиться данные, касающиеся стилевого оформления страницы. И если на этот момент соответствующие таблицы стилей еще не получены или не обработаны, то в ответ на свои запросы скрипты будут получать неверные данные, что само собой приведет к возникновению множества проблем. Может показаться, что это довольно крайний случай, но на самом деле они возникают довольно часто. Для предотвращения подобных ситуаций Firefox блокирует выполнение всех скриптов если имеется таблица стилей, процесс загрузки и/или парсинга которой еще не закончен. Webkit из тех же соображений блокирует скрипты только в том случае, если в ходе их выполнения имеют место попытки доступа к значениям определенных стилевых свойств, которые могут быть изменены после обработки еще не загруженных таблиц стилей.

Создание дерева визуализации.

Одновременно с построением DOM дерева, браузер формирует еще одно дерево — дерево визуализации. Как уже упоминалось ранее, это дерево состоит из визуальных элементов, которые размещаются в порядке их отображения на экране. То есть это не что иное, как визуальное представление документа. Основным назначением этого дерева является то, что оно позволяет в процессе отрисовки контента выводить его на экран в правильном порядке.

В Firefox элементы дерева визуализации именуются "frames" («фреймами»). Webkit для этого использует термин "renderer" («рендерер» — объект выполняющий визуализацию) или "render object" («объект визуализации»). Каждый рендерер располагает данными, касающимися размещения и отрисовки ассоциируемого с ним элемента, а также его дочерних элементов. Основной класс объектов визуализации движка Webkit, определен следующим образом:

class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node; // DOM узел
RenderStyle* style; // вычисляемое значение стилевого свойства
RenderLayer* containgLayer; // слой отображения объекта (определяется по
// значению свойства z-index)

}

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

Тип отображаемого бокса определяется в зависимости от значения, относящегося к узлу стилевого атрибута display (смотри раздел «Вычисление стилей»). Ниже приведен фрагмент кода движка 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;
}

Взаимосвязь дерева визуализации и DOM дерева.

Хотя для каждого рендерера имеется соответствующий ему элемент DOM дерева, однако не для каждого DOM элемента предусмотрен рендерер. Невидимые DOM элементы не будут включены в дерево визуализации. В качестве примера можно взять элемент <head>. Элементы со свойством display, установленным в none также не будут присутствовать в этом дереве (однако использование свойства visibility:hidden не исключает наличие элемента в дереве визуализации).

Более того, существуют определенные DOM элементы, которые соответствуют нескольким объектам визуализации. Это, как правило, элементы со сложной структурой, которые в отличие от большинства других элементов, не могут быть представлены одной прямоугольной областью. Так, к примеру, элемент select имеет три рендерера — один для окна отображения выбранного значения, другой для выпадающего списка и третий, реализующий кнопку подтверждения выбора. Дополнительные рендереры используются и в том случае, когда ширины вьюпорта недостаточно для размещения одной целой строки текста и она разбивается на несколько строк, для каждой из которых создается дополнительный объект визуализации.

Еще одним примером использования дополнительных рендереров может быть фрагментированный HTML код. Это объясняется тем, что согласно CSS спецификации, внутристрочные элементы должны содержать либо исключительно блочные, либо только строчные элементы. В случае, если такой элемент содержит контент смешанного типа, то это приводит к образованию анонимных блочных рендереров, которые используются для обертывания строчных элементов, приводя таким образом весь контент элемента к одному уровню — блочному.

Места расположения в дереве некоторых объектов визуализации отличаются от мест размещения соответствующих им DOM узлов. Это происходит с плавающими и абсолютно позиционированными элементами, которые изымаются из общего потока документа, что в ходе визуализации документа приводит к тому, что вместо отображения в своей нормальной позиции такие элементы смещаются. По этой причине соответствующие им объекты процесса визуализации размещаются в другом месте дерева и отображаются в реальном фрейме (который устанавливается с учетом позиционирования всех изъятых из основного потока документа элементов). Фреймы, в которых эти элементы должны были находится, если бы остались в основном потоке документа, называются фреймами-заполнителями.

DOM дерево и соответствующее ему дерево визуализации.

Изображение 13: DOM дерево и соответствующее ему дерево визуализации (смотри список ресурсов 3.1). Вьюпорт (Viewport) образует исходный содержащий блок. В движке Webkit он представлен объектом RenderView.

Процесс формирования дерева.

В Firefox процесс формирования отображения документа регистрируется в системе как перехватчик событий обновления DOM структуры документа, а ответственность за создание фреймов возлагает на отдельный модуль FrameConstructor («конструктор фреймов»), который после вычисления значений стилевых свойств (смотри вычисление стилей) создает фрейм.

Webkit для процесса разрешения стилей использует термин "attachment" («привязка» стилей). Для каждого DOM узла предусмотрен метод attach, который вызывается синхронно при добавлении узла в DOM дерево.

*Примечание переводчика: В данном месте статьи позволю себе сделать некоторое отступление от оригинала и внести ясность в понятие фрейма, используемого движком Gecko. Видимо с целью снижения объема статьи и как следствие по причине сжатости представляемого в ней материала, автор оригинала недостаточно углубляется в конструкцию данного движка, поэтому чтобы понять используемую в нем концепцию фрейма мне пришлось самостоятельно проработать этот вопрос. Итак, что же в Gecko подразумевается под фреймом?

Этот движок определяет так называемые ключевые структуры данных ("Key Data Structures"), которые представляют собой узлы взаимодействия основных функциональных модулей. Приведенная ниже диаграмма демонстрирует эти структуры:

Основные структуры данных.

Изображение 13-а: Основные структуры данных.

  • Контентный узел — это не что иное, как узел дерева контента, генерируемый для каждого элемента DOM структуры документа. На основе контентного узла начинается формирование визуальных составляющих контента, то есть последующих ключевых структур.
  • Фрейм можно представить себе в виде прямоугольной рамки, которая содержит все данные геометрического характера (размеры, позиция, смещение). Это так называемый примитив, который в дальнейшем форматируется с учетом результатов разрешения CSS стилей. Одному контентному узлу может соответствовать несколько фреймов. Так происходит, к примеру, при размещении цельного фрагмента текста, представленного одним элементом, на нескольких строках, что приводит к образованию нескольких фреймов для одного элемента, по одному на каждую строку. При этом первичный фрейм — это фрейм, содержащий первую строку текста, а последующие фреймы или продолжения создаются для остальных строк. Однако, определенные контентные узлы могут вовсе не иметь фреймов — (1 — [0..n]), последнее касается контентных узлов, не отображающихся на экране.
  • Контекст стилей — объект, представляющий собой заполнитель для частично вычисленных (разрешенных) данных стилей, получаемых в результате ленивого вычисления, то есть необязательного, а выполняемого по запросу (при возникновении необходимости в таких данных). Каждый объект контекста стилей содержит данные относящиеся к соответствующему HTML элементу, а значит и к ассоциированному с ним фрейму (фреймам). Как видно из диаграммы, каждому фрейму из группы образуемых одним контентным узлом фреймов соответствует один стилевой контекст ([1..n] — 1). Все обращения к объекту контекста стилей фиксируются.
  • Представление — видимый объект, который является структурой, на основе которой выбирается и форматируется конечный примитивный графический элемент пользовательского интерфейса (виджет). Эти объекты представляют собой результат визуального форматирования фреймов, производимого с учетом вычисленных стилей, то есть, отталкиваясь от стилевого контекста. Если, к примеру, элемент не имеет видимых границ, фона и т.д., а его контент перекрывается другим элементом или невидим по другой причине, то хотя для такого элемента и предусмотрен фрейм (или несколько фреймов), но видимый объект не строится. Это видно из показанного на диаграмме количественного отношения — одному фрейму может соответствовать от нуля до одного представления (1 — [0..1]).
  • Виджеты — это примитивные, минимальные окна, используемые при построении пользовательского интерфейса или, другими словами, повторно используемые визуальные компоненты (кнопки, ползунки, флаги, окна, текстовые области и т.д.), из которых строится GUI. Для каждого представления предусмотрен только один виджет, хотя он может вовсе отсутствовать (1 — [0..1]).

Схематически в презентации Gecko это выглядит вот так:

Взаимосвязь ключевых структур данных Gecko.

Изображение 13-б: Взаимосвязь ключевых структур данных Gecko.

Ну что ж, надеюсь, что мне удалось несколько прояснить ситуацию с используемой в Gecko терминологией. Теперь давайте вернемся к оригиналу. Напомню вам, что до того, как я вмешался, речь шла о процессе построения дерева визуализации (Webkit) и дерева фреймов (Gecko).

В результате анализа тегов корневых элементов документа html и body создается корень дерева визуализации. Корневой объект визуализации соответствует тому, что в CSS спецификации именуется исходным содержащим блоком — блочный элемент верхнего уровня, который содержит в себе все остальные блоки документа. В браузере исходный содержащий блок представлен вьюпортом, то есть его размеры соответствуют размерам основного рабочего окна браузера, в котором отображается запрашиваемый пользователем контент. В рассматриваемых здесь движках браузеров для вьюпорта используются различные объекты — в Firefox (Gecko) это ViewPortFrame, а в Webkit< - RenderView. Это объект визуализации, на который ссылается сам документ. Вся остальная часть дерева создается в процессе добавления DOM узлов.

Для более детальной информации по этому вопросу смотрите соответствующий раздел спецификации — 2.3 Модель обработки документа CSS 2.1.

Вычисление стилей.

Создание дерева визуализации сопряжено с процессом разрешения визуальных свойств для каждого объекта визуализации (рендерера). Это реализуется путем вычисления стилевых свойств каждого элемента.

Стилевая информация может черпаться из нескольких источников, таких как: внешние таблицы стилей (различного происхождения и назначения); стили, включаемые непосредственно в документ с помощью элементов <style>; встроенные (контекстные) стили, определяемые в HTML тегах (атрибут style); а также презентационная информация, «вшитая» в теги элементов с помощью специальных атрибутов (типа width, height, bgcolor и им подобных). Последние транслируются в соответствующие им CSS свойства.

Что касается происхождения таблиц стилей, то они могут быть дефолтными таблицами стилей браузера, таблицами стилей, предоставляемыми автором документа, а также пользовательскими, которые устанавливаются пользователем непосредственно в браузере (Браузеры позволяют определять предпочитаемые вами стили визуального оформления просматриваемых документов. В Firefox, к примеру, это можно сделать путем включения своего css файла в каталог Firefox Profile.

Процесс вычисления стилей связан с определенными трудностями:

  1. Информация стилевого оформления документа представлена с помощью очень объемной структуры данных, содержащей множество свойств стилей. Работа с такой структурой в некоторых случаях требует серьезных затрат аппаратных ресурсов, в особенности это касается оперативной памяти.
  2. Если структура CSS кода не оптимизирована, то в процессе поиска соответствующих определенному элементу правил могут появиться проблемы с производительностью. Прохождение полного списка правил при обработке каждого элемента представляет собой достаточно трудоемкую задачу. В некоторых случаях CSS селекторы имеют сложную, иногда обманчивую структуру. Так, в ходе поиска соответствующих элементу правил, в результате первичной оценки селектора может показаться, что он удовлетворяет условиям поиска, однако, далее, после более глубокого анализа это может оказаться не так и тогда поиск нужного селектора будет продолжен.

    Вот один из вариантов сложных селекторов:

    div div div div{

    }
  3. Применение правил производится с учетом их каскадности, которая предусматривает довольно сложную иерархическую структуру.

Давайте посмотрим, как браузеры с этим справляются.

Совместное использование стилевых данных.

В Webkit узлы дерева ссылаются на объекты стилей (RenderStyle). К этим объектам могут обращаться несколько узлов одновременно, однако такие узлы должны соответствовать родственным элементам, которые являются потомками одного родительского элемента либо предка. Кроме того, эти узлы должны отвечать дополнительным условиям:

  1. Соответствующие им элементы должны быть в одном, предусматриваемым взаимодействием с указательным устройством (мышью) состоянии (то есть ни один из элементов не может быть в состоянии :hover, если остальные тоже в нем не находятся).
  2. У всех элементов должен отсутствовать идентификатор (id).
  3. Имена тегов элементов должны быть одинаковы.
  4. Значения атрибутов class элементов должны совпадать.
  5. Используемые элементами атрибуты должны быть преобразованы в идентичные наборы атрибутов соответствующих узлов дерева.
  6. Если это ссылки, то их состояния также должны быть одинаковы (:active, :link или :visited).
  7. Это же касается и состояния фокуса элементов (:focus), оно должно совпадать у всех элементов.
  8. Ни один из элементов не должен соответствовать селектору атрибута, при этом не допускается соответствие любому селектору, в котором используются атрибуты, в каком бы месте селектора они не находились.
  9. Эти элементы не должны содержать контекстных стилей (атрибут style).
  10. При этом использование селекторов родственных элементов одного уровня (дочерних элементов одного родительского элемента) недопустимо. При встрече такого селектора WebCore генерирует глобальный флаг, запрещающий совместное использование стилевой информации в рамках всего документа. Это касается селектора +, а также подобных :first-child и :last-child.

Дерево правил в Firefox.

В Firefox для облегчения процесса вычисления стилей предусмотрены две дополнительных структуры — дерево правил и дерево контекстов стилей. Webkit для этого использует объекты стилей, однако, они хранятся и используются несколько иным способом — каждый DOM узел непосредственно ссылается на соответствующий объект стиля.

Дерево контекстов стилей в Firefox.

Изображение 14: Дерево контекстов стилей в Firefox (2.2).

Контексты стилей содержат результирующие значения, полученные путем вычислений, учитывающих все соответствующие каждому элементу правила в предусмотренном кодом порядке, с последующим приведением логических, указанных автором или используемых по умолчанию величин к фактически применяемым. В качестве примера логического значения можно взять процентную величину, используемую автором при определении свойства, которая взята, допустим, относительно полного размера окна браузера. Тогда фактически применяемое значение свойства, будет преобразовано в абсолютную величину (в пикселах), соответствующую указанному процентному отношению. Сама идея создания дерева правил на самом деле очень умна. За счет такой структуры создается возможность совместного использования стилевой информации несколькими узлами одновременно, без необходимости ее повторного расчета. Это, к тому же, позволяет сэкономить системные ресурсы, так как уменьшается объем используемых, а соответственно и хранящихся в оперативной памяти данных.

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

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

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

Дерево правил.

Каждый узел дерева правил соответствует уже применяемому к одному из элементов документа правилу. Теперь предположим, что после определения правил, которые должны применяться к ещё одному элементу дерева контента (пусть это будут три правила, соответствующие узлам B, E и I дерева правил), мы выясняем, что нужные нам правила применяются к элементу строго в последовательности B-E-I. Проанализировав структуру уже построенного на данный момент дерева правил мы, обнаружили, что нужная нам последовательность правил уже присутствует в дереве в составе ранее сформированной ветки A-B-E-I-L. А это значительно облегчает нам задачу. Однако, как именно дерево правил упрощает процесс вычисления стилей пока не понятно. Ну что ж, давайте разбираться дальше.

Разделение стилевых данных на конструкции.

Содержащаяся в контекстах стилей информация разделена на отдельные составляющие, именуемые конструкциями. Такая организация позволяет упростить хранение и доступ к данным стилей. Каждая конструкция содержит информацию определенной категории. То есть, допустим, свойства, определяющие стили границ элемента (border-width, border-color, border-style, border-collapse или просто border) относятся к одной категории (а значит содержаться в отдельной конструкции), свойства, задающие параметры шрифта (font-family, font-style, font-variant, font-weight, font-size или просто font), к другой, свойства, устанавливающие размеры полей (margin-top, margin-right, margin-bottom и margin-left) к третьей, у свойства цвета шрифта (color) своя категория, и так далее. Все конструкции подразделяются на два вида — те которые состоят из наследуемых свойств и те, которые состоят из ненаследуемых свойств. Наследуемые свойства характерны тем, что если их значения не определены, то они наследуют значения аналогичного свойства родительского элемента. Ненаследуемые свойства отличаются тем, что когда их значения не установлены, то им присваиваются исходные значения, так называемые «сброшенные» ("reset") значения, то есть сброшенные в исходное состояние.

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

Расчет контекстов стилей с помощью дерева правил.

В ходе рассчета контекста стилей для отдельного элемента документа, в первую очередь фиксируется последовательность применяемых к нему правил, которая представляется в виде ветки дерева правил. Если такая последовательность уже была создана ранее для другого элемента, то используется уже существующая ветка дерева. Далее, путем применения присутствующих в последовательности правил, заполняются конструкции нового контекста стилей. Процесс заполнения конструкций начинается с нижнего узла ветки правил, самого приоритетного (который, как правило, соответствует правилу с самым специфичным селектором) и продвигается в верх по иерархии дерева правил до тех пор, пока необходимые конструкции не будут заполнены полностью. Если узел, с которого начинается заполнение структур (самый нижний), не содержит декларации ни одного из свойств определенной конструкции, то появляется отличная возможность оптимизации — процесс создания контекста стилей поднимается вверх по ветке правил до тех пор, пока не будет найден узел, в котором полностью определена данная конструкция. Тогда в нижний узел включается лишь указатель на узел, содержащий требуемую конструкцию, что является идеальным случаем оптимизации, поскольку имеет место совместное использование целой конструкции несколькими узлами. Это позволяет сэкономить ресурсы системы, необходимые для повторного вычисления уже существующей в дереве правил конструкции, а также для хранения этих данных в памяти. Если при подъеме по ветке будут встречаться узлы, содержащие лишь некоторые из свойств заполняемой конструкции, то она будет комплектоваться постепенно, пока не будут определены все входящие в ее состав свойства.

Если в рамках найденных правил значения для свойств конструкции не определены, тогда если эта конструкция наследуемого типа, то используется указатель на аналогичную конструкцию родительского узла в дереве контекстов стилей и в этом случае у нас также происходит совместное разделение конструкции. А если эта конструкция ненаследуемого типа (reset-типа), тогда ее свойствам присваиваются их исходные значения.

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

В идеальном случае описываемой здесь оптимизации производится разделение целого объекта контекста стилей несколькими узлами дерева контекстов стилей. Это происходит когда у обрабатываемого элемента есть родственный элемент одного уровня (имеет того же родителя) или родственный элемент предыдущего уровня (имеет того же предка), который указывает на тот же узел дерева контестов стилей, что и рассматриваемый элемент.

Давайте все вышеизложенное воспроизведем с помощью примера. Предположим у нас есть вот такой 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>

и следующие правила:

1. div {margin:5px;color:black}
2. .err {color:red}
3. .big {margin-top:3px}
4. div span {margin-bottom:4px}
5. #div1 {color:blue}
6. #div2 {color:green}

Для упрощения поставленной перед нами задачи давайте предположим, что нам необходимо заполнить только две конструкцииcolor и margin. Конструкция color состоит лишь из одного компонента — собственно цвета шрифта, а в состав конструкции margin входят свойства, определяющие поля элемента с четырех его сторон, а соответственно имеем четыре компонента.
В результате получаем вот такое дерево правил (формат маркировки узлов — имя узла : #номер правила, на который он указывает):

Дерево правил.

Изображение 15: Дерево правил.

А дерево контекстов стилей будет выглядеть следующим образом (имя узла : узел дерева правил, на который он ссылается):

Дерево контестов стилей.

Изображение 16: Дерево контекстов стилей.

Допустим, что в ходе обработки документа дело дошло до второго тега <div>. Теперь процессу парсинга необходимо создать контекст стилей для этого узла и заполнить его конструкции.
После анализа имеющихся правил обнаружится, что второму div элементу удовлетворяют селекторы правил 1, 2 и 6. А значит в дереве правил уже существует ветка из требуемой последовательности уже применяемых правил (B:1 — C:2), которую можно использовать и необходимо лишь добавить новый узел в дерево для шестого правила (узел F в дереве правил).
Далее создается контекст стилей и размещается в дереве контекстов стилей. Новый контекст стилей будет указывать на узел F дерева правил.

Теперь нужно заполнить конструкции стилей. Начнем с конструкции margin. Поскольку последнее, применяемое к элементу правило, соответствующее узлу F, не добавляет никаких данных в конструкцию полей, поиск будет выполняться путем прохода ветки дерева вверх, начиная от данного узла до тех пор, пока не встретится искомая конструкция. В нашем случае будет найдена кэшированная конструкция полей (margin), которая была рассчитана при добавлении предыдущего узла. Эта конструкция находится в самом верхнем узле, определяющем значения полей — узле B. Остается лишь вставить в узел F указатель на сохраненную ранее конструкцию.

Со второй конструкцией дело обстоит несколько иначе. В последнем правиле имеется определение для входящего в ее состав свойства color, а это значит, что рассчитанная ранее, кэшированная конструкция в данном случае не может быть использована. Поскольку конструкция color состоит всего лишь из одного компонента (в отличие от margin, у которой их четыре: -top, -right, -bottom и -left), то необходимости в проходе по ветке дерева с целью заполнения конструкции нет. Остается сделать расчет конечного значения (т.е. привести указанное в декларации строчное значение к его фактическому эквиваленту, в нашем случае к RGB) и кэшировать вновь рассчитанную конструкцию в узле F дерева правил.

Что касается второго элемента <span>, то здесь все еще проще. После анализа имеющихся правил обнаружится, что этот элемент, как и предыдущий <span> указывает на узел G дерева правил. Поскольку эти два элемента являются родственными элементами одного уровня, указывающими на один и тот же узел правил, появляется возможность максимально эффективной оптимизации, то есть совместного использования целого контекста стилей и в ходе определения стилей для второго <span> элемента необходимо лишь установить для него указатель на контекст стилей предыдущего элемента.

Рассмотренные в примере две конструкции (полей и цвета шрифта) относятся к ненаследуемому типу (reset-типу), поэтому они кэшируются в дереве правил. Конструкции наследуемого типа (исходные значения свойств которых наследуются от родительского элемента) кэшируются в дереве контекстов стилей. Здесь имеет место некоторое недоразумение, ведь в действительности свойство color является наследуемым, но движок браузера Firefox обрабатывает его как reset-свойство и поэтому соответствующая ему конструкция кэшируется в дереве правил.

Примечание переводчика:*Для того чтобы внести ясность в этот вопрос давайте рассмотрим случай с конструкцией шрифта (font) для элемента абзаца (<p>) из примера. Это наследуемая конструкция и поэтому она кэшируется в дереве контекстов стилей, а поскольку элемент абзац является дочерним по отношению к <div>, то в качестве конструкции шрифта его контекста стилей будет использоваться соответствующая конструкция родительского элемента, которая была рассчитана ранее и сохранена в узле дерева контекстов стилей, который соответствует элементу <div>. Но если в таблице стилей будет найдено правило с декларацией новых значений для свойства шрифта, применяемое непосредственно к элементу абзацу:

p {font-family:Verdana;font size:10px;font-weight:bold}

тогда будет выполнен расчет новой конструкции font (целой или отдельных ее компонентов, что зависит от определяемых в рамках правила свойств), которая затем сохраняется в контексте стилей элемента абзаца. Тогда как сбрасываемые ("reset") конструкции (такие как margin и color) кэшируются в соответствующих узлах дерева правил.

В Webkit нет дерева правил, а соответствующие элементам правила анализируются четыре раза. При первом анализе учитываются только декларации, определяющие свойства с высоким приоритетом (которые должны применяться первыми, поскольку все остальные стили зависят от них, подобные свойству display) без ключевого слова !important. Во второй раз расчитываются тоже высокоприоритетные свойства, но с пометкой !important. Затем обрабатываются декларации свойств нормального приоритета (те, которые как раз зависят от высокоприоритетных) без !important и в заключение (четвертый обход) «важные» (!important) декларации с нормальным приоритетом. Таким образом в случае наличия многократных объявлений некоторых свойств будут применены последние, согласно модели CSS каскадности самые важные объявления, получаемые в результате описанного выше разрешения конфликтных ситуаций путем четрехкратного прохождения всех применяемых к элементам документа правил. Обрабатываемые в последнем цикле декларации и будут определять форматирование документа.

Подводя итог можно сказать, что благодаря возможности совместного использования объектов стилей (полного или частичного при разделении лишь некоторых, входящих в состав этих объектов конструкций) решаются первая и третья проблемные ситуации, возникающие в ходе вычисления стилей. Используемое в Firefox дерево правил к тому же способствует корректному и удобному применению стилей.

Манипулирование правилами для упрощения процесса их сопоставления.

Существует несколько способов определения правил стилей:

  • CSS правила, устанавливаемые как с помощью элемента <style>, так и в рамках внешних таблиц стилей.
    p {color:blue}
  • Встроенные или контекстные атрибуты style.
    <p style="color:blue">
  • Визуальные HTML атрибуты (которые трансформируются в соответствующие им правила стилей).
    <p bgcolor="blue">

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

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

После парсинга таблиц стилей имеющиеся в них правила распределяются по нескольким хэш-картам, которые согласно используемым в них селекторам бывают следующих видов: карты правил, использующих идентификаторы элементов (атрибут id); имена классов (атрибут class); имена тегов элементов; и общая хэш-карта, включающая все остальные правила, не входящие в перечисленные ранее категории. Если в правиле в качестве селектора используется идентификатор элемента, то оно будет добавлено в хэш-карту идентификаторов, если селектором является имя класса, то в хэш-карту классов и так далее.

Такое манипулирование позволяет значительно упростить процесс поиска соответствующих правил, освобождая от необходимости в переборе каждой имеющейся в наличии декларации. Правила стилей для необходимого элемента могут быть извлечены из систематизированных описанным выше способом хэш-карт. Благодаря такой оптимизации количество обрабатываемых правил сокращается более чем на 95%, то есть они могут даже не учитываться в процессе поиска соответствия (4.1).

Давайте в качестве примера рассмотрим набор следующих правил:

p.error {color:red}
#messageDiv {height:50px}
div {margin:5px}

Первое правило будет занесено в карту классов, второе в карту идентификаторов, а третье в карту имен элементов. К примеру, у нас есть вот такой фрагмент разметки:

<p class="error">an error occurred </p>
<div id=" messageDiv">this is a message</div>

Сначала мы попытаемся найти стилевые правила для элемента <p>. Хэш-карта классов будет содержать ключ "error", указывающий на нужное правило p.error. Элементу <div> будут соответствовать правила из карты идентификаторов (ключом в этом случае является значение атрибута id элемента) и из карты имен тегов. Все, что нам остается, это выяснить какие из извлеченных правил на самом деле соответствуют требуемым элементам. То есть, если, допустим, у нас имеется вот такое правило для элемента <div>:

table div {margin:5px}

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

Оба движка Webkit и Gecko применяют манипуляцию правилами.

Корректное применение правил согласно модели каскадности.

У объекта стилей есть свойства, соответствующие каждому из визуальных атрибутов (представляют CSS свойства, но более универсальны в использовании). Если какое-либо свойство не определено с помощью применяемых правил, тогда некоторые из них могут быть унаследованы от стилевых объектов родительских элементов. Другие же будут установлены в их исходные значения.

Проблемы начинаются когда имеются многократные определения свойств и здесь выручает предусмотренная моделью CSS каскадности последовательность их применения.

Каскадность таблиц стилей.

Декларации стилевых свойств могут находиться в различных по происхождению таблицах стилей и встречаться по несколько раз в пределах одной из них. Это говорит о том, что для их согласованного применения необходимо руководствоваться четкой последовательностью. Эта последовательность определена спецификацией CSS2 в рамках «Модели CSS каскадности» и имеет следующий вид (в порядке восходящей приоритетности — от низкой до высшей):

  1. Декларации, определяемые браузером.
  2. Стандартные декларации пользователя браузера.
  3. Авторские стандартные декларации.
  4. Авторские важные (!important) декларации.
  5. Важные (!important) декларации пользователя браузера.

Самыми низкоприоритетными являются декларации браузера, а пользовательские декларации могут переопределить авторские только в случае их использования совместно с ключевым словом !important. Дальнейшая приоритетность деклараций, которые согласно каскадности находятся на одном уровне, определяется на основе их специфичности, а в случае и ее эквивалентности, последовательностью их определения (последняя декларация переопределяет объявленную ранее). Предусмотренные HTML синтаксисом визуальные атрибуты преобразуются в соответствующие им CSS декларации, приоритет которых приравнивается к авторским стандартным декларациям (третий уровень каскадности).

Специфичность.

Специфичность селекторов правил определена в спецификации CSS2. Для представления специфичности используется особый вид записи в форме четырех чисел, разделенных запятой (a,b,c,d), каждое из которых является отдельным разрядом. Разряды заполняются следующим образом:

  • Если декларация находится в контекстном атрибуте style, в разряд а заносится единица. То есть, если свойство объявлено в одном из правил таблицы стилей (как это обычно делается), то в разряд а не добавляется ничего.
  • Число, равное количеству используемых в селекторе идентификаторов элементов (id) вносится в разряд b.
  • Число, получаемое в результате суммирования количества находящихся в селекторе имен классов (.className), псевдоклассов (подобных :hover) и упоминаний атрибутов (element[attributeName] или element[attributeName=attributeValue]) заносится в разряд с.
  • Последний разряд d отражает количество используемых в селекторе имен элементов и псевдоэлементов.

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

Несколько примеров:

* {} /* 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 */

*Если у вас все-таки остались некоторые сомнения по поводу ясности вопроса CSS специфичности, рекомендую развеять их, ознакомившись со статьей Крисса Койера «Специфика CSS специфичности».

Сортировка правил.

После сопоставления и нахождения применяемых правил производится их сортировка в соответствии с предусмотренными каскадностью приоритетами. В 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;
}

Последовательность процесса построения дерева визуализации.

Для сигнализации о том, что все таблицы стилей высшего уровня (влияющие на глобальное форматирование документа), включая объявленные с помощью деклараций @import, успешно загружены, Webkit использует специальный системный флаг. В том случае, если не все прилагаемые к документу таблицы стилей загружены, при форматировании элементов используются их заполнители (т.е. аналоги элементов, к которым применены стилевые настройки по умолчанию) с пометкой о том, что соответствующие им данные стилей еще не получены. Далее, по окончании загрузки всех таблиц производится перерасчет стилей, и заполнители заменяются необходимым образом отформатированными элементами.

Компоновка.

Сразу же после создания рендерера и вставки его в дерево визуализации он еще не наделен такой информацией как соответствующие ему размеры и позиция. Процесс расчета этих данных называется «компоновкой» ("layout" в Webkit) или «реорганизацией потока» ("reflow" в Gecko).

HTML стандартом предусмотрена поточная модель компоновки документа, благодаря которой расчет геометрии производится, как правило, за один проход. Таким образом все находящиеся ниже в потоке документа элементы зачастую не оказывают влияния на геометрические характеристики элементов, которые расположены выше по потоку. В результате чего компоновка выполняется последовательно («поточным образом») в направлении слева направо и сверху вниз до конца документа. Однако, существуют исключения. В качестве примера можно привести таблицы, для формирования которых может понадобиться более одного прохода их структуры (3.5).

Используемая в ходе компоновки система координат определяется относительно корневого фрейма с координатами 'top' (сдвиг относительно верхней грани фрейма) и 'left' (сдвиг относительно левой грани фрейма).

Компоновка является рекурсивным процессом, то есть она производится за несколько подходов, начиная с корневого рендерера, который соответствует <html> элементу HTML документа и рекурсивно продолжается (другими словами вызывается применительно к каждому задействованному фрейму), проходя далее через некоторые или все фреймы фреймовой структуры документа, в результате чего производится расчет геометрических данных для каждого рендерера, который в этом нуждается.

Позиция корневого рендерера устанавливается с помощью координат 0, 0 ('top' и 'left' соответственно), а его размеры определяются размерами вьюпорта, т.е. видимой областью окна браузера.

Для каждого объекта рендерера предусмотрен метод layout (Webkit) или reflow (Gecko), который вызывается им для требующих компоновки дочерних объектов.

Система «битов очистки».

Для того, чтобы не выполнять компоновку всего документа в целом при каждом небольшом изменении его структуры, браузеры используют специальную систему, основанную на «битах очистки». Смысл в том, что новый, добавляемый в дерево или модифицированный фрейм с помощью служебного бита (флага) помечается сам, а также устанавливает соответствующие биты своих дочерних фреймов как "dirty" (дословно «грязный»), то есть требующий компоновки.

Существует два типа dirty-флага:

  • dirty
    Компоновки требует сам фрейм и как следствие его дочерние фреймы.
  • dirty children
    Означает, что хотя с самим фреймом, у которого установлен этот флаг все в порядке, его дочерним фреймам нужна перекомпоновка.

Глобальная и инкрементная компоновка.

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

  1. Изменение стилей в масштабе документа (к примеру, когда добавляются или удаляются таблицы стилей или изменяется используемый в рамках документа тип шрифта), которое распространяется на все объекты визуализации.
  2. При изменении размеров окна браузера.

Возможна также «инкрементная» компоновка, которая по сравнению с глобальной не столь масштабна и производится только для помеченных битом очистки рендереров, что в свою очередь может стать причиной выполнения побочных изменений в визуальной структуре документа, требующих дополнительных, связанных с компоновкой действий. Инкрементные компоновки выполняются "en masse" («в массе» или совместно) и асинхронно, в специально отведенный для этого момент. Все накопившиеся к этому времени «грязные» (требующие перекомпоновки) фреймы соответствующим образом обрабатываются. Компоновка такого типа может происходить, к примеру, по мере поступления дополнительного контента из сети с последующим добавлением новых узлов в DOM дерево и как следствие новых рендереров в дерево визуализации.

Инкрементная компоновка.

Изображение 17: Инкрементная компоновка — производится форматирование только помеченных флагом «очистки» рендереров и их дочерних объектов визуализации (3.6).

*Примечание переводчика: Раз уж дело дошло до используемых в Gecko вариантов компоновки, считаю необходимым вкратце познакомить читателя со всеми, предусмотренными этим движком типами компоновки:

  • Initial
    Начальная — имеет место при самом первом проходе иерархии дерева процессом визуализации.
  • Incremental
    Инкрементная — как уже говорилось выше, выполняется в случае каких-либо изменений в дереве фреймов, применительно к задействованному этими изменениями фрейму.
  • Resize
    Модификация размеров — имеет место в том случае, если изменяются размеры границ области, содержащей фреймовую структуру. Здесь речь идет исключительно об изменении величины пространства, заключающего в себе иерархию визуальных объектов, то есть в этом случае пересчитывается только позиции фреймов, находящихся в рамках модифицируемого в размерах родительского фрейма, без изменения их внутренних стилей (шрифта, цвета, границ и т.д.).
  • StyleChange
    Смена стилей — производится только в тех случаях, когда меняются прилагаемые к документу стили, требующие полного обхода иерархии дерева фреймов целиком. Это может быть, к примеру, смена дефолтного типа шрифта.
  • Dirty
    «Грязная» — инициализируется, если фрейм-контейнер, путем установки битов очистки сообщает о необходимости выполнения ряда отдельных инкрементных компоновок своих дочерних фреймов.

Асинхронная и синхронная компоновка.

Как уже было сказано выше, инкрементная компоновка, как правило, производится асинхронно. В Firefox это делается путем формирования очереди из требующих выполнения reflow-команд, из которой в предусмотренный планировщиком момент времени эти команды извлекаются и пакетным образом обрабатываются. В Webkit аналогичная задача решается похожим способом — для запуска инкрементных компоновок предусмотрен специальный таймер, периодически инициализирующий проход по дереву с компоновкой помеченных битом очистки рендереров. Компоновка инкрементного типа может происходить и синхронно. Это бывает когда в ходе выполнения скриптов производится запрос информации о стилях определенного элемента (с помощью скриптовых свойств подобных offsetHeight).

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

Оптимизация компоновки.

Если компоновка вызвана модификацией размеров (Resize) или сменой позиции рендерера (но ни его собственных размеров), тогда значения его размеров извлекаются из кэша, а не рассчитываются заново.

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

Процесс компоновки.

Процесс компоновки, как правило, происходит по следующей схеме:

  1. Определяется ширина родительского рендерера.
  2. Выполняется обход его дочерних объектов визуализации, в ходе которого:
    1. Каждый дочерний рендерер позиционируется (устанавливаются его координаты X и Y).
    2. При необходимости вызывается компоновка дочерних рендереров (для этого есть несколько причин: если у дочернего рендерера установлен dirty-флаг; входе выполнения глобальной компоновки; или по какой-либо другой причине), в результате которой рассчитываются значения их высоты.
  3. Путем сложения совокупной высоты дочерних рендереров, их вертикальных (верхних и нижних) полей и внутренних отступов, определяется высота самого родительского рендерера, которая в свою очередь используется при расчете уже его родительского объекта визуализации.
  4. Сбрасывается бит очистки родительского рендерера.

В Firefox в качестве одного из параметров, передаваемых процессу компоновки (именуемому "reflow"), используется объект «состояния» (nsHTMLReflowState). Помимо всего прочего данный объект содержит информацию о ширине родительского фрейма. Выходные данные компоновки в Firefox реализованы в виде объекта «метрик» (nsHTMLReflowMetrics), посредством которого родительскому фрейму сообщается значение высоты его дочерних объектов визуализации.

Расчет ширины.

Вычисление значения ширины рендерера производится с учетом ширины блока контейнера, значения стилевого свойства width рендерера, его полей и границ.

К примеру, ширина этого div элемента:

<div style="width:30%"/>

в WebKit была бы рассчитана следующим образом (посредством применения метода calcWidth класса RenderBox):

  • Сначала определяется ширина контейнера, представляющая собой максимум от значений доступной ширины контейнера (availableWidth) и нуля. В нашем случае доступная ширина является шириной области контента (contentWidth), которая рассчитывается так:
    clientWidth() — paddingLeft() — paddingRight()

    Горизонтальный clientWidth и вертикальный clientHeight размер клиента представляет собой внутреннюю область содержащего объекта визуализации без учета его границ (border) и полос прокрутки.

  • Далее вычисляется ширина самого элемента, определяемая атрибутом style с помощью свойства width. Его значение будет преобразовано к абсолютной величине путем расчета указанного процентного значения ширины контейнера, установленной ранее.
  • К полученной ширине элемента добавляются значения горизонтальных внутренних отступов и границ.

До этого момента мы имели дело с «предпочтительной» шириной. Теперь необходимо откорректировать ее в соответствии с минимально и максимально допустимыми значениями ширины (определяются свойствами min-width и max-width). Если предпочтительная ширина больше максимальной, в качестве результирующего значения ширины рендерера берется максимальная ширина. Если же она меньше минимально допустимого значения, то используется минимальное значение.

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

Переносы строк.

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

Отрисовка.

На стадии отрисовки происходит проход по дереву визуализации с вызовом метода paint() для каждого рендерера, отвечающего за отображение контента на экране. В процессе отрисовки используются компоненты UI инфраструктуры (*простейшие графические элементы, виджеты — кнопка, флажок и т.д.).

Глобальная и инкрементная отрисовка.

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

Последовательность отрисовки.

Спецификацией CSS2 определен порядок процесса отрисовки, который фактически отображает последовательность формирования стека элементов в стековом контексте. Данный порядок имеет прямое отношение к процессу отрисовки поскольку визуализация стека производится начиная с низлежащих объектов, распространяясь к его поверхности. Последовательность формирования стека рендерера блочного элемента имеет следующий вид:

  1. Цвет фона элемента.
  2. Фоновое изображение.
  3. Границы элемента.
  4. Его дочерние элементы.
  5. Контур элелемента.

Список отображения в Firefox.

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

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

Хранение отрисованных прямоугольных областей в WebKit.

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

Динамичные изменения.

Браузеры стараются в максимальной степени минимизировать количество действий, выполняемых в результате внесения изменений. Поэтому если изменения касаются только цвета определенного элемента, то достаточно будет только перерисовки этого элемента. Если модифицируется позиция элемента — браузер вынужден выполнить компоновку и перерисовку самого элемента, его дочерних элементов и, возможно, последующих потомков. После добавления нового узла в структуру DOM производится компоновка и отрисовка генерируемых данным узлом объектов визуализации. Более существенные изменения, подобные увеличению размера шрифта html элемента приведут к удалению соответствующего кэша, перекомпоновке и перерисовке всего дерева визуализации.

Потоки движка визуализации.

Модуль визуализации браузера функционирует на одном потоке. Все выполняемые им задачи, за исключением сетевых, реализуются средствами одного потока. В Firefox и Safari это главный процесс браузера, а в Chrome — основной поток процесса вкладки. Сетевые операции могут выполняться посредством нескольких параллельных потоков, количество которых ограничено (как правило, от 2 до 6 соединений).

Цикл событий.

Основной поток браузера функционирует по принципу цикла событий, непрерывного цикла, поддерживающего «жизнь» главного процесса. Он находится в состоянии постоянного ожидания событий (подобных тем, которые инициализируют компоновку и отрисовку) и выполняет их обработку. Вот, к примеру, в Firefox фрагмент кода, реализующий основной цикл событий выглядит таким образом:

while (!mExiting)
NS_ProcessNextEvent(thread);

Визуальная модель форматирования стандарта CSS2.

Канва.

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

Согласно Приложению E — Детальное описание стекового контекста, если канва находится в рамках другой канвы, то она прозрачна, в противном случае ее цвет определяется браузером.

Модель CSS бокса.

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

Модель CSS бокса.

Изображение 18: Модель CSS бокса.

Для каждого узла создается от 0 до n боксов. Все элементы обладают свойством display, которое определяет тип генерируемого для элемента бокса. Вот несколько примеров значений данного свойства:

block: создается блочный бокс.
inline: создается один или более внутристрочных боксов.
none: для элемента бокс не генерируется.

Хотя исходным значением данного свойства является inline, оно может быть переопределено другим дефолтным значением в таблице стилей браузера. Так, к примеру, элемент div по умолчанию устанавливается как блочный. Увидеть пример дефолтной таблицы стилей вы можете здесь: http://www.w3.org/TR/CSS2/sample.html.

Схемы позиционирования.

Существует три схемы позиционирования:

  1. Нормальный поток: элемент размещается согласно его позиции в структуре документа. Это значит, что расположение соответствующего ему объекта дерева визуализации и узла DOM структуры совпадают, а сам элемент позиционируется с учетом типа генерируемого им бокса и его размеров.
  2. Обтекание: изначально элемент размещается как предусмотрено нормальным потоком документа, затем максимально смещается влево или вправо.
  3. Абсолютное позиционирование: позиции DOM узла элемента и соответствующего ему объекта дерева визуализации не совпадают.

Cхема позиционирования, в которой задействован элемент, определяется исходя из значений его свойств position и float.

  • Если position установлено в static или relative, то позиция элемента определяется нормальным потоком.
  • Если для свойства position используются значения absolute или fixed — имеет место абсолютное позиционирование.

В ходе компоновки документа позиция бокса элемента определяется с учетом следующих факторов:

  • Тип бокса.
  • Размеры бокса.
  • Схема позиционирования.
  • Внешняя информация, такая как размер изображения, экрана устройства и т.п.

Типы боксов.

Бокс блочного уровня — формирует блок (отдельная прямоугольная область окна браузера).

Блочный бокс.

Изображение 19: Блочный бокс.

Бокс строчного уровня — не образует отдельного блока в окне браузера, а находится внутри содержащего его блока.

Внутристрочные боксы.

Изображение 20: Внутристрочные боксы.

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

Форматирование блочных и строчных элементов.

Изображение 21: Форматирование блочных и строчных элементов.

Боксы строчного уровня распространяются построчно внутри строчных боксов. Высота строки всегда не меньше высоты находящегося в ней самого высокого внутристочного бокса, но может превышать ее если выравнивание боксов производится по базовой линии, при котором базовая линия бокса и базовая линия его родительского бокса совпадают (т.е. нижняя грань бокса выравнивается не по нижней грани родительского бокса). Если ширины бокса-контейнера недостаточно, то не вмещающиеся в строку внутристрочные боксы будут отображены с использованием нескольких строк. Вот что обычно происходит внутри элемента абзаца <p>.

Строчные боксы.

Изображение 22: Строчные боксы.

Позиционирование.

Относительное.

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

Относительное позиционирование.

Изображение 23: Относительное позиционирование.

Плавающие элементы.

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

<p>
<img style="float: right" src="images/image.gif" width="100"
height="100">
Lorem ipsum dolor sit amet, consectetuer…
</p>

приведет к такому форматированию:

Смещенный элемент.

Изображение 24: Смещенный элемент.

Абсолютное и фиксированное.

В этой схеме позиционирования компоновка производится абсолютно независимо от нормального потока и задействованные в ней элементы просто изымаются из потока документа. При этом вся геометрия этих элементов просчитывается относительно их контейнера (*здесь контейнером является не просто элемент, в рамки которого заключены абсолютно позиционированные элементы, а потомок, со значением свойства position отличным от static). Когда к элементу применяется фиксированное позиционирование, то в качестве его контейнера выступает вьюпорт.

Фиксированное позиционирование.

Изображение 25: Фиксированное позиционирование.

Фиксировано позиционированный бокс «фиксируется» в определенном месте окна браузера и никогда не сдвигается, даже при прокрутке документа.

Слои представления документа.

Многослойность представления определена с помощью CSS свойства z-index, которое устанавливает позицию элемента в третьем измерении, относительно оси Z.

Боксы документа задействованы в различных стеках (именуемых стековыми контекстами). Нижний элемент каждого стека будет отрисован первым, а верхний ближе к «поверхности», то есть к пользователю. Если участвующие в определенном контексте форматирования элементы накладываются друг на друга, то верхний элемент будет частично или полностью перекрывать нижний. Организация стеков производится путем манипуляции свойством z-index. Любой элемент со значением этого свойства отличным от auto образует собственный локальный стековый контекст. Вьюпорт устанавливает внешний стековый контекст.

Для наглядности рассмотрим пример:

<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>

В результате имеем:

Слои документа.

Изображение 26: Слои документа.

Несмотря на то, что согласно исходному коду красный див предшествует зеленому, вследствие чего согласно нормальному потоку должен отрисовываться раньше, а значит под зеленым блоком, он находится в верхнем слое документа, так как значение его свойства z-index превышает значение аналогичного свойства зеленого дива. Оба слоя принадлежат стековому контексту, образованному корневым боксом.

Использованная литература.

  1. Архитектура браузера:
    1. Grosskurth, Alan. «Базовая архитектура веб-браузеров».
    2. Gupta, Vineet. «Как работают браузеры — Часть 1 — Архитектура».
  2. Парсинг:
    1. Aho, Sethi, Ullman, Compilers. Принципы, Технологии и Инструментарии (известная как «Книга дракона»), Addison-Wesley, 1986.
    2. Rick Jelliffe. «Дерзкие и красивые: два новых проекта HTML5 стандарта».
  3. Firefox:
    1. L. David Baron. «Ускорение HTML и CSS: внутреннее устройство движка компоновки для веб-разработчиков».
    2. L. David Baron. «Ускорение HTML и CSS: внутреннее устройство движка компоновки для веб-разработчиков (Google tech talk видео)».
    3. L. David Baron. «Движок компоновки Mozilla».
    4. L. David Baron. «Документация по системе стилизации Mozilla».
    5. Chris Waterson. «Комментарии по реорганизации HTML потока».
    6. Chris Waterson. «Обзор движка Gecko Overview».
    7. Alexander Larsson. «Жизненный цикл HTTP запроса HTML документа».
  4. Webkit:
    1. David Hyatt. «Реализация CSS (часть 1)».
    2. David Hyatt. «Обзор платформы WebCore».
    3. David Hyatt. Визуализация WebCore».
    4. David Hyatt. «Проблема FOUC».
  5. Спецификации W3C:
    1. «Спецификация HTML 4.01».
    2. «W3C версия спецификации HTML5».
    3. «Спецификация CSS 2.1 (Каскадные таблицы стилей — Уровень 2 — Редакция 1)».
  6. Рекомендации по разаботке браузеров:
    1. Firefox. https://developer.mozilla.org/en/Build_Documentation.
    2. WebKit. http://webkit.org/building/build.html.
* Примечание переводчика.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *