Jak działają przeglądarki

Kulisy nowoczesnych przeglądarek

Wstęp

Ten obszerny przewodnik po wewnętrznych działaniach WebKit i Gecko jest wynikiem wielu badań przeprowadzonych przez izraelską programistkę Tali Garsiel. Przez kilka lat sprawdzała wszystkie opublikowane dane o wewnętrznych mechanizmach przeglądarki i wiele czasu spędziła na czytaniu kodu źródłowego przeglądarki internetowej. Napisała:

Jako web developer poznanie wewnętrznych mechanizmów działania przeglądarki pomoże Ci podejmować lepsze decyzje i poznać uzasadnienia sprawdzonych metod rozwoju. Ten dokument jest dość obszerny, ale warto poświęcić trochę czasu na zapoznanie się z jego treścią. Warto.

Paul Irish, zespół ds. relacji z deweloperami Chrome

Wprowadzenie

Przeglądarki internetowe to najczęściej używane oprogramowanie. Wyjaśnię w nim, jak to działa. Zobaczymy, co się dzieje, gdy wpisujesz google.comw pasku adresu, aż do momentu, gdy na ekranie przeglądarki pojawi się strona Google.

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

Obecnie na komputerach używa się 5 głównych przeglądarek: Chrome, Internet Explorer, Firefox, Safari i Opera. Głównymi przeglądarkami na urządzeniach mobilnych są: Android, iPhone, Opera Mini i Opera Mobile, przeglądarka UC, Nokia S40/S60 oraz Chrome. Wszystkie z nich, z wyjątkiem przeglądarek Opera, są oparte na technologii WebKit. Podam przykłady z przeglądarek open source Firefox i Chrome oraz Safari (który jest częściowo open source). Według statystyk StatCounter (stan na czerwiec 2013 r.) przeglądarki Chrome, Firefox i Safari stanowią około 71% użytkowników przeglądarek na komputery. Na urządzeniach mobilnych przeglądarki Android, iPhone i Chrome stanowią około 54% użytkowników.

Główne funkcje przeglądarki

