Jak działają przeglądarki

Za kulisami nowoczesnych przeglądarek

Wstęp

To kompleksowe wprowadzenie do wewnętrznych operacji WebKit i Gecko jest wynikiem wielu badań przeprowadzonych przez izraelskiego programistę Tali Garsiel. W ciągu kilku lat przeanalizowała wszystkie opublikowane dane o wewnętrznych mechanizmach przeglądarek i poświęciła dużo czasu na czytanie kodu źródłowego przeglądarek internetowych. Napisała:

Jeśli jesteś programistą stron internetowych, poznanie wewnętrznych mechanizmów działania przeglądarki pomoże Ci podejmować lepsze decyzje i zrozumieć uzasadnienie sprawdzonych metod programistycznych. Chociaż jest to dość długi dokument, zalecamy poświęcenie trochę czasu na jego przeczytanie. Warto!

Paul Irish, Chrome Developer Relations

Wprowadzenie

Przeglądarki internetowe to najczęściej używane oprogramowanie. W tym wprowadzeniu wyjaśniam, jak działają za kulisami. Zobaczymy, co się stanie, gdy wpiszesz google.com w pasku adresu, aż na ekranie przeglądarki pojawi się strona Google.

Przeglądarki, o których będziemy rozmawiać

Obecnie na komputerach używa się 5 głównych przeglądarek: Chrome, Internet Explorer, Firefox, Safari i Opera. Na urządzeniach mobilnych główne przeglądarki to przeglądarka Androida, iPhone, Opera Mini i Opera Mobile, UC Browser, przeglądarki Nokia S40/S60 oraz Chrome. Wszystkie z nich, z wyjątkiem przeglądarek Opera, są oparte na WebKit. Podam przykłady z przeglądarek open source Firefox i Chrome oraz Safari (która jest częściowo open source). Według statystyk StatCounter (z czerwca 2013 r.) Chrome, Firefox i Safari stanowią około 71% globalnego wykorzystania przeglądarek na komputerach. Na urządzeniach mobilnych przeglądarka Androida, iPhone i Chrome stanowią około 54% użycia.

główne funkcje przeglądarki,

Główną funkcją przeglądarki jest prezentowanie wybranego zasobu internetowego przez wysłanie żądania do serwera i wyświetlenie go w oknie przeglądarki. Zasób to zwykle dokument HTML, ale może to być też plik PDF, obraz lub inny rodzaj treści. Lokalizację zasobu określa użytkownik za pomocą identyfikatora URI (Uniform Resource Identifier).

Sposób, w jaki przeglądarka interpretuje i wyświetla pliki HTML, jest określony w specyfikacjach HTML i CSS. Te specyfikacje są utrzymywane przez organizację W3C (World Wide Web Consortium), która jest organizacją standardów internetowych. Przez lata przeglądarki były zgodne tylko z częścią specyfikacji i tworzyły własne rozszerzenia. Powodowało to poważne problemy ze zgodnością dla twórców stron internetowych. Obecnie większość przeglądarek jest mniej lub bardziej zgodna ze specyfikacjami.

Interfejsy przeglądarek mają ze sobą wiele wspólnego. Typowe elementy interfejsu użytkownika:

  1. Pasek adresu do wstawiania identyfikatora URI
  2. Przyciski Wstecz i Dalej
  3. Opcje dodawania zakładek
  4. Przyciski odświeżania i zatrzymywania do odświeżania lub zatrzymywania wczytywania bieżących dokumentów
  5. przycisk strony głównej, który przenosi Cię na stronę główną;

Co ciekawe, interfejs przeglądarki nie jest określony w żadnej formalnej specyfikacji. Wynika on po prostu z dobrych praktyk ukształtowanych przez lata doświadczeń i naśladowania się nawzajem przez przeglądarki. Specyfikacja HTML5 nie definiuje elementów interfejsu, które musi mieć przeglądarka, ale wymienia niektóre typowe elementy. Są to m.in. pasek adresu, pasek stanu i pasek narzędzi. Istnieją oczywiście funkcje unikalne dla konkretnej przeglądarki, takie jak menedżer pobierania w Firefoksie.

Infrastruktura wysokiego poziomu

Główne komponenty przeglądarki to:

  1. Interfejs użytkownika: obejmuje pasek adresu, przycisk wstecz/dalej, menu zakładek itp. Każda część wyświetlacza przeglądarki z wyjątkiem okna, w którym widać żądaną stronę.
  2. Silnik przeglądarki: zarządza działaniami między interfejsem a silnikiem renderowania.
  3. Silnik renderowania: odpowiada za wyświetlanie żądanych treści. Jeśli na przykład żądana treść jest w formacie HTML, silnik renderowania analizuje kod HTML i CSS, a następnie wyświetla przeanalizowaną treść na ekranie.
  4. Sieć: w przypadku wywołań sieciowych, takich jak żądania HTTP, używanie różnych implementacji dla różnych platform za interfejsem niezależnym od platformy.
  5. UI backend: używany do rysowania podstawowych widżetów, takich jak pola kombi i okna. Ten backend udostępnia ogólny interfejs, który nie jest powiązany z konkretną platformą. W tle używa metod interfejsu systemu operacyjnego.
  6. Interpreter JavaScript. Służy do analizowania i wykonywania kodu JavaScript.
  7. Magazyn danych. Jest to warstwa trwałości. Przeglądarka może potrzebować zapisać lokalnie różne rodzaje danych, np. pliki cookie. Przeglądarki obsługują też mechanizmy pamięci masowej, takie jak localStorage, IndexedDB, WebSQL i FileSystem.
Komponenty przeglądarki
Ilustracja 1. Komponenty przeglądarki

Warto pamiętać, że przeglądarki takie jak Chrome uruchamiają wiele instancji silnika renderowania – po jednej na każdą kartę. Każda karta działa w osobnym procesie.

Silniki renderowania

Silnik renderowania odpowiada za renderowanie, czyli wyświetlanie żądanych treści na ekranie przeglądarki.

Domyślnie silnik renderowania może wyświetlać dokumenty HTML i XML oraz obrazy. Może wyświetlać inne typy danych za pomocą wtyczek lub rozszerzeń, np. dokumenty PDF za pomocą wtyczki przeglądarki PDF. W tym rozdziale skupimy się jednak na głównym zastosowaniu: wyświetlaniu kodu HTML i obrazów sformatowanych za pomocą CSS.

Różne przeglądarki używają różnych silników renderowania: Internet Explorer używa Trident, Firefox – Gecko, a Safari – WebKit. Chrome i Opera (od wersji 15) używają silnika Blink, który jest rozwidleniem WebKit.

WebKit to silnik renderujący typu open source, który powstał jako silnik dla platformy Linux i został zmodyfikowany przez Apple, aby obsługiwać systemy Mac i Windows.

Główny proces

Silnik renderowania zacznie pobierać zawartość żądanego dokumentu z warstwy sieciowej. Zwykle odbywa się to we fragmentach o rozmiarze 8 kB.

Następnie silnik renderowania wykonuje te podstawowe czynności:

Podstawowy przepływ silnika renderowania
Rysunek 2. Podstawowy przepływ silnika renderowania

Silnik renderowania rozpocznie analizowanie dokumentu HTML i przekonwertuje elementy na węzły DOM w drzewie zwanym „drzewem treści”. Silnik przeanalizuje dane dotyczące stylu zarówno w zewnętrznych plikach CSS, jak i w elementach stylu. Informacje o stylu wraz z instrukcjami wizualnymi w kodzie HTML zostaną użyte do utworzenia kolejnego drzewa: drzewa renderowania.

Drzewo renderowania zawiera prostokąty z atrybutami wizualnymi, takimi jak kolor i wymiary. Prostokąty są w odpowiedniej kolejności, aby wyświetlać się na ekranie.

Po utworzeniu drzewa renderowania następuje proces „układu”. Oznacza to, że każdy węzeł ma dokładne współrzędne, które określają, gdzie powinien pojawić się na ekranie. Następnym etapem jest malowanie – drzewo renderowania zostanie przeanalizowane, a każdy węzeł zostanie pomalowany przy użyciu warstwy backendu interfejsu.

Ważne jest, aby zrozumieć, że jest to proces stopniowy. Aby zapewnić użytkownikom większą wygodę, silnik renderowania stara się jak najszybciej wyświetlić zawartość na ekranie. Nie czeka na przeanalizowanie całego kodu HTML, zanim zacznie tworzyć i układać drzewo renderowania. Części treści będą analizowane i wyświetlane, podczas gdy proces będzie kontynuowany z pozostałymi treściami, które będą nadal napływać z sieci.

Przykłady głównego przepływu

Główny przepływ WebKit.
Ilustracja 3. Główny przepływ WebKit
Główny przepływ mechanizmu renderowania Gecko firmy Mozilla.
Ilustracja 4. Główny przepływ silnika renderowania Gecko Mozilli

Na rysunkach 3 i 4 widać, że chociaż WebKit i Gecko używają nieco innej terminologii, proces jest zasadniczo taki sam.

Gecko nazywa drzewo elementów sformatowanych wizualnie „drzewem ramek”. Każdy element to klatka. WebKit używa terminu „drzewo renderowania”, które składa się z „obiektów renderowania”. WebKit używa terminu „układ” do umieszczania elementów, podczas gdy Gecko nazywa to „ponownym przepływem”. „Załącznik” to termin WebKit oznaczający łączenie węzłów DOM i informacji wizualnych w celu utworzenia drzewa renderowania. Niewielka, niesemantyczna różnica polega na tym, że Gecko ma dodatkową warstwę między kodem HTML a drzewem DOM. Nazywa się on „content sink” i jest fabryką do tworzenia elementów DOM. Omówimy każdą część tego procesu:

Analizowanie – ogólne

Ponieważ analizowanie jest bardzo ważnym procesem w silniku renderowania, przyjrzymy się mu nieco dokładniej. Zacznijmy od krótkiego wprowadzenia do analizowania.

Analiza dokumentu oznacza przetłumaczenie go na strukturę, której może używać kod. Wynikiem analizy jest zwykle drzewo węzłów, które reprezentują strukturę dokumentu. Nazywa się to drzewem analizy składniowej lub drzewem składniowym.

Na przykład przeanalizowanie wyrażenia 2 + 3 - 1 może zwrócić to drzewo:

Węzeł drzewa wyrażeń matematycznych.
Rysunek 5. Węzeł drzewa wyrażenia matematycznego

Gramatyka

Analiza składniowa opiera się na regułach składni, których przestrzega dokument: języku lub formacie, w którym został napisany. Każdy format, który można przeanalizować, musi mieć deterministyczną gramatykę składającą się ze słownictwa i reguł składni. Nazywa się to gramatyką bezkontekstową. Języki ludzkie nie są takimi językami, dlatego nie można ich analizować za pomocą konwencjonalnych technik analizy składniowej.

Kombinacja parsera i analizatora leksykalnego

Analizę składniową można podzielić na 2 podprocesy: analizę leksykalną i analizę składniową.

Analiza leksykalna to proces dzielenia danych wejściowych na tokeny. Tokeny to słownictwo języka, czyli zbiór prawidłowych elementów składowych. W języku ludzkim będzie się składać ze wszystkich słów, które pojawiają się w słowniku danego języka.

Analiza składniowa polega na stosowaniu reguł składni języka.

Parsery zwykle dzielą pracę między 2 komponenty: lexer (czasami nazywany tokenizerem), który odpowiada za dzielenie danych wejściowych na prawidłowe tokeny, oraz parser, który odpowiada za konstruowanie drzewa składniowego przez analizowanie struktury dokumentu zgodnie z regułami składni języka.

Lekser wie, jak usunąć nieistotne znaki, takie jak odstępy i podziały wiersza.

Od dokumentu źródłowego do drzew składniowych
Rysunek 6. Od dokumentu źródłowego do drzew składniowych

Proces analizy jest iteracyjny. Parser zwykle prosi analizator leksykalny o nowy token i próbuje dopasować go do jednej z reguł składni. Jeśli reguła jest spełniona, do drzewa składniowego zostanie dodany węzeł odpowiadający tokenowi, a parser poprosi o kolejny token.

Jeśli żadna reguła nie pasuje, parser zapisze token wewnętrznie i będzie prosić o tokeny, dopóki nie znajdzie reguły pasującej do wszystkich zapisanych wewnętrznie tokenów. Jeśli nie znajdzie żadnej reguły, parser zgłosi wyjątek. Oznacza to, że dokument był nieprawidłowy i zawierał błędy składni.

Tłumaczenie

W wielu przypadkach drzewo składniowe nie jest ostatecznym produktem. Analiza składniowa jest często używana w tłumaczeniu: przekształca dokument wejściowy na inny format. Przykładem jest kompilacja. Kompilator, który kompiluje kod źródłowy do kodu maszynowego, najpierw analizuje go i tworzy drzewo składniowe, a następnie przekształca je w dokument kodu maszynowego.

Proces kompilacji
Rysunek 7. Proces kompilacji

Przykład analizy składniowej

Na rysunku 5 utworzyliśmy drzewo składniowe z wyrażenia matematycznego. Spróbujmy zdefiniować prosty język matematyczny i zobaczmy, jak przebiega proces analizy.

Składnia:

  1. Elementami składowymi składni języka są wyrażenia, terminy i operacje.
  2. Nasz język może zawierać dowolną liczbę wyrażeń.
  3. Wyrażenie jest definiowane jako „termin”, po którym następuje „operacja”, a po niej kolejny termin.
  4. Operacja to token plusa lub minusa.
  5. Termin to token liczby całkowitej lub wyrażenie.

Przeanalizujmy dane wejściowe 2 + 3 - 1.

Pierwszy podłańcuch pasujący do reguły to 2: zgodnie z regułą 5 jest to hasło. Drugie dopasowanie to 2 + 3: pasuje do trzeciej reguły: termin, po którym następuje operacja, a po niej kolejny termin. Następne dopasowanie zostanie znalezione dopiero na końcu danych wejściowych. 2 + 3 - 1 jest wyrażeniem, ponieważ wiemy już, że 2 + 3 jest członem, więc mamy człon, po którym następuje operacja, a po niej kolejny człon. Wartość 2 + + nie będzie pasować do żadnej reguły, więc jest nieprawidłowa.

Formalne definicje słownictwa i składni

Słownictwo jest zwykle wyrażane za pomocą wyrażeń regularnych.

Na przykład nasz język będzie zdefiniowany jako:

INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -

Jak widać, liczby całkowite są definiowane przez wyrażenie regularne.

Składnia jest zwykle definiowana w formacie BNF. Nasz język będzie zdefiniowany jako:

expression :=  term  operation  term
operation :=  PLUS | MINUS
term := INTEGER | expression

Powiedzieliśmy, że język może być analizowany przez zwykłe parsery, jeśli jego gramatyka jest gramatyką bezkontekstową. Intuicyjna definicja gramatyki bezkontekstowej to gramatyka, którą można w całości wyrazić w BNF. Formalną definicję znajdziesz w artykule w Wikipedii na temat gramatyki bezkontekstowej.

Typy parserów

Istnieją 2 rodzaje parserów: parsery zstępujące i wstępujące. Intuicyjne wyjaśnienie jest takie, że parsery zstępujące analizują strukturę składni na wysokim poziomie i próbują znaleźć pasującą regułę. Analizatory oddolne zaczynają od danych wejściowych i stopniowo przekształcają je w reguły składni, zaczynając od reguł niskiego poziomu, aż do spełnienia reguł wysokiego poziomu.

Zobaczmy, jak oba typy parserów przeanalizują nasz przykład.

Analizator składniowy typu „od góry do dołu” rozpocznie od reguły wyższego poziomu: zidentyfikuje 2 + 3 jako wyrażenie. Następnie zidentyfikuje 2 + 3 - 1 jako wyrażenie (proces identyfikacji wyrażenia ewoluuje, dopasowując inne reguły, ale punktem wyjścia jest reguła najwyższego poziomu).

Analizator składniowy typu bottom-up skanuje dane wejściowe, dopóki nie znajdzie dopasowania do reguły. Następnie zastąpi pasujące dane wejściowe regułą. Będzie to trwało do końca danych wejściowych. Częściowo dopasowane wyrażenie jest umieszczane na stosie parsera.

Nakładaj Dane wejściowe
2 + 3 - 1
hasło + 3 – 1
operacja na haśle 3 - 1
wyrażenie – 1
operacja wyrażenia 1
wyrażenie -

Ten typ parsera oddolnego jest nazywany parserem shift-reduce, ponieważ dane wejściowe są przesuwane w prawo (wyobraź sobie wskaźnik, który najpierw wskazuje początek danych wejściowych i przesuwa się w prawo) i stopniowo redukowane do reguł składniowych.

Automatyczne generowanie parserów

Istnieją narzędzia, które mogą wygenerować parser. Podajesz im gramatykę swojego języka – jego słownictwo i reguły składni – a one generują działający parser. Utworzenie parsera wymaga dogłębnego zrozumienia procesu analizowania składni i nie jest łatwo utworzyć zoptymalizowany parser ręcznie, dlatego generatory parserów mogą być bardzo przydatne.

WebKit używa 2 dobrze znanych generatorów parserów: Flex do tworzenia analizatora leksykalnego i Bison do tworzenia parsera (możesz się z nimi spotkać pod nazwami Lex i Yacc). Dane wejściowe Flex to plik zawierający definicje wyrażeń regularnych tokenów. Dane wejściowe Bisona to reguły składni języka w formacie BNF.

Parser HTML

Zadaniem parsera HTML jest przeanalizowanie znaczników HTML i utworzenie drzewa analizy.

Gramatyka HTML

Słownictwo i składnia HTML są zdefiniowane w specyfikacjach utworzonych przez organizację W3C.

Jak widzieliśmy we wprowadzeniu do analizy składniowej, składnię gramatyczną można zdefiniować formalnie przy użyciu formatów takich jak BNF.

Niestety wszystkie konwencjonalne tematy dotyczące parserów nie mają zastosowania do HTML (nie wspomniałem o nich tylko dla zabawy – będą używane do analizowania CSS i JavaScriptu). HTML nie można łatwo zdefiniować za pomocą gramatyki bezkontekstowej, której potrzebują parsery.

Istnieje formalny format definiowania HTML – DTD (Document Type Definition) – ale nie jest to gramatyka bezkontekstowa.

Na pierwszy rzut oka może to wyglądać dziwnie, ponieważ HTML jest dość zbliżony do XML. Dostępnych jest wiele parserów XML. Istnieje odmiana XML języka HTML – XHTML – jaka jest więc różnica?

Różnica polega na tym, że podejście HTML jest bardziej „wyrozumiałe”: pozwala pominąć niektóre tagi (które są następnie dodawane niejawnie) lub czasami pominąć tagi początkowe lub końcowe itd. Ogólnie rzecz biorąc, jest to „miękka” składnia, w przeciwieństwie do sztywnej i wymagającej składni XML.

Ten pozornie drobny szczegół robi ogromną różnicę. Z jednej strony jest to główny powód, dla którego HTML jest tak popularny: wybacza błędy i ułatwia życie twórcom stron internetowych. Z drugiej strony utrudnia to napisanie formalnej gramatyki. Podsumowując, konwencjonalne parsery nie mogą łatwo przeanalizować kodu HTML, ponieważ jego gramatyka nie jest bezkontekstowa. Parser XML nie może przeanalizować kodu HTML.

HTML DTD