Główną funkcją przeglądarki jest prezentowanie wybranych zasobów internetowych. Aby to zrobić, przeglądarka wysyła żądanie do serwera i wyświetla zasób w oknie przeglądarki. Takim zasobem jest zwykle dokument HTML, ale może to być również plik PDF, obraz lub inny materiał. Lokalizacja zasobu jest określana przez użytkownika za pomocą identyfikatora URI (ang. 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ą zajmującą się opracowywaniem standardów internetowych. Przez lata przeglądarki stosowały się tylko do części specyfikacji i opracowywały własne rozszerzenia. Spowodowało to poważne problemy ze zgodnością dla autorów stron internetowych. Obecnie większość przeglądarek mniej lub bardziej odpowiada specyfikacji.

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

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

Co ciekawe, interfejs użytkownika przeglądarki nie jest określony w żadnej specyfikacji, a po prostu pochodzi z dobrych praktyk kształtowanych przez wieloletnie doświadczenie i przez przeglądarki naśladujące siebie nawzajem. Specyfikacja HTML5 nie definiuje elementów interfejsu użytkownika, które musi zawierać przeglądarka, ale wymienia niektóre typowe elementy. Dotyczy to na przykład paska adresu, paska stanu i paska narzędzi. Istnieją oczywiście pewne funkcje charakterystyczne dla określonej przeglądarki, takie jak menedżer pobierania w Firefoksie.

Infrastruktura na wysokim poziomie

Główne komponenty przeglądarki to:

  1. Interfejs użytkownika: obejmuje pasek adresu, przycisk Wstecz/Dalej, menu zakładek itp. Każdą część ekranu przeglądarki z wyjątkiem okna, w którym wyświetla się żądana strona.
  2. Silnik przeglądarki: zarządza działaniami między interfejsem użytkownika a silnikiem renderowania.
  3. Silnik renderowania: odpowiada za wyświetlanie żądanych treści. Jeśli na przykład żądane treści to HTML, mechanizm renderowania przeanalizuje kod HTML i CSS, a następnie wyświetli przeanalizowane treści na ekranie.
  4. Sieć: w przypadku wywołań sieciowych, takich jak żądania HTTP, stosowanie różnych implementacji na różnych platformach za pomocą interfejsu niezależnego od platformy.
  5. UI backend: służy do wyświetlania podstawowych widżetów, takich jak listy rozwijane i okna. Ten backend udostępnia ogólny interfejs, który nie jest powiązany z konkretną platformą. Pod spodem używa metod interfejsu systemu operacyjnego.
  6. Współczynnik JavaScript. Służy do analizowania i wykonywania kodu JavaScript.
  7. Przechowywanie danych. To jest warstwa trwałości. Przeglądarka może potrzebować zapisywania na urządzeniu wszystkich rodzajów danych, takich jak pliki cookie. Przeglądarki obsługują również mechanizmy pamięci masowej, takie jak localStorage, IndexedDB, WebSQL czy FileSystem.
Komponenty przeglądarki
Rysunek 1. Komponenty przeglądarki

Pamiętaj, że przeglądarki takie jak Chrome uruchamiają wiele instancji silnika renderowania: po jednej na każdą kartę. Każda karta działa w ramach osobnego procesu.

Silniki renderowania

Odpowiada on za renderowanie, czyli wyświetlanie żądanych treści na ekranie przeglądarki.

Domyślnie mechanizm renderowania może wyświetlać dokumenty i obrazy w formacie HTML oraz XML. Może wyświetlać inne typy danych za pomocą wtyczek lub rozszerzeń, np. wyświetlać dokumenty PDF za pomocą wtyczki do przeglądania 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 wykorzystują różne silniki renderowania: Internet Explorer używa Trident, Firefox używa Gecko, Safari używa WebKit. Chrome i Opera (od wersji 15) używają Blink – rozwidlenia WebKit.

WebKit to silnik renderowania typu open source, który początkowo był silnikiem dla platformy Linux, a później został zmodyfikowany przez Apple, aby obsługiwał systemy Mac i Windows.

Główny przepływ

Silnik renderowania zacznie pobierać zawartość żądanego dokumentu z warstwy sieciowej. Zwykle odbywa się to w porcjach po 8 KB.

Następnie silnik renderowania działa w ten sposób:

Podstawowy przepływ pracy silnika renderującego
Rysunek 2. Podstawowy przepływ danych w renderującej wyszukiwarce

Mechanizm renderowania rozpocznie analizę dokumentu HTML i przekonwertuje elementy na węzły DOM w drzewie nazywanym „drzewem treści”. Wyszukiwarka analizuje dane o stylu zarówno w zewnętrznych plikach CSS, jak i w elementach stylu. Informacje o stylach wraz z instrukcjami wizualnymi w pliku HTML zostaną wykorzystane do utworzenia innego drzewa: drzewa renderowania.

Drzewo renderowania zawiera prostokąty z atrybutami wizualnymi, takimi jak kolor i wymiary. Prostokąty muszą być ułożone w odpowiedniej kolejności.

Po utworzeniu drzewa renderowania przechodzi ono proces „layoutu”. Oznacza to, że każdy węzeł musi mieć określone współrzędne, w których miejscu ma się pojawić na ekranie. Następnym etapem jest renderowanie – drzewo renderowania zostanie przeanalizowane, a każdy węzeł zostanie namalowany za pomocą warstwy backendowej interfejsu.

Pamiętaj, że jest to proces stopniowy. Aby zapewnić większą wygodę użytkownikom, silnik renderowania będzie się starał wyświetlać treści na ekranie tak szybko, jak to możliwe. Nie będzie czekać, aż cały kod HTML zostanie przeanalizowany, zanim zacznie budować i układać drzewo renderowania. Część treści zostanie przeanalizowana i wyświetlona, a proces będzie kontynuowany w stosunku do pozostałych treści, które nadal napływają z sieci.

Przykłady głównego przepływu

Główny proces korzystania z WebKit.
Rys. 3. Główny proces korzystania z WebKit
Główny proces mechanizmu renderowania geko w Mozilli.
Rys. 4. Główny proces renderowania Gekon w Mozilli

Z rysunków 3 i 4 wynika, że chociaż WebKit i Gecko używają nieco innej terminologii, proces jest w podstawie taki sam.

W Gecko drzewo elementów z wizualnie sformatowanymi elementami nazywa się „drzewem ramki”. Każdy element jest ramką. WebKit używa terminu „drzewo renderowania”, które składa się z „obiektów renderowania”. W WebKit na potrzeby rozmieszczenia elementów używany jest termin „układ”, a Gecko nazywa go „przeformatowaniem”. „Załącznik” to termin używany w WebKit do określania łączenia węzłów DOM i informacji wizualnych w celu utworzenia drzewa renderowania. Niewielka różnica niesemantyczną jest taka, że Gecko ma dodatkową warstwę między HTML a drzewem DOM. Nazywa się go „zbiornikiem treści” i jest to fabryka elementów DOM. Omówimy każdy etap tego procesu:

Analiza – ogólnie

Analiza to bardzo ważny proces w ramach silnika renderowania, dlatego omówimy go trochę bardziej szczegółowo. Zacznijmy od krótkiego wprowadzenia do analizy składni.

Przetwarzanie dokumentu oznacza przekształcanie go w strukturę, której może używać kod. Wynik analizy powstaje zwykle drzewo węzłów reprezentujące strukturę dokumentu. Nazywa się to drzewem analizy lub drzewem składni.

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

Węzeł drzewa wyrażenia matematycznego.
Ilustracja 5. Węzeł drzewa wyrażenia matematycznego

Gramatyka

Analiza 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żesz przeanalizować, musi mieć deterministyczną gramatykę obejmującą słownictwo i reguły składni. Nazywa się ją gramatyką bezkontekstową. Języki ludzkie nie są takimi językami, dlatego nie można ich analizować za pomocą konwencjonalnych technik analizy.

Kombinacja parsera i Lexera

Analiza może być podzielona na 2 podprocesy: analizę leksykalną i analizę składni.

Analiza leksykalna to proces dzielenia danych wejściowych na tokeny. Tokeny to słownictwo językowe: zbiór prawidłowych elementów składowych. W języku ludzkim będzie zawierać wszystkie słowa, które występują w słowniku tego języka.

Analiza składni to stosowanie reguł składni języka.

Parsery zazwyczaj dzielą efekty pracy między 2 komponenty: lexer (czasami nazywany tokenizerem), który odpowiada za podział danych wejściowych na prawidłowe tokeny, i parser, który odpowiada za skonstruowanie drzewa analizy przez analizę struktury dokumentu zgodnie z regułami składni języka.

Lekser wie, jak usuwać nieistotne znaki, takie jak spacje i podziały wierszy.

Od dokumentu źródłowego do drzewa analizy
Rysunek 6. Analiza drzew w dokumencie źródłowym

Proces analizowania jest iteracyjny. Przetwarzacz zwykle prosi leksykona o nowy token i próbuje dopasować go do jednej z reguł składni. Jeśli reguła zostanie dopasowana, do drzewa parsowania zostanie dodany węzeł odpowiadający temu tokenowi, a parsownik poprosi o kolejne dane.

Jeśli żadna reguła nie pasuje, parser zapisze token wewnętrznie i będzie wyświetlać prośby o tokeny, aż znajdzie regułę odpowiadającą wszystkim tym przechowywanym wewnętrznie tokenom. Jeśli nie zostanie znaleziona żadna reguła, parsujący wyrzuci wyjątek. Oznacza to, że dokument był nieprawidłowy i zawierał błędy składni.

Tłumaczenie

W wielu przypadkach drzewo parsowania nie jest produktem końcowym. Analiza jest często używana w tłumaczeniu: przekształca dokument wejściowy w inny format. Przykładem jest kompilacja. Kompilator, który kompiluje kod źródłowy na kod maszynowy, najpierw przetwarza go na drzewo parsowania, a potem przekształca to drzewo w dokument kodu maszynowego.

Kompilacja
Rysunek 7. Proces kompilacji

Przykład analizy

Na rysunku 5 widać drzewo parsowania utworzone na podstawie wyrażenia matematycznego. Spróbujmy zdefiniować prosty język matematyczny i zobaczmy, jak przebiega proces analizy.

Składnia:

  1. Elementy składowe składni języka to wyrażenia, hasła i operacje.
  2. Nasz język może zawierać dowolną liczbę wyrażeń.
  3. Wyrażenie to kolejno „termin”, po którym następuje „operacja”, a po nim inne hasło
  4. operacja jest symbolem plusa lub minusa,
  5. Wyrażenie to liczba całkowita lub wyrażenie

Przeanalizujmy dane wejściowe. 2 + 3 - 1

Pierwszy podłańcuch, który pasuje do reguły, to 2. Zgodnie z regułą 5 jest to hasło. Drugie dopasowanie to 2 + 3: odpowiada ono trzeciej regule: terminowi, po którym następuje operacja, a potem kolejny termin. Kolejne dopasowanie zostanie znalezione dopiero na końcu ciągu znaków. 2 + 3 - 1 jest wyrażeniem, ponieważ wiemy już, że 2 + 3 jest terminem, więc mamy termin, po którym następuje operacja, a potem kolejny termin. 2 + + nie pasuje do żadnej reguły, dlatego jest nieprawidłowym wejściem.

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 widzisz, liczby całkowite są definiowane za pomocą wyrażenia regularnego.

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

Jak już wspomnieliśmy, 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 pełni wyrazić w BNF. Formalną definicję znajdziesz w artykule w Wikipedii na temat gramatyki bez kontekstu

Typy parserów

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

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

Analizator od góry do dołu zacznie od reguły wyższego poziomu: zidentyfikuje 2 + 3 jako wyrażenie. Następnie zidentyfikuje 2 + 3 - 1 jako wyrażenie (proces identyfikowania wyrażenia ewoluuje, dopasowując się do innych reguł, ale punktem wyjścia jest reguła najwyższego poziomu).

Parser oddolny będzie skanować dane wejściowe do momentu dopasowania reguły. Funkcja ta zastąpi wtedy 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 dotycząca terminu 3 - 1
wyrażenie – 1
operacja wyrażenia 1
wyrażenie -

Ten typ parsera od dołu jest nazywany parserem shift-reduce, ponieważ dane wejściowe są przesuwane w prawo (wyobraź sobie, że wskaźnik wskazuje początek na początku danych wejściowych, a następnie przesuwa się w prawo). Stopniowo ogranicza się do reguł składni.

automatyczne generowanie parsowników,

Istnieją narzędzia, które umożliwiają wygenerowanie parsera. Podajesz im gramatykę języka – jego słownictwo i reguły składni – a one generują działający parsownik. Tworzenie parsera wymaga dogłębnej wiedzy na temat analizy składni i nie jest łatwe do ręcznego utworzenia zoptymalizowanego parsera, więc generatory parserów mogą być bardzo przydatne.

WebKit używa 2 dobrze znanych generatorów analizatorów: Flex do tworzenia leksyfikatora i Bison do tworzenia analizatora (możesz je spotkać pod nazwami Lex i Yacc). Dane wejściowe Flex to plik zawierający definicje tokenów regularnych. Dane wejściowe dla Bison to reguły składni języka w formacie BNF.

Parser HTML

Zadaniem parsera HTML jest przetworzenie znaczników HTML w drzewo analizy.

Gramatyka HTML

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

Jak już wspomnieliśmy w części poświęconej parsowaniu, składnia gramatyki może być definiowana formalnie za pomocą formatów takich jak BNF.

Niestety wszystkie konwencjonalne tematy dotyczące parsowania nie mają zastosowania w przypadku HTML (nie wspominam o nich tylko dla zabawy – będą używane do parsowania CSS i JavaScript). Nie można łatwo zdefiniować HTML 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ść podobny do XML. Dostępnych jest wiele analizatorów XML. Istnieje odmiana XML HTML – XHTML – jaka jest główna 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 domyślnie) lub czasami pominąć tagi początkowe i końcowe itp. W ogóle jest to „miękka” składnia, w przeciwieństwie do sztywnej i wymagającej składni XML.

Ta pozornie niewielka zmiana ma ogromne znaczenie. Z jednej strony jest to główny powód, dla którego HTML jest tak popularny: wybacza błędy i ułatwia życie autorowi strony. Z drugiej strony utrudnia to tworzenie gramatyki formalnej. Podsumowując, konwencjonalne parsery nie mogą łatwo analizować kodu HTML, ponieważ jego gramatyka nie jest niezależna od kontekstu. Kodu HTML nie można przeanalizować przez parser XML.

DTD w HTML

Definicja HTML jest w formacie DTD. Ten format służy do definiowania języków z rodziny SGML. Format zawiera definicje wszystkich dozwolonych elementów, ich atrybutów i hierarchii. Jak już wiemy, 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 przez przeglądarki w przeszłości. Ma to na celu zapewnienie zgodności wstecznej ze starszymi treściami. Aktualne rygorystyczne zasady DTD znajdziesz tutaj: www.w3.org/TR/html4/strict.dtd

DOM

Drzewo wyjściowe („drzewo analizy”) to drzewo węzłów atrybutów i elementów DOM. DOM to skrót od Document Object Model. Jest to prezentacja obiektów w dokumencie HTML i interfejs elementów HTML dla świata zewnętrznego, np. JavaScript.

Pierwiastek tego drzewa to obiekt „Document”.

DOM jest prawie w 100% powiązany z oznaczaniem. Na przykład:

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

Ten znacznik zostanie przekształcony w drzewo DOM o takim wyglądzie:

Drzewo DOM przykładowego znacznika
Ilustracja 8. Drzewo DOM przykładowego znacznika

Podobnie jak HTML, DOM jest określany przez organizację W3C. Zobacz www.w3.org/DOM/DOMTR. Jest to ogólna specyfikacja manipulowania dokumentami. Konkretny moduł opisuje konkretne 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 wewnętrznie przez przeglądarkę.

Algorytm analizy

Jak już wspomnieliśmy w poprzednich sekcjach, kodu HTML nie można przeanalizować za pomocą zwykłych analizujących od góry do dołu ani od dołu do góry.

Przyczyny są następujące:

  1. wybaczający charakter języka,
  2. Fakt, że przeglądarki mają tradycyjną tolerancję błędów, co pozwala na obsługę dobrze znanych przypadków nieprawidłowego kodu HTML.
  3. Proces analizy jest reentrant. W przypadku innych języków źródło nie zmienia się podczas analizy, ale w języku HTML kod dynamiczny (np. elementy skryptu zawierające wywołania funkcji document.write()) może dodawać dodatkowe tokeny, więc proces analizy modyfikuje dane wejściowe.

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

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

Tokenizacja to analiza leksykalna, która polega na analizie danych wejściowych na tokeny. Do tokenów HTML należą tagi początkowe, tagi końcowe, nazwy atrybutów i wartości atrybutów.

Tokenizer rozpoznaje token, przekazuje go konstruktorowi drzewa i pobiera następny znak do rozpoznania następnego tokena, i tak dalej aż do końca danych wejściowych.

Proces analizy HTML (pobrany ze specyfikacji HTML5)
Rys. 9. Proces analizy HTML (pobrany ze specyfikacji HTML5)

Algorytm tokenizacji

Dane wyjściowe algorytmu to token HTML. Algorytm jest wyrażony jako maszyna stanowa. Każdy stan zużywa co najmniej 1 znak ze strumienia wejściowego i zmienia następny stan zgodnie z tymi znakami. Na decyzję ma wpływ bieżący stan tokenizacji i stan tworzenia drzewa. Oznacza to, że ten sam wykorzystany znak będzie dawał różne wyniki w zależności od bieżącego stanu. Algorytm jest zbyt skomplikowany, aby można go było w pełni opisać, więc zobaczmy prosty przykład, który pomoże nam zrozumieć tę zasadę.

Podstawowy przykład – tokenizacja tego kodu HTML:

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

Stan początkowy to „Stan danych”. Gdy napotka znak <, stan zmienia się na „Tag open state”. Użycie znaku a-z powoduje utworzenie „tokenu startowego tagu” i zmianę stanu na „Stan nazwy tagu”. Pozostaniemy w tym stanie, dopóki znak > nie zostanie wykorzystany. Każdy znak jest dołączany do nazwy nowego tokena. W naszym przypadku utworzony token to token html.

Gdy tag > zostanie osiągnięty, zostanie wyemitowany bieżący token, a stan zmieni się z powrotem na „Stan danych”. Tag <body> będzie traktowany w ten sam sposób. Do tej pory emitowane były tagi htmlbody. Wróciliśmy do „Stanu danych”. Użycie znaku H w ciągu ciągu Hello world spowoduje utworzenie i wydanie tokena znaku. Będzie to się powtarzać, aż do osiągnięcia < w ciągu </body>. Emitujemy token znaku dla każdego znaku w Hello world.

Wracamy do stanu „Tag otwarty”. Przetworzenie następnego wejścia / spowoduje utworzenie end tag token i przejście do stanu „Nazwa tagu”. Ponownie pozostajemy w tym stanie, dopóki nie dojdziemy do etapu >.Wtedy zostanie wyemitowany nowy token tagu i wrócimy do stanu „Dane”. Dane wejściowe </html> będą traktowane tak samo jak w poprzednim przypadku.

Tokenizacja przykładowych danych wejściowych
Ilustracja 10. Podział przykładowego tekstu na tokeny

Algorytm tworzenia drzewa

Podczas tworzenia parsowania tworzony jest obiekt Document. Na etapie tworzenia drzewa zostanie zmodyfikowane drzewo DOM z dokumentem w korzeniach i dodane do niego elementy. Każdy węzeł wyemitowany przez tokenizer zostanie przetworzony przez konstruktor drzewa. W przypadku każdego tokena specyfikacja określa, który element DOM jest dla niego odpowiedni i zostanie utworzony. Element jest dodawany do drzewa DOM, a także do stosu otwartych elementów. Ten stos służy do korygowania niedopasowań zagnieżdżonych i niezamkniętych tagów. Algorytm jest też opisany jako maszyna stanowa. Te stany nazywamy „trybami wstawiania”.

Zobaczmy, jak tworzymy drzewo na podstawie przykładowych danych wejściowych:

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

Dane wejściowe na etapie tworzenia drzewa to sekwencja tokenów z etapu tokenizacji. Pierwszy tryb to „tryb początkowy”. Odebranie 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 obiektu Document wyższego poziomu.

Stan zostanie zmieniony na „przed głową”. Następnie otrzymuje się token „body”. Element HTMLHeadElement zostanie utworzony domyślnie, mimo że nie mamy elementu „head” i zostanie on dodany do drzewa.

Przechodzimy teraz do trybu „w głowie”, a następnie do trybu „po głowie”. Token treści jest przetwarzany ponownie, obiekt HTMLBodyElement jest tworzony i wstawiony, a tryb jest przenoszony do "in body".

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

Odbieranie tokena zakończenia treści powoduje przejście do trybu „po treści”. Otrzymamy teraz tag końcowy HTML, który przeniesie nas do trybu „after after body”. Odebranie tokena końca pliku kończy analizowanie.

Budowanie drzewa w przykładowym kodzie HTML.
Rysunek 11. Budowa drzewa przykładowego pliku HTML

Działania po zakończeniu analizy

Na tym etapie przeglądarka oznacza dokument jako interaktywny i rozpoczyna analizowanie skryptów, które są w trybie „opóźnionym”: skryptów, które powinny zostać wykonane po przeanalizowaniu dokumentu. Stan dokumentu zostanie ustawiony na „ukończony”, a zdarzenie „load” zostanie uruchomione.

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

Tolerancja błędów w przeglądarkach