Definicja HTML jest w formacie DTD. Ten format jest używany do definiowania języków z rodziny SGML. Format zawiera definicje wszystkich dozwolonych elementów, ich atrybutów i hierarchii. Jak widzieliśmy wcześniej, DTD HTML nie tworzy gramatyki bezkontekstowej.

Istnieje kilka wersji DTD. Tryb ścisły jest zgodny wyłącznie ze specyfikacjami, ale inne tryby obsługują znaczniki używane w przeszłości przez przeglądarki. Ma to na celu zapewnienie zgodności wstecznej ze starszymi treściami. Aktualną ścisłą definicję DTD znajdziesz tutaj: www.w3.org/TR/html4/strict.dtd

DOM

Drzewo wyjściowe („drzewo składniowe”) to drzewo elementów DOM i węzłów atrybutów. DOM to skrót od Document Object Model (obiektowy model dokumentu). Jest to obiektowa reprezentacja dokumentu HTML i interfejs elementów HTML dla świata zewnętrznego, np. JavaScriptu.

Korzeniem drzewa jest obiekt „Document”.

DOM ma relację niemal jeden do jednego ze znacznikami. Na przykład:

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

Te znaczniki zostaną przetłumaczone na to drzewo DOM:

Drzewo DOM przykładowych znaczników
Rysunek 8. Drzewo DOM przykładowych znaczników

Podobnie jak HTML, DOM jest określany przez organizację W3C. Więcej informacji znajdziesz na stronie www.w3.org/DOM/DOMTR. Jest to ogólna specyfikacja służąca do manipulowania dokumentami. Konkretny moduł opisuje elementy HTML. Definicje HTML znajdziesz tutaj: www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html.

Gdy mówię, że drzewo zawiera węzły DOM, mam na myśli, że drzewo jest zbudowane z elementów, które implementują jeden z interfejsów DOM. Przeglądarki używają konkretnych implementacji, które mają inne atrybuty używane przez przeglądarkę wewnętrznie.

Algorytm analizy

Jak widzieliśmy w poprzednich sekcjach, kodu HTML nie można analizować za pomocą zwykłych parserów typu „od góry do dołu” ani „od dołu do góry”.

Oto przyczyny:

  1. język jest bardzo elastyczny,
  2. Przeglądarki mają tradycyjną tolerancję na błędy, aby obsługiwać znane przypadki nieprawidłowego kodu HTML.
  3. Proces analizy jest wielobieżny. W przypadku innych języków źródło nie zmienia się podczas analizowania, ale w HTML kod dynamiczny (np. elementy skryptu zawierające wywołania document.write()) może dodawać dodatkowe tokeny, więc proces analizowania faktycznie modyfikuje dane wejściowe.

Ponieważ nie można używać zwykłych metod analizy, przeglądarki tworzą parsery niestandardowe do analizowania kodu HTML.

Algorytm analizy składniowej jest szczegółowo opisany w specyfikacji HTML5. Algorytm składa się z 2 etapów: tokenizacji i tworzenia drzewa.

Tokenizacja to analiza leksykalna, która polega na analizowaniu danych wejściowych i dzieleniu ich na tokeny. Tokeny HTML obejmują tagi początkowe, tagi końcowe, nazwy atrybutów i wartości atrybutów.

Tokenizer rozpoznaje token, przekazuje go do konstruktora drzewa i pobiera następny znak, aby rozpoznać kolejny token, i tak dalej, aż do końca danych wejściowych.

Proces analizy kodu HTML (ze specyfikacji HTML5)
Rysunek 9. Proces analizy składniowej kodu HTML (ze specyfikacji HTML5)

Algorytm tokenizacji

Wynikiem działania algorytmu jest token HTML. Algorytm jest wyrażony jako maszyna stanowa. Każdy stan wykorzystuje co najmniej 1 znak strumienia wejściowego i aktualizuje następny stan zgodnie z tymi znakami. Na decyzję wpływa bieżący stan tokenizacji i stan konstrukcji drzewa. Oznacza to, że ten sam znak będzie dawał różne wyniki dla prawidłowego następnego stanu, w zależności od bieżącego stanu. Algorytm jest zbyt złożony, aby można go było w pełni opisać, więc przyjrzyjmy się prostemu przykładowi, który pomoże nam zrozumieć zasadę jego działania.

Podstawowy przykład – tokenizacja tego kodu HTML:

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

Stan początkowy to „Stan danych”. Gdy napotkany zostanie znak <, stan zmieni się na „Stan otwarcia tagu”. Przetworzenie znaku a-z powoduje utworzenie „tokenu tagu otwierającego”, a stan zmienia się na „stan nazwy tagu”. Pozostajemy w tym stanie, dopóki nie zostanie wykorzystany znak >. Każdy znak jest dodawany do nowej nazwy tokena. W naszym przypadku utworzony token to token html.

Po osiągnięciu tagu > bieżący token jest emitowany, a stan zmienia się z powrotem na „Stan danych”. Tag <body> zostanie potraktowany w ten sam sposób. Do tej pory wyemitowano tagi html i body. Wracamy do „stanu danych”. Przetworzenie znaku H w Hello world spowoduje utworzenie i wyemitowanie tokena znaku. Będzie to trwało, dopóki nie zostanie osiągnięty < w </body>. Wygenerujemy token znaku dla każdego znaku Hello world.

Wracamy do stanu otwarcia tagu. Przetworzenie następnego znaku wejściowego / spowoduje utworzenie elementu end tag token i przejście do stanu „Nazwa tagu”. Pozostajemy w tym stanie, dopóki nie osiągniemy >.Następnie zostanie wygenerowany nowy token tagu i wrócimy do „stanu danych”. Dane wejściowe </html> będą traktowane tak samo jak w poprzednim przypadku.

Tokenizacja przykładowych danych wejściowych
Rysunek 10. Tokenizacja przykładowych danych wejściowych

Algorytm tworzenia drzewa

Podczas tworzenia parsera tworzony jest obiekt Document. Na etapie tworzenia drzewa drzewo DOM z dokumentem w korzeniu zostanie zmodyfikowane i zostaną do niego dodane elementy. Każdy węzeł wygenerowany przez tokenizator zostanie przetworzony przez konstruktor drzewa. Dla każdego tokena specyfikacja określa, który element DOM jest z nim powiązany i zostanie dla niego utworzony. Element jest dodawany do drzewa DOM, a także do stosu otwartych elementów. Ten stos służy do poprawiania niezgodności zagnieżdżania i niezamkniętych tagów. Algorytm jest też opisywany jako maszyna stanowa. Stany te są nazywane „trybami wstawiania”.

Zobaczmy proces tworzenia drzewa na przykładowych danych wejściowych:

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

Dane wejściowe etapu tworzenia drzewa to sekwencja tokenów z etapu tokenizacji. Pierwszy tryb to „tryb początkowy”. Otrzymanie tokena „html” spowoduje przejście do trybu „before html” i ponowne przetworzenie tokena w tym trybie. Spowoduje to utworzenie elementu HTMLHtmlElement, który zostanie dołączony do głównego obiektu Document.

Stan zmieni się na „Przed nagłówkiem”. Następnie otrzymywany jest token „body”. Element HTMLHeadElement zostanie utworzony niejawnie, mimo że nie mamy tokena „head”, i zostanie dodany do drzewa.

Teraz przechodzimy do trybu „w nagłówku”, a następnie do trybu „po nagłówku”. Token treści jest ponownie przetwarzany, tworzony i wstawiany jest element HTMLBodyElement, a tryb jest zmieniany na „w treści”.

Tokeny znaków ciągu „Hello world” zostały odebrane. Pierwszy spowoduje utworzenie i wstawienie węzła „Text”, a pozostałe znaki zostaną do niego dołączone.

Odebranie tokena końca treści spowoduje przejście do trybu „po treści”. Teraz otrzymamy tag końcowy html, który przeniesie nas do trybu „after after body”. Otrzymanie tokena końca pliku spowoduje zakończenie analizy.

Struktura drzewa przykładowego kodu HTML.
Rysunek 11. Konstrukcja drzewa przykładowego kodu HTML

Działania po zakończeniu analizy

Na tym etapie przeglądarka oznaczy dokument jako interaktywny i rozpocznie analizowanie skryptów, które są w trybie „odroczonym”, czyli takich, które powinny zostać wykonane po przeanalizowaniu dokumentu. Stan dokumentu zostanie ustawiony na „complete” i zostanie wywołane zdarzenie „load”.

Pełne algorytmy tokenizacji i konstrukcji drzewa znajdziesz w specyfikacji HTML5.

Tolerancja błędów w przeglądarkach

Na stronie HTML nigdy nie występuje błąd „Nieprawidłowa składnia”. Przeglądarki naprawiają nieprawidłowe treści i kontynuują działanie.

Weźmy na przykład ten kod HTML:

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

Prawdopodobnie naruszono około miliona reguł („mytag” nie jest standardowym tagiem, nieprawidłowe zagnieżdżenie elementów „p” i „div” itd.), ale przeglądarka nadal wyświetla go prawidłowo i nie zgłasza błędów. Dlatego duża część kodu parsera służy do poprawiania błędów autorów kodu HTML.

Obsługa błędów jest dość spójna w przeglądarkach, ale nie była częścią specyfikacji HTML. Podobnie jak dodawanie zakładek i przyciski wstecz/dalej, jest to funkcja, która rozwijała się w przeglądarkach na przestrzeni lat. Istnieją znane nieprawidłowe konstrukcje HTML, które powtarzają się w wielu witrynach, a przeglądarki próbują je naprawić w sposób zgodny z innymi przeglądarkami.

Specyfikacja HTML5 określa niektóre z tych wymagań. (WebKit podsumowuje to ładnie w komentarzu na początku klasy parsera HTML).

Parser analizuje dane wejściowe podzielone na tokeny i tworzy drzewo dokumentu. Jeśli dokument jest poprawnie sformatowany, jego analiza jest prosta.

Niestety musimy obsługiwać wiele dokumentów HTML, które nie są prawidłowo sformatowane, więc parser musi być tolerancyjny wobec błędów.

Musimy obsłużyć co najmniej te błędy:

  1. Dodawany element jest bezpośrednio zabroniony w niektórych tagach zewnętrznych. W takim przypadku należy zamknąć wszystkie tagi aż do tego, który zabrania użycia elementu, i dodać go po nim.
  2. Nie możemy dodać elementu bezpośrednio. Możliwe, że osoba, która napisała dokument, zapomniała o jakimś tagu (lub że tag jest opcjonalny). Może się tak zdarzyć w przypadku tych tagów: HTML, HEAD, BODY, TBODY, TR, TD, LI (czy o jakimś zapomniałem?).
  3. Chcemy dodać element blokowy wewnątrz elementu wbudowanego. Zamknij wszystkie elementy wbudowane aż do następnego elementu blokowego wyższego poziomu.
  4. Jeśli to nie pomoże, zamknij elementy, aż będziemy mogli dodać element, lub zignoruj tag.

Oto kilka przykładów tolerancji błędów w WebKit:

</br> zamiast <br>

Niektóre witryny używają </br> zamiast <br>. Aby zachować zgodność z IE i Firefoksem, WebKit traktuje to jak <br>.

Kod:

if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
     reportError(MalformedBRError);
     t->beginTag = true;
}

Pamiętaj, że obsługa błędów jest wewnętrzna – nie będzie widoczna dla użytkownika.

stół,

Tabela osierocona to tabela znajdująca się w innej tabeli, ale nie w komórce tabeli.

Na przykład:

<table>
  <table>
    <tr><td>inner table</td></tr>
  </table>
  <tr><td>outer table</td></tr>
</table>

WebKit zmieni hierarchię na 2 tabele na tym samym poziomie:

<table>
  <tr><td>outer table</td></tr>
</table>
<table>
  <tr><td>inner table</td></tr>
</table>

Kod:

if (m_inStrayTableContent && localName == tableTag)
        popBlock(tableTag);

WebKit używa stosu dla zawartości bieżącego elementu: wyskakuje on z tabeli wewnętrznej ze stosu tabeli zewnętrznej. Tabele będą teraz elementami równorzędnymi.

Zagnieżdżone elementy formularza

Jeśli użytkownik umieści formularz w innym formularzu, drugi formularz zostanie zignorowany.

Kod:

if (!m_currentFormElement) {
        m_currentFormElement = new HTMLFormElement(formTag,    m_document);
}

zbyt głęboka hierarchia tagów,

Komentarz mówi sam za siebie.

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

Nieprawidłowo umieszczone tagi końcowe html lub body

Komentarz mówi sam za siebie.

if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;

Dlatego autorzy stron internetowych powinni uważać, aby nie stać się przykładem we fragmencie kodu WebKit dotyczącym tolerancji błędów, i pisać poprawny kod HTML.

Analiza CSS

Pamiętasz pojęcia związane z analizą składniową, które zostały omówione we wprowadzeniu? W przeciwieństwie do HTML, CSS jest gramatyką bezkontekstową i można ją analizować za pomocą parserów opisanych we wprowadzeniu. W rzeczywistości specyfikacja CSS definiuje gramatykę leksykalną i składniową CSS.

Oto kilka przykładów:

Gramatyka leksykalna (słownictwo) jest definiowana przez wyrażenia regularne dla każdego tokena:

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” to skrót od identyfikatora, np. nazwy klasy. „name” to identyfikator elementu (do którego odwołuje się „#”).

Gramatyka składni jest opisana w 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*] ')' ]
  ;

Wyjaśnienie:

Zbiór reguł ma taką strukturę:

div.error, a.error {
  color:red;
  font-weight:bold;
}

div.error i a.error to selektory. Część w nawiasach klamrowych zawiera reguły stosowane przez ten zestaw reguł. Ta struktura jest formalnie zdefiniowana w tej definicji:

ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;

Oznacza to, że zestaw reguł to selektor lub opcjonalnie kilka selektorów rozdzielonych przecinkami i spacjami (S oznacza spację). Zestaw reguł zawiera nawiasy klamrowe, a w nich deklarację lub opcjonalnie kilka deklaracji rozdzielonych średnikami. „declaration” i „selector” zostaną zdefiniowane w poniższych definicjach BNF.

Parser CSS WebKit

WebKit używa generatorów parserów Flex i Bison do automatycznego tworzenia parserów z plików gramatyki CSS. Jak pamiętasz z wprowadzenia do parsera, Bison tworzy parser typu shift-reduce, który działa od dołu do góry. Firefox używa parsera typu top-down, który został napisany ręcznie. W obu przypadkach każdy plik CSS jest analizowany i przekształcany w obiekt StyleSheet. Każdy obiekt zawiera reguły CSS. Obiekty reguł CSS zawierają obiekty selektorów i deklaracji oraz inne obiekty odpowiadające gramatyce CSS.

Analizowanie CSS.
Rysunek 12. Analizowanie kodu CSS

Kolejność przetwarzania skryptów i arkuszy stylów

Skrypty

Model sieci jest synchroniczny. Autorzy oczekują, że skrypty zostaną przeanalizowane i wykonane natychmiast, gdy parser dotrze do tagu <script>. Analiza dokumentu jest wstrzymywana do czasu wykonania skryptu. Jeśli skrypt jest zewnętrzny, zasób musi najpierw zostać pobrany z sieci – odbywa się to również synchronicznie, a analiza jest wstrzymywana do momentu pobrania zasobu. Ten model był używany przez wiele lat i jest też określony w specyfikacjach HTML4 i 5. Autorzy mogą dodać do skryptu atrybut „defer”, dzięki czemu nie będzie on zatrzymywać analizowania dokumentu i zostanie wykonany po jego przeanalizowaniu. HTML5 dodaje opcję oznaczania skryptu jako asynchronicznego, dzięki czemu będzie on analizowany i wykonywany przez inny wątek.

Analiza spekulatywna

Zarówno WebKit, jak i Firefox stosują tę optymalizację. Podczas wykonywania skryptów inny wątek analizuje resztę dokumentu i sprawdza, jakie inne zasoby muszą zostać załadowane z sieci, a następnie je ładuje. Dzięki temu zasoby mogą być ładowane przez połączenia równoległe, co zwiększa ogólną szybkość. Uwaga: parser spekulacyjny analizuje tylko odwołania do zasobów zewnętrznych, takich jak zewnętrzne skrypty, arkusze stylów i obrazy. Nie modyfikuje drzewa DOM – to zadanie należy do głównego parsera.

Arkusze stylów

Arkusze stylów mają natomiast inny model. Teoretycznie, ponieważ arkusze stylów nie zmieniają drzewa DOM, nie ma powodu, aby na nie czekać i zatrzymywać analizowanie dokumentu. Problem polega jednak na tym, że skrypty proszą o informacje o stylu na etapie analizowania dokumentu. Jeśli styl nie został jeszcze wczytany i przeanalizowany, skrypt otrzyma nieprawidłowe odpowiedzi, co może powodować wiele problemów. Wydaje się, że to rzadki przypadek, ale jest on dość powszechny. Firefox blokuje wszystkie skrypty, gdy arkusz stylów jest nadal wczytywany i analizowany. WebKit blokuje skrypty tylko wtedy, gdy próbują uzyskać dostęp do określonych właściwości stylu, na które mogą mieć wpływ niezaładowane arkusze stylów.

Tworzenie drzewa renderowania

Podczas tworzenia drzewa DOM przeglądarka tworzy inne drzewo – drzewo renderowania. To drzewo elementów wizualnych w kolejności, w jakiej będą wyświetlane. Jest to wizualna reprezentacja dokumentu. Celem tego drzewa jest umożliwienie renderowania treści we właściwej kolejności.

Firefox nazywa elementy w drzewie renderowania „ramkami”. WebKit używa terminu „renderer” lub „obiekt renderowania”.

Renderer wie, jak rozmieścić i wyświetlić siebie i swoje elementy podrzędne.

Klasa RenderObject w WebKit, czyli klasa bazowa modułów renderujących, ma następującą definicję:

class RenderObject{
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  //the DOM node
  RenderStyle* style;  // the computed style
  RenderLayer* containgLayer; //the containing z-index layer
}

Każdy moduł renderujący reprezentuje prostokątny obszar, który zwykle odpowiada polu CSS węzła, zgodnie ze specyfikacją CSS2. Zawiera on informacje geometryczne, takie jak szerokość, wysokość i pozycja.

Na typ pola wpływa wartość „display” atrybutu stylu, który jest powiązany z węzłem (patrz sekcja Obliczanie stylu). Oto kod WebKit, który decyduje o tym, jaki typ mechanizmu renderowania powinien zostać utworzony dla węzła DOM, zgodnie z atrybutem 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;
}

Uwzględniany jest też typ elementu: na przykład elementy sterujące formularza i tabele mają specjalne ramki.

Jeśli element chce utworzyć specjalny moduł renderowania w WebKit, zastępuje metodę createRenderer(). Moduły renderujące wskazują obiekty stylu, które zawierają informacje niegeometryczne.

Relacja drzewa renderowania do drzewa DOM

Obiekty renderujące odpowiadają elementom DOM, ale nie jest to relacja jeden do jednego. Niewizualne elementy DOM nie zostaną wstawione do drzewa renderowania. Przykładem jest element „head”. W drzewie nie pojawią się też elementy, których wartość wyświetlania została ustawiona na „none” (natomiast elementy z widocznością „hidden” będą widoczne w drzewie).