Na stronie HTML nigdy nie wystąpi błąd „Nieprawidłowa składnia”. Przeglądarki naprawiają nieprawidłowe treści i przechodzą dalej.

Weźmy na przykład ten kod HTML:

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

Musimy naruszyć około miliona zasad („mytag” nie jest tagiem standardowym, niewłaściwe zagnieżdżanie elementów „p” i „div” itp.), ale przeglądarka nadal wyświetla go prawidłowo i nie zgłasza żadnych błędów. Większość kodu parsowania służy do poprawiania błędów autora kodu HTML.

Obsługa błędów jest dość spójna w przeglądarkach, ale co zaskakujące, nie była częścią specyfikacji HTML. Podobnie jak przyciski dodawania zakładek i wstecz/dalej, jest to coś, co w przeglądarkach ewoluowało na przestrzeni lat. W wielu witrynach występują nieprawidłowe konstrukcje HTML, które przeglądarki próbują naprawić w sposób zgodny z innymi przeglądarkami.

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

Parser analizuje podzielone na tokeny dane wejściowe w dokumencie, tworząc drzewo dokumentu. Jeśli dokument jest poprawnie sformatowany, jego zanalizowanie jest proste.

Niestety musimy obsługiwać wiele dokumentów HTML, które nie są poprawnie sformułowane, więc parsujący musi być tolerancyjny wobec błędów.

Musimy rozwiązać co najmniej te problemy:

  1. Dodawany element jest wyraźnie zabroniony wewnątrz zewnętrznego tagu. W tym przypadku powinniśmy zamknąć wszystkie tagi aż do tego, który zakazuje elementu, a następnie dodać go.
  2. Nie możemy dodać elementu bezpośrednio. Możliwe, że osoba tworząca dokument zapomniała jakiś tag w środku (lub tag między nimi jest opcjonalny). Tak może być w przypadku następujących tagów: HTML HEAD BODY TBODY TR TD LI (czy coś mi umknęło?).
  3. Chcemy dodać element bloku wewnątrz elementu wbudowanego. Zamknij wszystkie elementy wstawiane do następnego elementu bloku o wyższym priorytecie.
  4. Jeśli to nie pomoże, zamknij elementy, dopóki nie będziemy mogli ich dodać, lub zignoruj tag.

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

</br> zamiast <br>

Niektóre strony używają </br> zamiast <br>. Aby zapewnić zgodność z IE i Firefox, WebKit traktuje to jako <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.

Tabela z błędem

Tabela przypadkowa 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 siostrzane tabele:

<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 do bieżącej zawartości elementu: umieszcza tabelę wewnętrzną z zewnętrznego stosu tabel. Tabele będą teraz tabelami siostrzanymi.

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 szczegółowa 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 zakończenia html lub body

Ponownie – komentarz mówi sam za siebie.

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

Dlatego autorzy stron internetowych piszą poprawnie sformatowany kod HTML, chyba że chcą oni pojawiać się jako przykład we fragmencie kodu tolerancji błędów w WebKit.

Analiza kodu CSS

Pamiętasz pojęcia dotyczące analizy z wstępu? W przeciwieństwie do HTML, CSS jest gramatyką bezkontekstową i można ją analizować za pomocą typów parserów opisanych we wstępie. W istocie specyfikacja CSS definiuje składnię i gramatykę leksykalną CSS.

Oto kilka przykładów:

Gramatyka leksykalna (słownictwo) jest definiowana za pomocą wyrażeń regularnych dla każdego elementu:

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 (odwołujący się do „#”).

Składnia gramatyki 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*] ')' ]
  ;

Objaśnienie:

Zestaw reguł ma taką strukturę:

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

div.errora.error to selektory. Część wewnątrz nawiasów klamrowych zawiera reguły stosowane przez ten zbiór 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 oddzielonych przecinkami i spacją (S oznacza białą przestrzeń). Reguły zawierają nawiasy klamrowe, a w nich deklarację lub opcjonalnie kilka deklaracji oddzielonych średnikami. „declaration” i „selector” zostaną zdefiniowane w następujących definicjach BNF.

WebKit CSS parser

WebKit używa generatorów parserów Flex i Bison do automatycznego tworzenia parserów na podstawie plików gramatyki CSS. Jak pamiętasz z wprowadzenia do analizatora, Bison tworzy analizę od dołu do góry. W przeglądarce Firefox używany jest parser od góry, który został zapisany ręcznie. W obu przypadkach każdy plik CSS jest analizowany w ramach obiektu StyleSheet. Każdy obiekt zawiera reguły CSS. Obiekty reguły CSS zawierają obiekty selektora i deklaracji oraz inne obiekty odpowiadające gramatyce CSS.

Analizuję kod CSS.
Rysunek 12. Analiza kodu CSS

Przetwarzanie zamówienia dotyczącego skryptów i arkuszy stylów

Skrypty

Model sieci jest synchroniczny. Autorzy oczekują, że skrypty będą analizowane i wykonywane natychmiast po osiągnięciu przez parsowanie tagu <script>. Analizowanie dokumentu zostanie wstrzymane do czasu wykonania skryptu. Jeśli skrypt jest zewnętrzny, zasób musi najpierw zostać pobrany z sieci. Odbywa się to również w sposób synchroniczny, a przetwarzanie zostaje wstrzymane do momentu pobrania zasobu. Był to model przez wiele lat, a dodatkowo jest określony w specyfikacjach HTML4 i 5. Autorzy mogą dodać do skryptu atrybut „defer”. W takim przypadku nie zatrzyma on analizowania dokumentu i zostanie wykonany po zakończeniu tego procesu. HTML5 dodaje opcję oznaczania skryptu jako asynchronicznego, aby był analizowany i wykonywany przez inny wątek.

Analiza spekulacyjna

Tę optymalizację przeprowadzają zarówno WebKit, jak i Firefox. Podczas wykonywania skryptów inny wątek analizuje pozostałą część dokumentu, sprawdzając, jakie inne zasoby muszą zostać załadowane z sieci, i wczytuje je. Dzięki temu zasoby mogą być ładowane na połączeniach równoległych, co poprawia ogólną szybkość. Uwaga: spekulatywny parser analizuje tylko odwołania do zasobów zewnętrznych, takich jak skrypty zewnętrzne, arkusze stylów i obrazy. Nie modyfikuje drzewa DOM – to zadanie parsera głównego.

arkusze stylów,

Arkusze stylów mają natomiast inny model. Zasadniczo wygląda na to, że skoro arkusze stylów nie zmieniają drzewa DOM, nie ma powodu, aby na nie czekać i zatrzymywać analizę dokumentu. Występuje jednak problem ze skryptami, które wymagają informacji o stylu na etapie analizowania dokumentu. Jeśli styl nie został jeszcze załadowany i przeanalizowany, skrypt otrzyma błędne odpowiedzi, co prawdopodobnie spowodowało wiele problemów. Wygląda na to, że jest to przypadek szczególny, ale 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 stylów, na które mogą mieć wpływ niewczytane arkusze stylów.

Budowa drzewa w renderze

Podczas tworzenia drzewa DOM przeglądarka tworzy też inne drzewo – drzewo renderowania. To drzewo zawiera elementy wizualne w kolejności, w jakiej będą wyświetlane. Jest to wizualna reprezentacja dokumentu. Celem tego drzewa jest umożliwienie wyświetlania treści w prawidłowej kolejności.

W Firefoxie elementy w drzewie renderowania nazywane są „ramkami”. WebKit używa terminu „renderowanie” lub „obiekt renderowania”.

Renderowanie wie, jak rozmieścić i wymalować siebie i swoje potomstwo.

Klasa RenderObject WebKit, która jest klasą podstawową dla wszystkich silnikó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 renderer reprezentuje prostokątny obszar, który zwykle odpowiada pudełku 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 odpowiedniego dla węzła (patrz sekcja obliczanie stylu). Oto kod WebKit, który określa, jaki typ renderowania ma być utworzony dla węzła DOM na podstawie atrybutu wyświetlania:

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 w WebKit chce utworzyć specjalny moduł renderujący, zastąpi on metodę createRenderer(). Mechanizmy renderowania wskazują obiekty stylu zawierające informacje niegeometryczne.