Istnieją elementy DOM, które odpowiadają kilku obiektom wizualnym. Zwykle są to elementy o złożonej strukturze, których nie można opisać za pomocą pojedynczego prostokąta. Na przykład element „select” ma 3 renderery: jeden dla obszaru wyświetlania, jeden dla pola listy rozwijanej i jeden dla przycisku. Jeśli tekst jest podzielony na kilka wierszy, ponieważ szerokość jest niewystarczająca dla jednego wiersza, nowe wiersze zostaną dodane jako dodatkowe moduły renderujące.

Innym przykładem wielu modułów renderujących jest uszkodzony kod HTML. Zgodnie ze specyfikacją CSS element wbudowany musi zawierać tylko elementy blokowe lub tylko elementy wbudowane. W przypadku treści mieszanych zostaną utworzone anonimowe moduły renderowania bloków, które będą zawierać elementy wbudowane.

Niektóre obiekty renderowania odpowiadają węzłowi DOM, ale nie w tym samym miejscu w drzewie. Elementy pływające i pozycjonowane bezwzględnie są poza przepływem, umieszczone w innej części drzewa i odwzorowane na rzeczywistą ramkę. W miejscu, gdzie powinny się znajdować, jest ramka zastępcza.

Drzewo renderowania i odpowiadające mu drzewo DOM.
Rysunek 13. Drzewo renderowania i odpowiadające mu drzewo DOM. „Widoczny obszar” to początkowy blok zawierający. W WebKit będzie to obiekt „RenderView”.

Proces tworzenia drzewa

W Firefoksie prezentacja jest zarejestrowana jako detektor aktualizacji DOM. Prezentacja przekazuje tworzenie ramki do FrameConstructor, a konstruktor rozwiązuje styl (patrz obliczanie stylu) i tworzy ramkę.

W WebKit proces rozwiązywania stylu i tworzenia mechanizmu renderowania jest nazywany „załącznikiem”. Każdy węzeł DOM ma metodę „attach”. Dołączenie jest synchroniczne, wstawienie węzła do drzewa DOM wywołuje metodę „attach” nowego węzła.

Przetwarzanie tagów html i body powoduje utworzenie korzenia drzewa renderowania. Główny obiekt renderowania odpowiada temu, co specyfikacja CSS nazywa blokiem zawierającym: najwyższym blokiem, który zawiera wszystkie inne bloki. Jego wymiary to widoczny obszar: wymiary obszaru wyświetlania okna przeglądarki. W Firefoxie jest to ViewPortFrame, a w WebKit – RenderView. Jest to obiekt renderowania, na który wskazuje dokument. Pozostała część drzewa jest tworzona jako wstawienie węzłów DOM.

Zobacz specyfikację CSS2 dotyczącą modelu przetwarzania.

Obliczanie stylu

Tworzenie drzewa renderowania wymaga obliczenia właściwości wizualnych każdego obiektu renderowania. W tym celu oblicza właściwości stylu każdego elementu.

Styl obejmuje arkusze stylów z różnych źródeł, elementy stylu wbudowanego i właściwości wizualne w kodzie HTML (np. właściwość „bgcolor”). Te ostatnie są przekształcane w odpowiednie właściwości stylu CSS.

Arkusze stylów pochodzą z domyślnych arkuszy stylów przeglądarki, arkuszy stylów dostarczonych przez autora strony i arkuszy stylów użytkownika – są to arkusze stylów dostarczone przez użytkownika przeglądarki (przeglądarki umożliwiają definiowanie ulubionych stylów). Na przykład w Firefoksie można to zrobić, umieszczając arkusz stylów w folderze „Firefox Profile”).

Obliczanie stylu wiąże się z kilkoma trudnościami:

  1. Dane stylu to bardzo duża konstrukcja, która zawiera wiele właściwości stylu, co może powodować problemy z pamięcią.
  2. Znajdowanie pasujących reguł dla każdego elementu może powodować problemy z wydajnością, jeśli nie jest zoptymalizowane. Przechodzenie przez całą listę reguł dla każdego elementu w celu znalezienia dopasowań jest bardzo obciążające. Selektory mogą mieć złożoną strukturę, która może spowodować, że proces dopasowywania rozpocznie się na pozornie obiecującej ścieżce, która okaże się bezużyteczna, i trzeba będzie wypróbować inną ścieżkę.

    Na przykład ten selektor złożony:

    div div div div{
    ...
    }
    

    Oznacza to, że reguły mają zastosowanie do elementu <div>, który jest elementem podrzędnym 3 elementów div. Załóżmy, że chcesz sprawdzić, czy reguła ma zastosowanie do danego elementu <div>. Wybierasz określoną ścieżkę w drzewie, aby ją sprawdzić. Może być konieczne przejście w górę drzewa węzłów, aby dowiedzieć się, że istnieją tylko 2 elementy div i reguła nie ma zastosowania. Następnie musisz wypróbować inne ścieżki w drzewie.

  3. Stosowanie reguł wiąże się z dość skomplikowanymi regułami kaskadowymi, które określają hierarchię reguł.

Zobaczmy, jak przeglądarki radzą sobie z tymi problemami:

Udostępnianie danych o stylu

Węzły WebKit odwołują się do obiektów stylu (RenderStyle). W niektórych warunkach te obiekty mogą być udostępniane przez węzły. Węzły są elementami równorzędnymi lub kuzynami i:

  1. Elementy muszą być w tym samym stanie myszy (np.jeden nie może być w stanie :hover, gdy drugi nie jest).
  2. Żaden element nie powinien mieć identyfikatora
  3. Nazwy tagów powinny być zgodne.
  4. Atrybuty klasy powinny być zgodne
  5. Zestaw zmapowanych atrybutów musi być identyczny.
  6. Stany połączeń muszą być zgodne
  7. Stany aktywne muszą być takie same.
  8. Żaden z elementów nie powinien być objęty selektorami atrybutów, przy czym „objęty” oznacza, że pasuje do niego dowolny selektor, który używa selektora atrybutu w dowolnej pozycji.
  9. Elementy nie mogą mieć atrybutu stylu w tekście.
  10. Nie można używać żadnych selektorów elementów nadrzędnych. WebCore po prostu rzuca globalny przełącznik, gdy napotka dowolny selektor rodzeństwa, i wyłącza udostępnianie stylów dla całego dokumentu, gdy są one obecne. Obejmuje to selektor + oraz selektory takie jak :first-child i :last-child.

Drzewo reguł Firefoksa

Firefox ma 2 dodatkowe drzewa ułatwiające obliczanie stylów: drzewo reguł i drzewo kontekstu stylu. WebKit również ma obiekty stylu, ale nie są one przechowywane w drzewie takim jak drzewo kontekstu stylu. Tylko węzeł DOM wskazuje odpowiedni styl.

Drzewo kontekstu w stylu Firefoksa.
Rysunek 14. Drzewo kontekstu stylu w przeglądarce Firefox.

Konteksty stylu zawierają wartości końcowe. Wartości są obliczane przez zastosowanie wszystkich pasujących reguł w odpowiedniej kolejności i wykonanie operacji, które przekształcają je z wartości logicznych na konkretne. Jeśli na przykład wartość logiczna jest procentem ekranu, zostanie obliczona i przekształcona na jednostki bezwzględne. Pomysł z drzewem reguł jest naprawdę sprytny. Umożliwia udostępnianie tych wartości między węzłami, aby uniknąć ich ponownego obliczania. Pozwala to też zaoszczędzić miejsce.

Wszystkie dopasowane reguły są przechowywane w drzewie. Węzły na dole ścieżki mają wyższy priorytet. Drzewo zawiera wszystkie ścieżki znalezionych dopasowań reguł. Reguły są przechowywane w sposób leniwy. Drzewo nie jest obliczane na początku dla każdego węzła, ale gdy trzeba obliczyć styl węzła, obliczone ścieżki są dodawane do drzewa.

Chodzi o to, aby ścieżki drzewa były traktowane jak słowa w leksykonie. Załóżmy, że obliczyliśmy już to drzewo reguł:

Drzewo wyliczonych reguł
Rysunek 15. Obliczone drzewo reguł.

Załóżmy, że musimy dopasować reguły do innego elementu w drzewie treści i okazuje się, że dopasowane reguły (w prawidłowej kolejności) to B-E-I. Ta ścieżka jest już w drzewie, ponieważ obliczyliśmy już ścieżkę A-B-E-I-L. Teraz będziemy mieć mniej pracy.

Zobaczmy, jak drzewo ułatwia nam pracę.

Podział na struktury

Konteksty stylów są podzielone na struktury. Te struktury zawierają informacje o stylu dla określonej kategorii, np. obramowania lub koloru. Wszystkie właściwości w strukturze są dziedziczone lub niedziedziczone. Właściwości dziedziczone to właściwości, które są dziedziczone z elementu nadrzędnego, chyba że zostały zdefiniowane przez element. Właściwości niedziedziczone (nazywane właściwościami „reset”) używają wartości domyślnych, jeśli nie zostały zdefiniowane.

Drzewo pomaga nam, zapisując w pamięci podręcznej całe struktury (zawierające obliczone wartości końcowe). Chodzi o to, że jeśli węzeł dolny nie dostarczył definicji struktury, można użyć struktury z pamięci podręcznej w węźle górnym.

Obliczanie kontekstów stylu za pomocą drzewa reguł

Podczas obliczania kontekstu stylu dla określonego elementu najpierw obliczamy ścieżkę w drzewie reguł lub używamy istniejącej. Następnie zaczynamy stosować reguły w ścieżce, aby wypełnić struktury w naszym nowym kontekście stylu. Zaczynamy od dolnego węzła ścieżki – tego o najwyższym priorytecie (zwykle najbardziej szczegółowego selektora) – i przechodzimy w górę drzewa, aż struktura zostanie wypełniona. Jeśli w tym węźle reguły nie ma specyfikacji struktury, możemy ją znacznie zoptymalizować – przechodzimy w górę drzewa, aż znajdziemy węzeł, który w pełni ją określa, i wskazujemy go. To najlepsza optymalizacja – cała struktura jest udostępniana. Pozwala to zaoszczędzić pamięć i obliczenia wartości końcowych.

Jeśli znajdziemy częściowe definicje, przechodzimy w górę drzewa, aż struktura zostanie wypełniona.

Jeśli nie znajdziemy żadnych definicji dla naszej struktury, to w przypadku, gdy struktura jest typu „dziedziczonego”, wskazujemy strukturę nadrzędną w drzewie kontekstu. W tym przypadku udało nam się też udostępnić struktury. Jeśli jest to struktura resetowania, zostaną użyte wartości domyślne.

Jeśli najbardziej szczegółowy węzeł dodaje wartości, musimy wykonać dodatkowe obliczenia, aby przekształcić je w rzeczywiste wartości. Następnie zapisujemy wynik w pamięci podręcznej w węźle drzewa, aby mogły go używać elementy podrzędne.

Jeśli element ma element równorzędny, który wskazuje ten sam węzeł drzewa, mogą one współdzielić cały kontekst stylu.

Oto przykład: załóżmy, że mamy taki kod 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>

Oraz te reguły:

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

Dla uproszczenia załóżmy, że musimy wypełnić tylko 2 struktury: strukturę koloru i strukturę marginesu. Struktura koloru zawiera tylko 1 element: kolor. Struktura marginesu zawiera 4 boki.

Wynikowe drzewo reguł będzie wyglądać tak (węzły są oznaczone nazwą węzła: numerem reguły, na którą wskazują):

drzewo reguł,
Rysunek 16. Drzewo reguł

Drzewo kontekstu będzie wyglądać tak (nazwa węzła: węzeł reguły, na który wskazuje):

Drzewo kontekstu.
Rysunek 17. Drzewo kontekstu

Załóżmy, że analizujemy kod HTML i docieramy do drugiego tagu <div>. Musimy utworzyć kontekst stylu dla tego węzła i wypełnić jego struktury stylu.

Dopasujemy reguły i odkryjemy, że reguły dopasowania dla <div> to 1, 2 i 6. Oznacza to, że w drzewie istnieje już ścieżka, której może użyć nasz element, i musimy tylko dodać do niej kolejny węzeł dla reguły 6 (węzeł F w drzewie reguł).

Utworzymy kontekst stylu i umieścimy go w drzewie kontekstu. Nowy kontekst stylu będzie wskazywać węzeł F w drzewie reguł.

Teraz musimy wypełnić struktury stylu. Zaczniemy od wypełnienia struktury marginesu. Ostatni węzeł reguły (F) nie dodaje nic do struktury marginesu, więc możemy przejść w górę drzewa, aż znajdziemy strukturę zapisaną w pamięci podręcznej, która została obliczona podczas poprzedniego wstawiania węzła, i jej użyć. Znajdziemy go w węźle B, który jest najwyższym węzłem z określonymi regułami marży.

Mamy definicję elementu struct koloru, więc nie możemy użyć elementu struct z pamięci podręcznej. Ponieważ kolor ma 1 atrybut, nie musimy przechodzić w górę drzewa, aby wypełnić inne atrybuty. Obliczymy wartość końcową (przekonwertujemy ciąg znaków na RGB itp.) i zapiszemy obliczoną strukturę w pamięci podręcznej w tym węźle.

Praca nad drugim elementem <span> jest jeszcze łatwiejsza. Dopasujemy reguły i dojdziemy do wniosku, że wskazuje ona na regułę G, podobnie jak poprzedni zakres. Ponieważ mamy elementy równorzędne, które wskazują ten sam węzeł, możemy udostępnić cały kontekst stylu i po prostu wskazać kontekst poprzedniego elementu span.

W przypadku struktur zawierających reguły odziedziczone po elemencie nadrzędnym buforowanie odbywa się w drzewie kontekstu (właściwość koloru jest dziedziczona, ale Firefox traktuje ją jako zresetowaną i buforuje w drzewie reguł).

Jeśli na przykład dodamy reguły dotyczące czcionek w akapicie:

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

Następnie element akapitu, który jest elementem podrzędnym elementu div w drzewie kontekstu, mógłby mieć tę samą strukturę czcionki co element nadrzędny. Dzieje się tak, jeśli dla akapitu nie określono żadnych reguł dotyczących czcionki.

W przypadku WebKit, który nie ma drzewa reguł, dopasowane deklaracje są przeglądane 4 razy. Najpierw stosowane są właściwości o wysokim priorytecie, które nie są ważne (właściwości, które powinny być stosowane jako pierwsze, ponieważ inne od nich zależą, np. display), następnie właściwości o wysokim priorytecie, które są ważne, potem właściwości o normalnym priorytecie, które nie są ważne, a na końcu właściwości o normalnym priorytecie, które są ważne. Oznacza to, że właściwości, które pojawiają się wielokrotnie, zostaną rozwiązane zgodnie z prawidłową kolejnością kaskadową. Ostatni wygrywa.

Podsumowując: udostępnianie obiektów stylu (całkowicie lub niektórych struktur w nich) rozwiązuje problemy 1 i 3. Drzewo reguł Firefoksa pomaga też stosować właściwości we właściwej kolejności.

Manipulowanie regułami w celu ułatwienia dopasowania

Istnieje kilka źródeł reguł stylu:

  1. reguły CSS w zewnętrznych arkuszach stylów lub w elementach stylu.css p {color: blue}
  2. Atrybuty stylu elementu w tekście, takie jak html <p style="color: blue" />
  3. Atrybuty wizualne HTML (które są mapowane na odpowiednie reguły stylu) html <p bgcolor="blue" /> Ostatnie 2 można łatwo dopasować do elementu, ponieważ zawiera on atrybuty stylu, a atrybuty HTML można mapować, używając elementu jako klucza.

Jak wspomnieliśmy wcześniej w przypadku problemu 2, dopasowywanie reguł CSS może być trudniejsze. Aby rozwiązać ten problem, reguły są modyfikowane w celu ułatwienia dostępu.

Po przeanalizowaniu arkusza stylów reguły są dodawane do jednej z kilku map mieszających zgodnie z selektorem. Dostępne są mapy według identyfikatora, nazwy klasy i nazwy tagu oraz ogólna mapa dla wszystkich elementów, które nie pasują do tych kategorii. Jeśli selektor jest identyfikatorem, reguła zostanie dodana do mapy identyfikatorów, jeśli jest klasą, zostanie dodana do mapy klas itd.

Ta manipulacja znacznie ułatwia dopasowywanie reguł. Nie trzeba sprawdzać każdej deklaracji: możemy wyodrębnić odpowiednie reguły dla elementu z map. Ta optymalizacja eliminuje ponad 95% reguł, dzięki czemu nie trzeba ich nawet brać pod uwagę podczas procesu dopasowywania(4.1).

Przyjrzyjmy się na przykład tym regułom stylu:

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

Pierwsza reguła zostanie wstawiona do mapy klas. Drugi do mapy identyfikatorów, a trzeci do mapy tagów.

W przypadku tego fragmentu kodu HTML:

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

Najpierw spróbujemy znaleźć reguły dla elementu p. Mapa klas będzie zawierać klucz „error”, pod którym znajduje się reguła dla „p.error”. Element div będzie miał odpowiednie reguły w mapie identyfikatorów (kluczem jest identyfikator) i mapie tagów. Pozostaje tylko sprawdzić, które z reguł wyodrębnionych przez klucze są rzeczywiście zgodne.

Jeśli na przykład reguła dla elementu div wyglądała tak:

table div {margin: 5px}

Nadal zostanie wyodrębniony z mapy tagów, ponieważ klucz jest selektorem znajdującym się najbardziej po prawej stronie, ale nie będzie pasować do naszego elementu div, który nie ma elementu nadrzędnego table.

Zarówno WebKit, jak i Firefox wykonują tę operację.

Kolejność kaskadowa arkusza stylów

Obiekt stylu ma właściwości odpowiadające każdemu atrybutowi wizualnemu (wszystkie atrybuty CSS, ale bardziej ogólne). Jeśli właściwość nie jest zdefiniowana przez żadną z pasujących reguł, niektóre właściwości mogą zostać odziedziczone przez obiekt stylu elementu nadrzędnego. Inne właściwości mają wartości domyślne.

Problem pojawia się, gdy istnieje więcej niż jedna definicja – wtedy rozwiązaniem jest kolejność kaskadowa.

Deklaracja właściwości stylu może występować w kilku arkuszach stylów i kilka razy w jednym arkuszu. Oznacza to, że kolejność stosowania reguł jest bardzo ważna. Jest to tzw. kolejność kaskadowa. Zgodnie ze specyfikacją CSS2 kolejność kaskadowa jest następująca (od najniższej do najwyższej):

  1. Deklaracje przeglądarki
  2. Deklaracje dotyczące normalnego użytkowania
  3. Deklaracje autora dotyczące normalnego oświetlenia
  4. Deklaracje ważne dla autorów
  5. Ważne deklaracje użytkownika

Deklaracje przeglądarki są najmniej ważne, a użytkownik zastępuje autora tylko wtedy, gdy deklaracja została oznaczona jako ważna. Deklaracje o tej samej kolejności będą sortowane według specyficzności, a następnie według kolejności, w jakiej zostały określone. Atrybuty wizualne HTML są przekształcane w odpowiadające im deklaracje CSS . Są one traktowane jako reguły autora o niskim priorytecie.

Zgodność ze specyfikacją

Specyficzność selektora jest określona w specyfikacji CSS2 w następujący sposób:

  1. 1, jeśli deklaracja pochodzi z atrybutu „style”, a nie z reguły z selektorem, w przeciwnym razie 0 (= a);
  2. policz liczbę atrybutów ID w selektorze (= b),
  3. policz liczbę innych atrybutów i pseudoklas w selektorze (= c)
  4. policz liczbę nazw elementów i pseudoelementów w selektorze (= d).