relacja drzewa renderowania do drzewa DOM,

Renderery odpowiadają elementom DOM, ale relacja nie jest relacją jeden do jednego. Niewizualne elementy DOM nie zostaną wstawione do drzewa renderowania. Przykładem jest element „head”. Na drzewie nie będą też widoczne elementy, których wartość wyświetlania została ustawiona na „brak” (elementy z widocznością „ukryte” będą widoczne na drzewie).

Istnieją elementy DOM, które odpowiadają kilku elementom wizualnym. Zwykle są to elementy o kompleksowej strukturze, których nie można opisać za pomocą pojedynczego prostokąta. Na przykład element „select” ma 3 mechanizmy renderowania: jeden dla obszaru wyświetlania, jeden dla pola listy i jeden dla przycisku. Jeśli tekst jest podzielony na kilka linii, ponieważ szerokość nie wystarcza na jedną linię, nowe linie zostaną dodane jako dodatkowe renderowanie.

Innym przykładem wielu mechanizmów renderowania jest uszkodzony kod HTML. Zgodnie ze specyfikacją CSS element wbudowany może zawierać tylko elementy blokowe lub tylko elementy wbudowane. W przypadku treści mieszanych zostaną utworzone anonimowe moduły renderowania bloków, które otaczają elementy wstawiane.

Niektóre obiekty renderowania odpowiadają węzłowi DOM, ale nie znajdują się w tym samym miejscu w drzewie. Elementy pływające oraz bezwzględnie umieszczone w wycieku są poza granicami, są umieszczane w innej części drzewa i zmapowane na prawdziwą ramkę. W miejscu, w którym powinny się znajdować, znajduje się ramka z placeholderem.

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

Proces tworzenia drzewa

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

W WebKit proces rozpoznawania stylu i tworzenia mechanizmu renderowania nosi nazwę „załącznik”. Każdy węzeł DOM ma metodę „attach”. Załącznik jest synchroniczny, a wstawianie węzła do drzewa DOM wywołuje nową metodę węzła „attach”.

Przetwarzanie tagów HTML i body powoduje utworzenie głównego elementu drzewa renderowania. Główny obiekt renderowania odpowiada temu, co specyfikacja CSS wywołuje zawierający go blok: najwyższy blok, który zawiera wszystkie pozostałe bloki. Jego wymiary to widoczny obszar, czyli wymiary obszaru wyświetlania okna przeglądarki. W Firefox nazywa się to ViewPortFrame, a w WebKit – RenderView. Jest to obiekt renderowania, do którego odwołuje się dokument. Pozostała część drzewa jest tworzona jako wstawianie węzłów DOM.

Zapoznaj się ze specyfikacją CSS2 dotyczącą modelu przetwarzania.

Obliczanie stylu

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

Styl zawiera różne arkusze stylów, elementy stylów wbudowanych i właściwości wizualne w kodzie HTML (np. właściwość „bgcolor”). Te ostatnie są tłumaczone na odpowiadające im właściwości stylów CSS.

Źródła arkuszy stylów to domyślne arkusze stylów przeglądarki, arkusze stylów dostarczane przez autora strony i arkusze stylów użytkownika – są to arkusze dostarczane przez użytkownika przeglądarki (przeglądarki umożliwiają definiowanie ulubionych stylów. W Firefoxie można to zrobić, umieszczając arkusz stylów w folderze „Profil Firefoxa”.

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

  1. Dane stylów to bardzo duży element zawierający wiele właściwości stylów, co może powodować problemy z pamięcią.
  2. Wyszukiwanie reguł dopasowywania dla poszczególnych elementów może powodować problemy ze skutecznością, jeśli nie są one zoptymalizowane. Przeszukiwanie całej listy reguł w przypadku każdego elementu w celu znalezienia dopasowań jest czasochłonne. Selektory mogą mieć złożoną strukturę, przez co proces dopasowywania rozpoczyna się na pozornie obiecującej ścieżce, która okazuje się bezcelowa, i konieczne jest wypróbowanie innej ścieżki.

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

    div div div div{
    ...
    }
    

    Oznacza, że reguły dotyczą elementu <div>, który jest elementem potomnym 3 elementów div. Załóżmy, że chcesz sprawdzić, czy reguła ma zastosowanie do danego elementu <div>. Wybierasz pewną ścieżkę w drzewie do sprawdzenia. Możesz musieć przejść przez drzewo węzłów, aby dowiedzieć się, że są tylko 2 divy i że reguła nie ma zastosowania. W takim przypadku musisz wypróbować inne ścieżki na drzewie.

  3. Stosowanie reguł wymaga stosowania dość złożonych reguł kaskadowych, które definiują 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 pewnych warunkach obiekty te mogą być współdzielone przez węzły. Węzły są węzłami równorzędnymi lub spokrewnionymi:

  1. Elementy muszą mieć ten sam stan myszy (np.jeden nie może być w stanie :hover, a drugi nie).
  2. Żaden z elementów nie powinien mieć identyfikatora.
  3. Nazwy tagów powinny być takie same.
  4. Atrybuty klas powinny być takie same
  5. Zestaw zmapowanych atrybutów musi być identyczny.
  6. Stany linków muszą być takie same
  7. Stany skupienia muszą być takie same
  8. Żaden z tych elementów nie powinien być dotknięty przez selektory atrybutów, przy czym „dotknięty” oznacza, że selektor zawiera dopasowanie, które używa selektora atrybutu w dowolnej pozycji w selektorze.
  9. Elementy nie mogą mieć atrybutu stylu wbudowanego.
  10. Nie można używać żadnych selektorów siostrza. WebCore po prostu uruchamia globalny przełącznik, gdy napotka dowolny selektor siostrza, i wyłącza udostępnianie stylów dla całego dokumentu, gdy takie selektory są obecne. Obejmuje to selektor + oraz selektory takie jak :first-child i :last-child.

Drzewo reguł w Firefoksie

W przeglądarce Firefox są 2 dodatkowe drzewa, które ułatwiają obliczanie stylu: drzewo reguł i drzewo kontekstu stylu. WebKit ma też obiekty stylów, ale nie są one przechowywane w drzewie, tak jak w przypadku drzewa kontekstu stylów. Tylko węzeł DOM wskazuje odpowiedni styl.

Drzewo kontekstu stylu w przeglądarce Firefox.
Ilustracja 14. Drzewo kontekstu w stylu Firefoxa.

Konteksty stylu zawierają wartości końcowe. Wartości są obliczane przez zastosowanie wszystkich reguł dopasowania w prawidłowej kolejności i przeprowadzenie na nich przekształceń, które przekształcają je z wartości logicznych w konkretne wartości. Jeśli na przykład wartość logiczna stanowi odsetek powierzchni ekranu, zostanie ona obliczona i przekształcona w jednostki bezwzględne. Pomysł drzewa reguł jest naprawdę sprytny. Umożliwia współdzielenie tych wartości między węzłami, co pozwala uniknąć ich ponownego przetwarzania. Pozwala to też zaoszczędzić miejsce.

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

Chodzi o to, aby zobaczyć ścieżki w postaci drzew w postaci słów w leksykonie. Załóżmy, że mamy już obliczony ten diagram reguł:

Wyliczone drzewo reguł
Rysunek 15. Obliczone drzewo reguł.

Załóżmy, że musimy dopasować reguły do innego elementu w drzewie treści i sprawdzić, które reguły (w prawidłowej kolejności) zostały dopasowane: B-E-I. Ta ścieżka jest już obecna w drzewie, ponieważ została już obliczona ścieżka A-B-E-I-L. Teraz będziemy mieć mniej pracy.

Zobaczmy, jak drzewo uratuje nam pracę.

Dzielenie na elementy struct

Konteksty stylów są podzielone na struktury. Te elementy typu struct zawierają informacje o stylu dla określonej kategorii, takiej jak obramowanie lub kolor. 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 nie dziedziczone (nazywane „zresetowanymi”) używają wartości domyślnych, jeśli nie są zdefiniowane.

Drzewo pomaga nam w przypadku buforowania w drzewie całych struktur (zawierających obliczone wartości końcowe). Jeśli dolny węzeł nie zawiera definicji struktury, można użyć z pamięci podręcznej struktury z węzła wyższego poziomu.

Obliczanie kontekstów stylów za pomocą drzewa reguł

Podczas obliczania kontekstu stylu dla danego elementu najpierw obliczamy ścieżkę w drzewie reguł lub używamy istniejącej ścieżki. Następnie zaczynamy stosować reguły na ścieżce, aby wypełnić struktury w kontekście nowego stylu. Zaczynamy od dolnego węzła ścieżki – tego o najwyższym priorytecie (zwykle najbardziej szczegółowego selektora) i przechodzimy w drzewie w górę, aż nasza struktura będzie pełna. Jeśli nie ma specyfikacji elementu struct w tym węźle reguły, możemy znacznie przeprowadzić optymalizację – idziemy do drzewa, aż znajdziemy węzeł, który w pełni go określa i wskazuje go – to najlepsza optymalizacja – cała struktura jest współdzielona. Pozwala to zaoszczędzić pamięć i czas na obliczenie wartości końcowych.

Jeśli znajdziemy częściowe definicje, przejdziemy w drzewie wyżej, aż struktura zostanie wypełniona.

Jeśli nie znajdziemy żadnych definicji struktury, a struktura jest typu „odziedziczona”, wskazujemy na strukturę rodzica w drzewie kontekstowym. W tym przypadku udało nam się też udostępnić struktury. Jeśli jest to struktura zresetowana, 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 przechowujemy wynik w konkretnym węźle drzewa, aby dzieci mogły z niego korzystać.

Jeśli element ma element siostrzany lub braterski, który wskazuje ten sam węzeł drzewa, cały kontekst stylów może być udostępniany między nimi.

Na przykład: załóżmy, że mamy ten 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}