Konkatenacja 4 liczb a-b-c-d (w systemie liczbowym o dużej podstawie) daje specyfikę.

Podstawa liczbowa, której musisz użyć, jest określana przez najwyższą liczbę w jednej z kategorii.

Jeśli na przykład a=14, możesz użyć systemu szesnastkowego. W mało prawdopodobnym przypadku, gdy a=17, potrzebna będzie baza liczbowa składająca się z 17 cyfr. Ta druga sytuacja może wystąpić w przypadku takiego selektora: html body div div p… (17 tagów w selektorze… mało prawdopodobne).

Oto kilka przykładów:

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

Sortowanie reguł

Po dopasowaniu reguł są one sortowane zgodnie z regułami kaskadowymi. WebKit używa sortowania bąbelkowego w przypadku małych list i sortowania przez scalanie w przypadku dużych. WebKit implementuje sortowanie, zastępując operator > dla reguł:

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

Stopniowy proces

WebKit używa flagi, która wskazuje, czy wszystkie arkusze stylów najwyższego poziomu (w tym @imports) zostały wczytane. Jeśli styl nie zostanie w pełni wczytany podczas dołączania, zostaną użyte symbole zastępcze i zostanie to oznaczone w dokumencie. Zostaną one ponownie obliczone po wczytaniu arkuszy stylów.

Układ

Gdy moduł renderujący jest tworzony i dodawany do drzewa, nie ma pozycji ani rozmiaru. Obliczanie tych wartości nazywa się układem lub ponownym przepływem.

HTML używa modelu układu opartego na przepływie, co oznacza, że w większości przypadków można obliczyć geometrię w jednym przebiegu. Elementy, które znajdują się później „w przepływie”, zwykle nie wpływają na geometrię elementów, które znajdują się wcześniej „w przepływie”, więc układ może być wyświetlany od lewej do prawej i od góry do dołu dokumentu. Istnieją wyjątki: na przykład tabele HTML mogą wymagać więcej niż jednego przejścia.

Układ współrzędnych jest określany względem ramki głównej. Używane są współrzędne górne i lewe.

Układ jest procesem rekurencyjnym. Zaczyna się od mechanizmu renderowania głównego, który odpowiada elementowi <html> dokumentu HTML. Układ jest kontynuowany rekurencyjnie przez część lub całą hierarchię ramek, obliczając informacje geometryczne dla każdego modułu renderowania, który ich wymaga.

Pozycja renderera głównego to 0,0, a jego wymiary to widoczny obszar – widoczna część okna przeglądarki.

Wszystkie mechanizmy renderowania mają metodę „layout” lub „reflow”. Każdy mechanizm renderowania wywołuje metodę layout swoich elementów podrzędnych, które wymagają układu.

System bitów modyfikacji

Aby nie wykonywać pełnego układu przy każdej drobnej zmianie, przeglądarki używają systemu „dirty bit”. Zmieniony lub dodany moduł renderujący oznacza siebie i swoje elementy podrzędne jako „dirty”, czyli wymagające układu.

Są 2 flagi: „dirty” i „children are dirty”, co oznacza, że chociaż sam moduł renderujący może być w porządku, ma co najmniej 1 element podrzędny, który wymaga układu.

Układ globalny i przyrostowy

Układ może być wywoływany na całym drzewie renderowania – jest to układ „globalny”. Może się tak zdarzyć, jeśli:

  1. Globalna zmiana stylu, która wpływa na wszystkie mechanizmy renderowania, np. zmiana rozmiaru czcionki.
  2. W wyniku zmiany rozmiaru ekranu

Układ może być przyrostowy, układane będą tylko nieprawidłowe mechanizmy renderowania (może to spowodować pewne uszkodzenia, które będą wymagać dodatkowych układów).

Układ przyrostowy jest aktywowany (asynchronicznie), gdy renderery są nieaktualne. Na przykład gdy do drzewa renderowania zostaną dołączone nowe mechanizmy renderowania po tym, jak dodatkowe treści zostaną pobrane z sieci i dodane do drzewa DOM.

Układ przyrostowy.
Ilustracja 18. Układ przyrostowy – układane są tylko nieaktualne mechanizmy renderowania i ich elementy podrzędne

Układ asynchroniczny i synchroniczny

Układ przyrostowy jest tworzony asynchronicznie. Firefox kolejkuje „polecenia ponownego przepływu” w przypadku układów przyrostowych, a harmonogram uruchamia zbiorcze wykonywanie tych poleceń. WebKit ma też minutnik, który wykonuje układ przyrostowy – drzewo jest przeszukiwane, a „nieaktualne” mechanizmy renderowania są układane.

Skrypty, które proszą o informacje o stylu, takie jak „offsetHeight”, mogą wywoływać przyrostowy układ synchronicznie.

Układ globalny jest zwykle wywoływany synchronicznie.

Czasami układ jest wywoływany jako wywołanie zwrotne po początkowym układzie, ponieważ niektóre atrybuty, takie jak pozycja przewijania, uległy zmianie.

Optymalizacje

Gdy układ jest wywoływany przez „zmianę rozmiaru” lub zmianę pozycji renderowania(a nie rozmiaru), rozmiary renderowania są pobierane z pamięci podręcznej i nie są ponownie obliczane…

W niektórych przypadkach modyfikowane jest tylko poddrzewo, a układ nie zaczyna się od korzenia. Może się tak zdarzyć, gdy zmiana jest lokalna i nie wpływa na otoczenie – na przykład tekst wstawiony w polach tekstowych (w przeciwnym razie każde naciśnięcie klawisza powodowałoby uruchomienie układu od elementu głównego).

Proces układu

Układ zwykle wygląda tak:

  1. Moduł renderujący nadrzędny określa własną szerokość.
  2. Rodzic podchodzi do dzieci i:
    1. Umieść podrzędny moduł renderujący (ustawia jego współrzędne x i y).
    2. W razie potrzeby wywołuje układ podrzędny – jeśli jest nieaktualny, jeśli jesteśmy w układzie globalnym lub z innego powodu – który oblicza wysokość elementu podrzędnego.
  3. Element nadrzędny używa skumulowanych wysokości elementów podrzędnych oraz wysokości marginesów i dopełnienia, aby ustawić własną wysokość – zostanie ona użyta przez element nadrzędny renderera nadrzędnego.
  4. Ustawia bit modyfikacji na wartość false.

Firefox używa obiektu „state” (nsHTMLReflowState) jako parametru układu (nazywanego „reflow”). Stan obejmuje m.in. szerokość elementu nadrzędnego.

Dane wyjściowe układu Firefoksa to obiekt „metrics” (nsHTMLReflowMetrics). Będzie on zawierać obliczoną wysokość modułu renderującego.

Obliczanie szerokości

Szerokość modułu renderującego jest obliczana na podstawie szerokości bloku kontenera, właściwości „width” stylu modułu renderującego, marginesów i obramowań.

Na przykład szerokość tego elementu div:

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

Zostanie obliczona przez WebKit w następujący sposób(metoda calcWidth klasy RenderBox):

  • Szerokość kontenera to maksymalna wartość availableWidth kontenera i 0. W tym przypadku availableWidth to contentWidth, który jest obliczany w ten sposób:
clientWidth() - paddingLeft() - paddingRight()

Właściwości clientWidth i clientHeight reprezentują wnętrze obiektu bez obramowania i paska przewijania.

  • Szerokość elementu to atrybut stylu „width”. Będzie ona obliczana jako wartość bezwzględna przez obliczenie wartości procentowej szerokości kontenera.

  • Dodano poziome obramowania i dopełnienia.

Do tej pory obliczaliśmy „preferowaną szerokość”. Teraz zostaną obliczone minimalna i maksymalna szerokość.

Jeśli preferowana szerokość jest większa niż maksymalna, używana jest szerokość maksymalna. Jeśli jest mniejsza niż minimalna szerokość (najmniejsza niepodzielna jednostka), używana jest minimalna szerokość.

Wartości są zapisywane w pamięci podręcznej na wypadek, gdyby układ był potrzebny, ale szerokość się nie zmienia.

Podział wiersza

Gdy moduł renderujący w środku układu zdecyduje, że musi się przerwać, zatrzymuje się i przekazuje do elementu nadrzędnego układu, że musi zostać przerwany. Element nadrzędny tworzy dodatkowe obiekty renderujące i wywołuje na nich układ.

Malarstwo

Na etapie renderowania następuje przejście przez drzewo renderowania i wywołanie metody „paint()” mechanizmu renderowania w celu wyświetlenia treści na ekranie. Malowanie korzysta z komponentu infrastruktury interfejsu.

Globalne i przyrostowe

Podobnie jak układ, renderowanie może być globalne (renderowane jest całe drzewo) lub przyrostowe. W przypadku renderowania przyrostowego niektóre mechanizmy renderujące zmieniają się w sposób, który nie wpływa na całe drzewo. Zmieniony moduł renderujący unieważnia swój prostokąt na ekranie. W rezultacie system operacyjny traktuje go jako „dirty region” i generuje zdarzenie „paint”. System operacyjny robi to sprytnie i łączy kilka regionów w jeden. W Chrome jest to bardziej skomplikowane, ponieważ mechanizm renderowania znajduje się w innym procesie niż proces główny. Chrome w pewnym stopniu symuluje działanie systemu operacyjnego. Prezentacja nasłuchuje tych zdarzeń i przekazuje wiadomość do elementu render root. Drzewo jest przeszukiwane, dopóki nie zostanie znaleziony odpowiedni moduł renderujący. Zostanie on ponownie narysowany (i zwykle jego elementy podrzędne).

Kolejność renderowania

CSS2 definiuje kolejność procesu renderowania. Jest to kolejność, w jakiej elementy są ułożone w kontekstach stosu. Ta kolejność wpływa na renderowanie, ponieważ stosy są renderowane od tyłu do przodu. Kolejność nakładania elementów renderujących blok:

  1. kolor tła
  2. obraz tła
  3. border
  4. dzieci
  5. konspekt