Aby uprościć sobie sprawę, załóżmy, że musimy wypełnić tylko 2 struktury: koloru i marginesów. Struktura kolorów zawiera tylko 1 element: kolor. Struktura marginesów zawiera 4 strony.

Wygenerowane drzewo reguł będzie wyglądać tak (węzły są oznaczone nazwą węzła: numerem reguły, do której odnoszą):

Drzewo reguł
Rysunek 16. Drzewo reguł

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

Drzewo kontekstu.
Rysunek 17. Drzewo kontekstu

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

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

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

Teraz musimy wypełnić struktury stylów. Zaczniemy od wypełnienia struktury marginesu. Ponieważ ostatni węzeł reguły (F) nie dodaje nic do struktury marginesów, możemy przejść w drzewie wyżej, aż znajdziemy strukturę w pamięci podręcznej obliczoną w poprzednim wstawieniu węzła, i ją wykorzystać. Znajdziemy go w węźle B, który jest najwyższym węzłem, w którym określone są reguły dotyczące marży.

Mamy definicję elementu struct koloru, więc nie możemy użyć elementu struct z bufora. Kolor ma jeden atrybut, więc nie trzeba ingerować w drzewo, aby wypełnić inne atrybuty. Obliczymy wartość końcową (przekonwertuj ciąg 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 wskazują one na regułę G, tak jak poprzedni zakres. Ponieważ mamy elementy siostrzane, które wskazują na ten sam węzeł, możemy udostępnić cały kontekst stylu i wskazywać tylko kontekst poprzedniego elementu.

W przypadku elementów typu struct zawierających reguły dziedziczone z elementu nadrzędnego buforowanie odbywa się w drzewie kontekstu (właściwość koloru jest w rzeczywistości dziedziczona, ale Firefox traktuje ją jako zresetowaną i zapisuje ją w pamięci podręcznej drzewa reguł).

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

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

Wtedy element akapitu, który jest elementem podrzędnym div w drzewie kontekstowym, mógł udostępniać tę samą strukturę czcionki co jego element nadrzędny. Jest to możliwe, jeśli nie określono żadnych reguł dotyczących czcionek dla akapitu.

W pakiecie WebKit, który nie ma drzewa reguł, pasujące deklaracje są sprawdzane cztery razy. Najpierw są stosowane nieważne reguły o wysokim priorytecie (czyli reguły, które powinny być stosowane jako pierwsze, ponieważ inne reguły od nich zależą, np. reguły wyświetlania), potem ważne reguły o wysokim priorytecie, potem nieważne reguły o normalnym priorytecie i na końcu ważne reguły o normalnym priorytecie. Oznacza to, że właściwości, które pojawiają się wielokrotnie, zostaną rozwiązane zgodnie z prawidłową kolejnością kaskadową. Wygrywa ostatni.

Podsumowując: udostępnienie obiektów stylu (całkowicie lub niektórych zawartych w nich elementów struct) rozwiązuje problemy 1 i 3. Drzewo reguł Firefoxa pomaga też stosować właściwości w prawidłowej kolejności.

Manipulowanie regułami, aby ułatwić dopasowanie

Istnieje kilka źródeł reguł stylów:

  1. Reguły CSS w zewnętrznych arkuszach stylów lub w elementach stylu. css p {color: blue}
  2. Wbudowane atrybuty stylu, 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 dwa atrybuty można łatwo dopasować do elementu, ponieważ ma on atrybuty stylu, a atrybuty HTML można mapować, używając elementu jako klucza.

Jak już wspomnieliśmy w problemie 2, dopasowywanie reguł CSS może być trudniejsze. Aby ułatwić rozwiązanie problemu, reguły są modyfikowane w celu ułatwienia dostępu.

Po przeanalizowaniu arkusza stylów reguły są dodawane do jednej z kilku map haszowych zgodnie z selektorem. Istnieją mapy według identyfikatora, nazwy klasy, nazwy tagu oraz ogólna mapa dla wszystkich elementów, które nie pasują do tych kategorii. Jeśli selektorem jest identyfikator, reguła zostanie dodana do mapy identyfikatorów, jeśli jest to klasa, 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ł, więc nie trzeba ich nawet brać pod uwagę podczas procesu dopasowywania(4.1).

Zobaczmy na przykład następujące reguły stylu:

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

Pierwsza reguła zostanie wstawiona do mapy zajęć. 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 klasy będzie zawierać klucz „error”, pod którym znajduje się reguła „p.error”. Element div będzie mieć odpowiednie reguły w mapie identyfikatorów (kluczem jest identyfikator) i mapie tagów. Pozostało więc tylko ustalenie, które z reguł wyodrębnionych przez klucze rzeczywiście pasują.

Jeśli na przykład reguła dla elementu div była taka:

table div {margin: 5px}

W dalszym ciągu zostanie on wyodrębniony z mapy tagów, ponieważ klucz znajduje się skrajnie z prawej strony, ale nie pasuje do elementu div, który nie ma elementu nadrzędnego tabeli.

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

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 dana właściwość nie jest zdefiniowana przez żadne 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 zaczyna się, gdy istnieje więcej niż jedna definicja – tu następuje kaskadowa kolejność rozwiązań problemu.

Deklaracja właściwości stylu może występować w kilku arkuszach stylów i kilka razy w ramach jednego arkusza. 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 niskiej do wysokiej):

  1. Deklaracje przeglądarki
  2. Deklaracje normalnego użytkownika
  3. Normalne deklaracje autora
  4. Autorzy ważnych deklaracji
  5. Ważne deklaracje użytkownika

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

Zgodność ze specyfikacją

Specyficzność selektora jest zdefiniowana w specyfikacji CSS2 w ten sposób:

  1. liczba 1, jeśli deklaracja, z której pochodzi, jest atrybutem „style”, a nie regułą z selektorem; w przeciwnym razie 0 (w przeciwnym razie (= a))
  2. zlicz liczbę atrybutów identyfikatora w selektorze (= b);
  3. zlicz liczbę innych atrybutów i pseudoklas w selektorze (= c);
  4. zlicz liczbę nazw elementów i pseudoelementów w selektorze (= d)