Lista wyświetlania w Firefoksie

Firefox analizuje drzewo renderowania i tworzy listę wyświetlania dla namalowanego prostokąta. Zawiera on elementy renderujące odpowiednie dla prostokąta, w odpowiedniej kolejności rysowania (tła elementów renderujących, następnie obramowania itd.).

Dzięki temu drzewo musi zostać przeanalizowane tylko raz, a nie kilka razy – najpierw wszystkie tła, potem wszystkie obrazy, potem wszystkie obramowania itd.

Firefox optymalizuje ten proces, nie dodając elementów, które będą ukryte, np. elementów całkowicie pod innymi nieprzezroczystymi elementami.

Pamięć masowa prostokątów WebKit

Przed ponownym renderowaniem WebKit zapisuje stary prostokąt jako bitmapę. Następnie rysuje tylko różnicę między nowym a starym prostokątem.

Zmiany dynamiczne

Przeglądarki starają się wykonywać jak najmniej działań w odpowiedzi na zmianę. Dlatego zmiany koloru elementu spowodują tylko jego ponowne narysowanie. Zmiany pozycji elementu spowodują zmianę układu i ponowne renderowanie elementu, jego elementów podrzędnych i być może elementów równorzędnych. Dodanie węzła DOM spowoduje zmianę układu i ponowne renderowanie węzła. Duże zmiany, takie jak zwiększenie rozmiaru czcionki elementu „html”, spowodują unieważnienie pamięci podręcznych, zmianę układu i ponowne renderowanie całego drzewa.

Wątki silnika renderowania

Silnik renderowania jest jednowątkowy. Prawie wszystko, z wyjątkiem operacji sieciowych, odbywa się w jednym wątku. W przypadku przeglądarek Firefox i Safari jest to główny wątek przeglądarki. W Chrome jest to główny wątek procesu karty.

Operacje sieciowe mogą być wykonywane przez kilka równoległych wątków. Liczba równoległych połączeń jest ograniczona (zwykle do 2–6 połączeń).

Pętla zdarzeń

Główny wątek przeglądarki to pętla zdarzeń. To nieskończona pętla, która utrzymuje proces przy życiu. Czeka na zdarzenia (takie jak zdarzenia układu i renderowania) i je przetwarza. Oto kod Firefoksa dla głównej pętli zdarzeń:

while (!mExiting)
    NS_ProcessNextEvent(thread);

Model wizualny CSS2

Obszar roboczy

Zgodnie ze specyfikacją CSS2 termin „canvas” (obszar roboczy) opisuje „przestrzeń, w której renderowana jest struktura formatowania”, czyli miejsce, w którym przeglądarka wyświetla zawartość.

Obszar roboczy jest nieskończony w każdym wymiarze przestrzeni, ale przeglądarki wybierają początkową szerokość na podstawie wymiarów widocznego obszaru.

Zgodnie z informacjami na stronie www.w3.org/TR/CSS2/zindex.html obszar roboczy jest przezroczysty, jeśli znajduje się w innym obszarze roboczym, a jeśli nie, ma kolor zdefiniowany przez przeglądarkę.

Model pudełkowy CSS

Model pudełkowy CSS opisuje prostokątne pola, które są generowane dla elementów w drzewie dokumentu i rozmieszczane zgodnie z modelem formatowania wizualnego.

Każde pole ma obszar zawartości (np. tekst, obraz itp.) oraz opcjonalne otaczające go obszary dopełnienia, obramowania i marginesu.

Model pudełkowy CSS2
Rysunek 19. Model pudełkowy CSS2

Każdy węzeł generuje od 0 do n takich pól.

Wszystkie elementy mają właściwość „display”, która określa typ generowanego pola.

Przykłady:

block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.

Domyślna wartość to inline, ale arkusz stylów przeglądarki może ustawić inne wartości domyślne. Na przykład: domyślny sposób wyświetlania elementu „div” to blok.

Przykładowy domyślny arkusz stylów znajdziesz tutaj: www.w3.org/TR/CSS2/sample.html.

Schemat pozycjonowania

Istnieją 3 systemy:

  1. Normalny: obiekt jest umieszczany zgodnie z jego miejscem w dokumencie. Oznacza to, że jego miejsce w drzewie renderowania jest takie samo jak w drzewie DOM i jest rozmieszczone zgodnie z typem i wymiarami pola.
  2. Float: obiekt jest najpierw układany jak w normalnym przepływie, a następnie przesuwany jak najdalej w lewo lub w prawo.
  3. Bezwzględne: obiekt jest umieszczany w drzewie renderowania w innym miejscu niż w drzewie DOM.

Schemat pozycjonowania jest ustawiany za pomocą właściwości „position” i atrybutu „float”.

  • static i relative powodują normalny przepływ,
  • absolute i fixed powodują pozycjonowanie bezwzględne,

W przypadku pozycjonowania statycznego nie jest zdefiniowana żadna pozycja i używane jest pozycjonowanie domyślne. W innych schematach autor określa pozycję: góra, dół, lewo, prawo.

Układ pola jest określany przez:

  • Typ pola
  • Wymiary pudełka
  • Schemat pozycjonowania
  • informacje zewnętrzne, takie jak rozmiar obrazu i rozmiar ekranu;

Typy pól

Pole blokowe: tworzy blok – ma własny prostokąt w oknie przeglądarki.

Pole blokowania.
Rysunek 20. Pole blokowania

Pole wbudowane: nie ma własnego bloku, ale znajduje się w bloku nadrzędnym.

Pola w tekście.
Rysunek 21. Pola wbudowane

Bloki są sformatowane pionowo, jeden po drugim. Reklamy In-line są formatowane poziomo.

Formatowanie blokowe i wbudowane.
Rysunek 22. Formatowanie blokowe i wbudowane

Pola wbudowane są umieszczane w wierszach lub „polach wierszy”. Wiersze są co najmniej tak wysokie jak najwyższe pole, ale mogą być wyższe, gdy pola są wyrównane do „linii bazowej” – oznacza to, że dolna część elementu jest wyrównana do punktu innego pola, który nie jest jego dolną krawędzią. Jeśli szerokość kontenera jest niewystarczająca, elementy wbudowane zostaną umieszczone w kilku wierszach. Zwykle dzieje się tak w akapicie.

Linie.
Rysunek 23. Linie

Pozycjonowanie

Względny

Pozycjonowanie względne – pozycjonowanie jak zwykle, a następnie przesunięcie o wymaganą wartość delta.

Pozycjonowanie względne.
Rysunek 24. Pozycjonowanie względne

Swobodne

Pole pływające jest przesunięte w lewo lub w prawo od linii. Ciekawą cechą jest to, że inne pola opływają go. Kod HTML:

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

Będzie wyglądać tak:

Liczba zmiennoprzecinkowa.
Rysunek 25. Pływak

Bezwzględne i stałe

Układ jest zdefiniowany dokładnie, niezależnie od normalnego przepływu. Element nie uczestniczy w normalnym przepływie. Wymiary są podawane względem kontenera. W przypadku stałego pozycjonowania kontenerem jest widoczny obszar.

Stałe pozycjonowanie.
Rysunek 26. Pozycjonowanie stałe

Przedstawianie informacji w warstwach

Określa to właściwość CSS z-index. Reprezentuje trzeci wymiar pola: jego pozycję wzdłuż osi Z.

Pola są podzielone na stosy (nazywane kontekstami stosu). W każdym stosie elementy z tyłu będą renderowane jako pierwsze, a elementy z przodu – na górze, bliżej użytkownika. W przypadku nakładania się elementów element na pierwszym planie ukryje element pod nim.

Stosy są uporządkowane według właściwości z-index. Pola z właściwością „z-index” tworzą stos lokalny. Widoczny obszar zawiera stos zewnętrzny.

Przykład:

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

Wynik będzie wyglądał tak:

Stałe pozycjonowanie.
Ilustracja 27. Pozycjonowanie stałe

Chociaż czerwony element div poprzedza zielony w znacznikach i zostałby namalowany wcześniej w zwykłym przepływie, właściwość z-index jest wyższa, więc znajduje się on bardziej z przodu w stosie przechowywanym przez pole główne.

Zasoby

  1. Architektura przeglądarki

    1. Grosskurth, Alan. Architektura referencyjna przeglądarek internetowych (PDF)
    2. Gupta, Vineet. Jak działają przeglądarki – część 1 – architektura
  2. Analizowanie

    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (aka the "Dragon book"), Addison-Wesley, 1986
    2. Rick Jelliffe. The Bold and the Beautiful: two new drafts for HTML 5.
  3. Firefox

    1. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers.
    2. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers (Google tech talk video)
    3. L. David Baron, Mozilla's Layout Engine
    4. L. David Baron, Mozilla Style System Documentation
    5. Chris Waterson, Notes on HTML Reflow
    6. Chris Waterson, Gecko Overview
    7. Alexander Larsson, The life of an HTML HTTP request
  4. WebKit

    1. David Hyatt, Implementing CSS(part 1)
    2. David Hyatt, An Overview of WebCore
    3. David Hyatt, WebCore Rendering
    4. David Hyatt, The FOUC Problem
  5. Specyfikacje W3C

    1. Specyfikacja HTML 4.01
    2. Specyfikacja HTML5 W3C
    3. Specyfikacja kaskadowych arkuszy stylów, poziom 2, wersja 1 (CSS 2.1)
  6. Instrukcje kompilacji przeglądarek

    1. Firefox: https://developer.mozilla.org/Build_Documentation
    2. WebKit. http://webkit.org/building/build.html

Tłumaczenia

Ta strona została przetłumaczona na język japoński dwukrotnie:

Możesz wyświetlić tłumaczenia na język koreański i turecki hostowane zewnętrznie.

Dziękuję!