Połączenie 4 liczb a-b-c-d (w systemie liczbowym o dużej podstawie) daje specyficzność.

Wymagana liczba jest zdefiniowana na podstawie największej liczby w jednej z kategorii.

Jeśli na przykład a=14, możesz użyć podstawy szesnastkowej. W nieprawdopodobnym przypadku, gdy a=17, potrzebna będzie baza liczbowa o długości 17 cyfr. 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 scalającego w przypadku dużych. WebKit implementuje sortowanie przez zastąpienie operatora > w regułach:

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

Proces stopniowy

WebKit używa flagi, która wskazuje, czy wszystkie arkusze stylów najwyższego poziomu (w tym @imports) zostały załadowane. Jeśli styl nie zostanie w pełni załadowany podczas dołączania, używane są uchwyty miejsc i są one oznaczone w dokumencie. Zostaną one obliczone ponownie po wczytaniu arkuszy stylów.

Układ

Gdy renderer zostanie utworzony i dodany do drzewa, nie ma pozycji ani rozmiaru. Obliczanie tych wartości jest nazywane układem lub przeformatowaniem.

HTML korzysta z modelu układu opartego na przepływie, co oznacza, że w większości przypadków możliwe jest obliczenie geometrii w ramach jednego przebiegu. Elementy znajdujące się później „w przepływie” zwykle nie wpływają na geometrię elementów znajdujących się wcześniej „w przepływie”, więc układ może być stosowany w dokumentach od lewej do prawej oraz od góry do dołu. Istnieją wyjątki, na przykład tabele HTML mogą wymagać więcej niż jednej karty.

System współrzędnych jest względny względem ramki głównej. Używane są współrzędne góra i lewo.

Układ jest procesem rekurencyjnym. Rozpoczyna się na głównym mechanizmie renderowania, który odpowiada elementowi <html> dokumentu HTML. Układ działa rekurencyjnie w ramach całej lub części hierarchii ramki, obliczając informacje geometryczne dla każdego wymagającego tego renderowania.

Pozycja głównego renderera to 0,0, a jego wymiary to widoczny obszar przeglądarki.

Wszystkie renderery mają metodę „layout” lub „reflow”. Każdy renderer wywołuje metodę layout swoich elementów, które wymagają ułożenia.

System bitów brudnych

Aby nie tworzyć pełnego układu przy każdej drobnej zmianie, przeglądarki używają systemu „brudnego bitu”. Zmieniony lub dodany mechanizm renderowania oznacza siebie i jego elementy podrzędne jako „brudny”: wymaga układu.

Istnieją 2 flagi: „dirty” (brudny) i „children are dirty” (Dzieci są zanieczyszczone), co oznacza, że chociaż mechanizm renderowania może być prawidłowy, ma co najmniej 1 element podrzędny, który wymaga układu.

Układ globalny i przyrostowy

Układ może być uruchamiany na całym drzewie renderowania – jest to układ „globalny”. Może się to zdarzyć, gdy:

  1. Zmiana globalna stylu, która wpływa na wszystkich procesorów, np. zmiana rozmiaru czcionki.
  2. W wyniku zmiany rozmiaru ekranu

Układ może być stopniowy, a wyświetlane będą tylko renderowane elementy. Może to spowodować pewne uszkodzenia, które będą wymagać dodatkowych układów.

Układ przyrostowy jest uruchamiany (asymetrycznie), gdy renderowanie jest nieaktualne. Na przykład gdy nowe renderowanie zostanie dodane do drzewa renderowania po tym, jak dodatkowe treści zostały przesłane z sieci i dodane do drzewa DOM.

Układ przyrostowy.
Rysunek 18. Układ przyrostowy – rozmieszczane 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” dla przyrostowych układów, a planista uruchamia ich grupowe wykonywanie. WebKit ma też zegar, który wykonuje stopniowe rozmieszczanie – drzewo jest przeszukiwane, a „brudne” renderowanie jest rozmieszczane.

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

Układ globalny jest zwykle uruchamiany synchronicznie.

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

Optymalizacje

Gdy układ jest uruchamiany przez „zmiana 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 nie układ od korzenia. Może się to zdarzyć w przypadkach, gdy zmiana jest lokalna i nie wpływa na jej otoczenie – tak jak w przypadku tekstu wstawionego w pola tekstowe (w przeciwnym razie każde naciśnięcie klawisza powoduje uruchomienie układu, zaczynając od elementu głównego).

Proces układu

Układ ma zwykle taki wzór:

  1. Nadrzędny mechanizm renderowania określa własną szerokość.
  2. Rodzic sprawdza dzieci i:
    1. Umieść podrzędny procesor graficzny (ustaw jego współrzędne x i y).
    2. W razie potrzeby wywołuje układ podrzędny (jeśli jest brudny lub jest to układ globalny albo z jakiegoś innego powodu) i oblicza wysokość podrzędnego.
  3. Element nadrzędny używa łącznej wysokości elementów podrzędnych oraz wysokości marginesów i odstępów, aby ustawić własną wysokość. Będzie ona używana przez element nadrzędny nadrzędnego.
  4. Ustawia bit zanieczyszczenia na wartość „false” (fałsz).

Firefox używa obiektu „state” (nsHTMLReflowState) jako parametru układu (tzw. „reflow”). Stan zawiera m.in. szerokość rodziców.

Dane wyjściowe układu Firefoxa to obiekt „metrics” (nsHTMLReflowMetrics). Zawiera ona wysokość obliczoną przez renderer.

Obliczanie szerokości

Szerokość renderera jest obliczana na podstawie szerokości bloku kontenera, właściwości stylu „szerokość” renderera oraz marginesów i ramek.

Na przykład szerokość tego elementu div:

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

Obliczane przez WebKit w ten sposób(metoda klasy RenderBox calcWidth):

  • Szerokość kontenera to maksymalna wartość z dostępnej szerokości kontenera i 0. W tym przypadku jest to parametr contentWidth, który jest obliczany jako:
clientWidth() - paddingLeft() - paddingRight()

clientWidth i clientHeight reprezentują wnętrze obiektu, z wyjątkiem obramowania i paska przewijania.

  • Szerokość elementów to atrybut stylu „width”. Zostanie ona obliczona jako wartość bezwzględna poprzez obliczenie procentu szerokości kontenera.

  • Dodaliśmy poziome obramowanie i dopełnienie.

Do tej pory było to obliczenie „preferowanej szerokości”. Teraz zostanie obliczona minimalna i maksymalna szerokość.

Jeśli preferowana szerokość jest większa niż maksymalna, zostanie użyta szerokość maksymalna. Jeśli jest mniejsza niż minimalna szerokość (najmniejsza nierozłączna jednostka), używana jest minimalna szerokość.

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

Podział wiersza

Gdy w trakcie renderowania układu okazuje się, że trzeba go przerwać, renderowanie zostaje wstrzymane i przekazywane do elementu nadrzędnego układu, aby przerwać renderowanie. Rodzic tworzy dodatkowe moduły renderujące i wywołuje ich układ.

Malarstwo

Na etapie malowania drzewo renderowania jest przeszukiwane, a metoda „paint()” w renderze jest wywoływana, aby wyświetlić zawartość na ekranie. Malowanie używa komponentu infrastruktury interfejsu użytkownika.

Globalne i przyrostowe

Podobnie jak układ, malowanie może być globalne (malowane jest całe drzewo) lub przyrostowe. Podczas malowania przyrostowego niektóre mechanizmy renderowania zmieniają się w sposób, który nie ma wpływu na całe drzewo. Zmieniony renderer unieważnia swój prostokąt na ekranie. Powoduje to, że system operacyjny traktuje go jako „brudny region” i generuje zdarzenie „paint”. System operacyjny robi to sprytnie i łączy kilka regionów w jeden. W Chrome jest to bardziej skomplikowane, ponieważ procesor graficzny działa w ramach innego procesu niż proces główny. Chrome do pewnego stopnia symuluje działanie systemu operacyjnego. Prezentacja słucha tych zdarzeń i przekazuje wiadomość do głównej platformy renderowania. Drzewo jest przemierzane, dopóki nie zostanie osiągnięty odpowiedni mechanizm renderowania. Odświeży się ona sama (i zwykle też jej elementy podrzędne).

Zamówienie na malowanie

CSS2 określa kolejność procesu malowania. Jest to właściwa kolejność, w jakiej elementy są nakładane w kontekstach nakładania. Ta kolejność ma wpływ na malowanie, ponieważ elementy są malowane od tyłu do przodu. Kolejność nakładania mechanizmu renderowania blokowego:

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

Lista wyświetlania w Firefoksie

Firefox przegląda drzewo renderowania i tworzy listę wyświetlania dla narysowanego prostokąta. Zawiera mechanizmy renderowania właściwe dla prostokąta we właściwej kolejności renderowania (tła mechanizmów renderowania, obramowanie itp.).

Dzięki temu drzewo wystarczy przejść tylko raz, a nie kilka razy – najpierw pomaluj wszystkie tła, potem wszystkie obrazy, a potem wszystkie krawędzie itd.

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

Pamięć prostokąta WebKit

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

Zmiany dynamiczne

Przeglądarki starają się wykonywać jak najmniej działań w odpowiedzi na zmiany. Oznacza to, że zmiany koloru elementu spowodują tylko jego ponowne narysowanie. Zmiany pozycji elementu spowodują jego układ oraz ponowne wyrenderowanie elementu, jego elementów podrzędnych i ewentualnie elementów potomnych. Dodanie węzła DOM spowoduje jego ponowne wyrenderowanie i zmianę układu. Duże zmiany, np. zwiększenie rozmiaru czcionki elementu „html”, spowodują unieważnienie pamięci podręcznej, ponowne ułożenie i odświeżenie całego drzewa.

wątki silnika renderującego;

Silnik renderowania jest jednowątkowy. Prawie wszystko (oprócz operacji sieciowych) odbywa się w jednym wątku. W 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 2–6 połączeń).

pętla zdarzeń,

Główny wątek przeglądarki to pętla zdarzeń. Jest to nieskończona pętla, która utrzymuje proces w ruchu. Czeka na zdarzenia (np. zdarzenia układu i malowania) i przetwarza je. Oto kod Firefoxa dla głównego pętli zdarzeń:

while (!mExiting)
    NS_ProcessNextEvent(thread);

Model wizualny CSS2

Obszar roboczy

Zgodnie z specyfikacją CSS2 termin „canvas” oznacza „przestrzeń, w której renderowana jest struktura formatowania”: miejsce, w którym przeglądarka wyświetla zawartość.

Płótno jest nieskończone w przypadku każdego wymiaru przestrzeni, ale przeglądarki wybierają początkową szerokość na podstawie wymiarów obszaru widoku.

Zgodnie z www.w3.org/TR/CSS2/zindex.html tło jest przezroczyste, jeśli jest zawarte w innym, i ma kolor zdefiniowany przez przeglądarkę, jeśli nie.

Model CSS Box

Model pudełka CSS opisuje prostokątne pudełka generowane dla elementów w drzewie dokumentu i układane zgodnie z modelem formatowania wizualnego.

Każde pole ma obszar treści (np. tekst, obraz) oraz opcjonalne obszary obramowania, marginesu i wypełnienie.

Model pudełka CSS2
Rysunek 19. Model pudełka CSS2

Każdy węzeł generuje 0…n takich pudełek.

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ślnie jest to wartość wbudowana, ale arkusz stylów przeglądarki może ustawiać inne wartości domyślne. Przykład: domyślne wyświetlanie elementu „div” to blok.

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

Schemat pozycjonowania

Istnieją 3 schematy:

  1. Normalna: obiekt jest umieszczany zgodnie ze swoim miejscem w dokumencie. Oznacza to, że jego miejsce w drzewie renderowania jest takie samo jak w drzewie DOM i jest rozmieszczone zgodnie z rodzajem i wymiarami elementu.
  2. Pływający: obiekt jest najpierw rozmieszczony jak w normalnym przepływie, a następnie przesuwany w lewo lub w prawo tak daleko, jak to możliwe.
  3. Bezwzględne: obiekt jest umieszczany w drzewie renderowania w innym miejscu niż w drzewie DOM.

Schemat pozycjonowania jest ustawiany przez właściwość „position” i atrybut „float”.

  • statyczne i względne powodują normalny przepływ
  • bezwzględne i stałe 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.

Sposób rozmieszczenia okna zależy od:

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

Typy pól

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

Zablokuj.
Rysunek 20. Pole blokowania

Pole wstawione: nie ma własnego bloku, ale znajduje się w bloku zawierającym.

pola w tekście;
Rysunek 21. Pola wstawiane w tekście

Bloki są sformatowane pionowo jeden po drugim. Wstawione są formatowane poziomo.

Formatowanie blokowe i wstawianie.
Rysunek 22. Formatowanie blokowe i wstawiane

Ramki wstawia się wewnątrz linii lub „ramek”. Linie mają co najmniej taką samą wysokość jak najwyższa ramka, ale mogą być wyższe, gdy pola są wyrównane na podstawie „odniesienia”. Oznacza to, że dolna część elementu jest wyrównana w punkcie innej ramki niż dolna. Jeśli szerokość kontenera jest niewystarczająca, elementy wewnętrzne zostaną umieszczone w kilku wierszach. Zwykle tak się dzieje w przypadku akapitu.

Linie.
Rysunek 23. Linie

Pozycjonowanie

Krewny

Pozycjonowanie względne – obiekt jest najpierw umieszczony w zwykły sposób, a następnie przesunięty o wymaganą wartość.

Pozycjonowanie względne.
Rys. 24. Pozycjonowanie względne

Swobodne

Pływające pole jest przesunięte w lewo lub w prawo od linii. Ciekawostką jest to, że inne pola płyną wokół niego. Kod HTML:

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

Wyglądać będzie tak:

Liczba zmiennoprzecinkowa.
Rysunek 25. Liczba zmiennoprzecinkowa

Bezwzględne i stałe

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

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

Warstwowy sposób prezentacji

Jest ona określana przez właściwość CSS z-index. Reprezentuje trzeci wymiar pola: jego położenie na osi „z”.

Pudełka są podzielone na stosy (nazywane kontekstami nakładania). W każdej grupie elementy z tyłu będą renderowane jako pierwsze, a elementy z przodu – na wierzchu, bliżej użytkownika. W przypadku nakładania się elementów element z przodu zakrywa poprzedni element.

Warstwy są uporządkowane według właściwości z-index. Pudła z właściwością „z-index” tworzą lokalny stos. Widok zawiera zewnętrzny stos.

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. Umieszczenie stałe

Chociaż czerwony element div występuje przed zielonym w znaczniku i zostałby narysowany wcześniej w zwykłym przepływie, jego właściwość z-index jest wyższa, więc znajduje się wyżej w stosie elementów trzymanym przez element root.

Zasoby

  1. Architektura przeglądarki

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

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

    1. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers (Szybszy HTML i CSS: wewnętrzne mechanizmy silnika układu dla programistów internetowych)
    2. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers (Google tech talk video) (szybszy HTML i CSS: wewnętrzna budowa silnika układu – wykład techniczny Google)
    3. L. David Baron, architekt silnika układu Mozilla
    4. L. David Baron, Mozilla Style System Documentation
    5. Chris Waterson, Uwagi na temat reflow HTML
    6. Chris Waterson, Gecko Overview
    7. Alexander Larsson, The life of an HTML HTTP request (Życie żądania HTTP HTML)
  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 W3C HTML5
    3. Specyfikacja kaskadowych arkuszy stylów poziomu 2, wersja 1 (CSS 2.1)
  6. Instrukcje dotyczące kompilacji przeglądarek

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

Tłumaczenia

このページは、2 回にわたって、日本語に翻訳されています。

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

Dziękuję wszystkim!