Funktionsweise von Browsern

Hinter den Kulissen moderner Webbrowser

Vorwort

Diese umfassende Einführung in die internen Abläufe von WebKit und Gecko ist das Ergebnis umfangreicher Recherchen des israelischen Entwicklers Tali Garsiel. Über mehrere Jahre hinweg hat sie alle veröffentlichten Daten zu Browser-Interna geprüft und viel Zeit damit verbracht, den Quellcode von Webbrowsern zu lesen. Sie schrieb:

Als Webentwickler hilft es Ihnen, die internen Abläufe von Browsern zu verstehen, um bessere Entscheidungen zu treffen und die Gründe für Best Practices bei der Entwicklung zu kennen. Dieses Dokument ist zwar recht lang, wir empfehlen Ihnen jedoch, sich etwas Zeit zu nehmen, um es durchzulesen. Schnell und einfach.

Paul Irish, Chrome Developer Relations

Einführung

Webbrowser sind die am häufigsten verwendete Software. In dieser Einführung erkläre ich, wie sie hinter den Kulissen funktionieren. Wir sehen uns an, was passiert, wenn Sie google.com in die Adressleiste eingeben, bis die Google-Seite im Browserfenster angezeigt wird.

Browser, über die wir sprechen werden

Derzeit werden auf Computern fünf Hauptbrowser verwendet: Chrome, Internet Explorer, Firefox, Safari und Opera. Auf Mobilgeräten sind die wichtigsten Browser Android Browser, iPhone, Opera Mini und Opera Mobile, UC Browser, die Nokia S40/S60-Browser und Chrome. Alle diese Browser basieren auf WebKit, mit Ausnahme der Opera-Browser. Ich werde Beispiele aus den Open-Source-Browsern Firefox und Chrome sowie Safari (teilweise Open Source) zeigen. Laut StatCounter-Statistiken (Stand: Juni 2013) machen Chrome, Firefox und Safari etwa 71 % der weltweiten Desktop-Browser-Nutzung aus. Auf Mobilgeräten machen Android-Browser, iPhone und Chrome etwa 54 % der Nutzung aus.

Die Hauptfunktion des Browsers

Die Hauptfunktion eines Browsers besteht darin, die von Ihnen ausgewählte Webressource darzustellen, indem er sie vom Server anfordert und im Browserfenster anzeigt. Die Ressource ist in der Regel ein HTML-Dokument, kann aber auch eine PDF-Datei, ein Bild oder ein anderer Inhaltstyp sein. Der Speicherort der Ressource wird vom Nutzer mithilfe eines URI (Uniform Resource Identifier) angegeben.

Wie der Browser HTML-Dateien interpretiert und anzeigt, ist in den HTML- und CSS-Spezifikationen festgelegt. Diese Spezifikationen werden vom W3C (World Wide Web Consortium) verwaltet, der Standardisierungsorganisation für das Web. Jahrelang hielten sich Browser nur an einen Teil der Spezifikationen und entwickelten ihre eigenen Erweiterungen. Das führte zu erheblichen Kompatibilitätsproblemen für Webautoren. Heute entsprechen die meisten Browser mehr oder weniger den Spezifikationen.

Browser-Benutzeroberflächen haben viele Gemeinsamkeiten. Zu den gängigen Elementen der Benutzeroberfläche gehören:

  1. Adressleiste zum Einfügen eines URI
  2. Schaltflächen „Zurück“ und „Weiter“
  3. Lesezeichenoptionen
  4. Schaltflächen zum Aktualisieren und Beenden, um das Laden aktueller Dokumente zu aktualisieren oder zu beenden
  5. Button „Startseite“, über den Sie zur Startseite gelangen

Seltsamerweise ist die Benutzeroberfläche des Browsers in keiner formalen Spezifikation festgelegt. Sie basiert lediglich auf Best Practices, die sich im Laufe der Jahre durch Erfahrung und durch die Nachahmung anderer Browser entwickelt haben. In der HTML5-Spezifikation werden keine UI-Elemente definiert, die ein Browser haben muss, aber es werden einige gängige Elemente aufgeführt. Dazu gehören die Adressleiste, die Statusleiste und die Symbolleiste. Natürlich gibt es auch Funktionen, die nur in einem bestimmten Browser verfügbar sind, z. B. den Download-Manager von Firefox.

Übergeordnete Infrastruktur

Die Hauptkomponenten des Browsers sind:

  1. Die Benutzeroberfläche: Dazu gehören die Adressleiste, die Schaltflächen „Zurück“ und „Vorwärts“, das Lesezeichenmenü usw. Also alle Teile der Browseranzeige mit Ausnahme des Fensters, in dem die angeforderte Seite zu sehen ist.
  2. Die Browser-Engine: koordiniert Aktionen zwischen der Benutzeroberfläche und der Rendering-Engine.
  3. Rendering-Engine: zuständig für die Anzeige der angeforderten Inhalte. Wenn der angeforderte Inhalt beispielsweise HTML ist, parst die Rendering-Engine HTML und CSS und zeigt den geparsten Inhalt auf dem Bildschirm an.
  4. Netzwerk: Für Netzwerkaufrufe wie HTTP-Anfragen werden unterschiedliche Implementierungen für verschiedene Plattformen hinter einer plattformunabhängigen Schnittstelle verwendet.
  5. UI-Backend: Wird zum Zeichnen grundlegender Widgets wie Kombinationsfelder und Fenster verwendet. Dieses Backend stellt eine allgemeine Schnittstelle bereit, die nicht plattformspezifisch ist. Im Hintergrund werden Methoden der Benutzeroberfläche des Betriebssystems verwendet.
  6. JavaScript-Interpreter. Wird zum Parsen und Ausführen von JavaScript-Code verwendet.
  7. Datenspeicherung: Dies ist eine Persistenzebene. Der Browser muss möglicherweise alle Arten von Daten lokal speichern, z. B. Cookies. Browser unterstützen auch Speichermechanismen wie localStorage, IndexedDB, WebSQL und FileSystem.
Browserkomponenten
Abbildung 1: Browserkomponenten

Browser wie Chrome führen mehrere Instanzen der Rendering-Engine aus: eine für jeden Tab. Jeder Tab wird in einem separaten Prozess ausgeführt.

Rendering-Engines

Die Aufgabe der Rendering-Engine ist das Rendern, also die Anzeige der angeforderten Inhalte auf dem Browserbildschirm.

Standardmäßig kann die Rendering-Engine HTML- und XML-Dokumente sowie Bilder anzeigen. Über Plug-ins oder Erweiterungen können andere Datentypen angezeigt werden, z. B. PDF-Dokumente mit einem PDF-Viewer-Plug-in. In diesem Kapitel konzentrieren wir uns jedoch auf den Hauptanwendungsfall: die Anzeige von HTML und Bildern, die mit CSS formatiert sind.

Verschiedene Browser verwenden unterschiedliche Rendering-Engines: Internet Explorer verwendet Trident, Firefox verwendet Gecko, Safari verwendet WebKit. Chrome und Opera (ab Version 15) verwenden Blink, eine Abspaltung von WebKit.

WebKit ist eine Open-Source-Rendering-Engine, die ursprünglich für die Linux-Plattform entwickelt und von Apple für Mac und Windows angepasst wurde.

Der Hauptablauf

Die Rendering-Engine beginnt, die Inhalte des angeforderten Dokuments von der Netzwerkschicht abzurufen. Dies geschieht normalerweise in 8-kB-Blöcken.

Danach sieht der grundlegende Ablauf der Rendering-Engine so aus:

Grundlegender Ablauf der Rendering-Engine
Abbildung 2: Grundlegender Ablauf der Rendering-Engine

Die Rendering-Engine beginnt mit dem Parsen des HTML-Dokuments und konvertiert Elemente in DOM-Knoten in einer Baumstruktur, die als „Inhaltsbaum“ bezeichnet wird. Die Engine parst die Stildaten sowohl in externen CSS-Dateien als auch in Stilelementen. Die Stilinformationen werden zusammen mit den visuellen Anweisungen im HTML-Code verwendet, um einen weiteren Baum zu erstellen: die Renderstruktur.

Der Renderbaum enthält Rechtecke mit visuellen Attributen wie Farbe und Abmessungen. Die Rechtecke sind in der richtigen Reihenfolge, um auf dem Bildschirm angezeigt zu werden.

Nach der Erstellung des Renderbaums wird ein Layout-Prozess durchlaufen. Das bedeutet, dass Sie jedem Knoten die genauen Koordinaten zuweisen, an denen er auf dem Bildschirm angezeigt werden soll. Die nächste Phase ist das Zeichnen. Der Renderbaum wird durchlaufen und jeder Knoten wird mithilfe der UI-Backend-Ebene gezeichnet.

Es ist wichtig zu wissen, dass dies ein schrittweiser Prozess ist. Für eine bessere Nutzererfahrung versucht die Rendering-Engine, Inhalte so schnell wie möglich auf dem Bildschirm anzuzeigen. Es wird nicht gewartet, bis der gesamte HTML-Code geparst wurde, bevor mit dem Erstellen und Layouten der Renderstruktur begonnen wird. Teile des Inhalts werden geparst und angezeigt, während der Prozess mit dem Rest des Inhalts fortgesetzt wird, der weiterhin aus dem Netzwerk kommt.

Beispiele für Hauptabläufe

WebKit-Hauptablauf.
Abbildung 3: WebKit-Hauptablauf
Hauptablauf der Gecko-Rendering-Engine von Mozilla.
Abbildung 4: Hauptablauf der Gecko-Rendering-Engine von Mozilla

In den Abbildungen 3 und 4 sehen Sie, dass WebKit und Gecko zwar eine etwas andere Terminologie verwenden, der Ablauf aber im Wesentlichen derselbe ist.

Gecko nennt den Baum der visuell formatierten Elemente einen „Frame-Baum“. Jedes Element ist ein Frame. WebKit verwendet den Begriff „Render Tree“ und besteht aus „Render Objects“. WebKit verwendet den Begriff „Layout“ für die Platzierung von Elementen, während Gecko ihn „Reflow“ nennt. „Attachment“ ist der WebKit-Begriff für das Verbinden von DOM-Knoten und visuellen Informationen, um den Renderbaum zu erstellen. Ein kleiner, nicht semantischer Unterschied besteht darin, dass Gecko eine zusätzliche Ebene zwischen dem HTML- und dem DOM-Baum hat. Sie wird als „Inhaltssenke“ bezeichnet und ist eine Factory zum Erstellen von DOM-Elementen. Wir werden jeden Teil des Ablaufs besprechen:

Parsen – Allgemeines

Da das Parsen ein sehr wichtiger Prozess innerhalb der Rendering-Engine ist, werden wir uns etwas genauer damit befassen. Beginnen wir mit einer kurzen Einführung in das Parsen.

Ein Dokument zu parsen bedeutet, es in eine Struktur zu übersetzen, die der Code verwenden kann. Das Ergebnis des Parsens ist in der Regel ein Baum von Knoten, die die Struktur des Dokuments darstellen. Dies wird als Parse-Baum oder Syntaxbaum bezeichnet.

Beispiel: Beim Parsen des Ausdrucks 2 + 3 - 1 könnte dieser Baum zurückgegeben werden:

Strukturknoten für mathematischen Ausdruck.
Abbildung 5: Knoten im mathematischen Ausdrucksbaum

Grammatik

Das Parsen basiert auf den Syntaxregeln, denen das Dokument folgt: der Sprache oder dem Format, in dem es geschrieben wurde. Jedes Format, das Sie parsen können, muss eine deterministische Grammatik haben, die aus Vokabular und Syntaxregeln besteht. Sie wird als kontextfreie Grammatik bezeichnet. Menschliche Sprachen sind keine solchen Sprachen und können daher nicht mit herkömmlichen Parsing-Techniken geparst werden.

Kombination aus Parser und Lexer

Das Parsen kann in zwei Teilprozesse unterteilt werden: lexikalische Analyse und Syntaxanalyse.

Bei der lexikalischen Analyse wird die Eingabe in Tokens aufgeteilt. Tokens sind das Vokabular der Sprache: die Sammlung gültiger Bausteine. In einer menschlichen Sprache besteht es aus allen Wörtern, die im Wörterbuch für diese Sprache vorkommen.

Bei der Syntaxanalyse werden die Syntaxregeln der Sprache angewendet.

Parser teilen die Arbeit normalerweise zwischen zwei Komponenten auf: dem Lexer (manchmal auch Tokenizer genannt), der die Eingabe in gültige Tokens aufteilt, und dem Parser, der den Parse-Baum erstellt, indem er die Dokumentstruktur gemäß den Syntaxregeln der Sprache analysiert.

Der Lexer weiß, wie irrelevante Zeichen wie Leerzeichen und Zeilenumbrüche entfernt werden.

Vom Quelldokument zu Parse-Bäumen
Abbildung 6: Vom Quelldokument zu Parse-Bäumen

Der Parsing-Prozess ist iterativ. Der Parser fordert in der Regel ein neues Token vom Lexer an und versucht, das Token mit einer der Syntaxregeln abzugleichen. Wenn eine Regel übereinstimmt, wird dem Parse-Baum ein Knoten hinzugefügt, der dem Token entspricht, und der Parser fordert ein weiteres Token an.

Wenn keine Regel übereinstimmt, speichert der Parser das Token intern und fordert so lange Tokens an, bis eine Regel gefunden wird, die mit allen intern gespeicherten Tokens übereinstimmt. Wenn keine Regel gefunden wird, löst der Parser eine Ausnahme aus. Das bedeutet, dass das Dokument ungültig war und Syntaxfehler enthielt.

Übersetzung

In vielen Fällen ist der Parse-Baum nicht das Endprodukt. Das Parsen wird häufig bei der Übersetzung verwendet, um das Eingabedokument in ein anderes Format zu transformieren. Ein Beispiel ist die Kompilierung. Der Compiler, der den Quellcode in Maschinencode kompiliert, parst ihn zuerst in einen Parse-Baum und übersetzt den Baum dann in ein Maschinencode-Dokument.

Kompilierungsablauf
Abbildung 7: Kompilierungsablauf

Parsing-Beispiel

In Abbildung 5 haben wir einen Parse-Baum aus einem mathematischen Ausdruck erstellt. Versuchen wir, eine einfache mathematische Sprache zu definieren und den Parsing-Prozess zu beobachten.

Syntax:

  1. Die Bausteine der Sprachsyntax sind Ausdrücke, Begriffe und Vorgänge.
  2. Unsere Sprache kann eine beliebige Anzahl von Ausdrücken enthalten.
  3. Ein Ausdruck ist definiert als „Begriff“, gefolgt von einem „Vorgang“ und einem weiteren Begriff.
  4. Ein Vorgang ist ein Plus- oder Minus-Token
  5. Ein Term ist ein Ganzzahl-Token oder ein Ausdruck.

Analysieren wir die Eingabe 2 + 3 - 1.

Der erste Teilstring, der mit einer Regel übereinstimmt, ist 2: Gemäß Regel 5 ist es ein Begriff. Die zweite Übereinstimmung ist 2 + 3. Dies entspricht der dritten Regel: ein Begriff, gefolgt von einer Operation, gefolgt von einem weiteren Begriff. Die nächste Übereinstimmung wird erst am Ende der Eingabe gefunden. 2 + 3 - 1 ist ein Ausdruck, da wir bereits wissen, dass 2 + 3 ein Term ist. Wir haben also einen Term, gefolgt von einer Operation, gefolgt von einem weiteren Term. 2 + + entspricht keiner Regel und ist daher eine ungültige Eingabe.

Formale Definitionen für Vokabular und Syntax

Das Vokabular wird normalerweise durch reguläre Ausdrücke ausgedrückt.

Die Sprache wird beispielsweise so definiert:

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

Wie Sie sehen, werden Ganzzahlen durch einen regulären Ausdruck definiert.

Die Syntax wird normalerweise in einem Format namens BNF definiert. Unsere Sprache wird so definiert:

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

Wir haben gesagt, dass eine Sprache von regulären Parsern geparst werden kann, wenn ihre Grammatik eine kontextfreie Grammatik ist. Eine intuitive Definition einer kontextfreien Grammatik ist eine Grammatik, die vollständig in BNF ausgedrückt werden kann. Eine formale Definition finden Sie im Wikipedia-Artikel zu kontextfreien Grammatiken.

Parser-Typen

Es gibt zwei Arten von Parsern: Top-down-Parser und Bottom-up-Parser. Eine intuitive Erklärung ist, dass Top-Down-Parser die allgemeine Struktur der Syntax untersuchen und versuchen, eine Regelübereinstimmung zu finden. Bottom-up-Parser beginnen mit der Eingabe und wandeln sie schrittweise in die Syntaxregeln um, beginnend mit den Regeln auf niedriger Ebene, bis die Regeln auf hoher Ebene erfüllt sind.

Sehen wir uns an, wie die beiden Parser-Typen unser Beispiel parsen.

Der Top-Down-Parser beginnt mit der Regel auf höherer Ebene: Er identifiziert 2 + 3 als Ausdruck. Anschließend wird 2 + 3 - 1 als Ausdruck identifiziert. Der Prozess der Identifizierung des Ausdrucks entwickelt sich weiter und entspricht den anderen Regeln, aber der Ausgangspunkt ist die Regel auf der höchsten Ebene.

Der Bottom-up-Parser scannt die Eingabe, bis eine Regel übereinstimmt. Anschließend wird die übereinstimmende Eingabe durch die Regel ersetzt. Dies wird bis zum Ende der Eingabe fortgesetzt. Der teilweise übereinstimmende Ausdruck wird auf dem Stack des Parsers platziert.

Stapel Eingabe
2 + 3 - 1
Begriff + 3 - 1
Begriffsvorgang 3–1
Ausdruck – 1
Ausdrucksvorgang 1
Ausdruck -

Diese Art von Bottom-up-Parser wird als Shift-Reduce-Parser bezeichnet, da die Eingabe nach rechts verschoben wird (stellen Sie sich einen Zeiger vor, der zuerst auf den Anfang der Eingabe zeigt und sich nach rechts bewegt) und schrittweise auf Syntaxregeln reduziert wird.

Parser automatisch generieren

Es gibt Tools, mit denen sich Parser generieren lassen. Sie füttern sie mit der Grammatik Ihrer Sprache – dem Vokabular und den Syntaxregeln – und sie generieren einen funktionierenden Parser. Das Erstellen eines Parsers erfordert ein tiefes Verständnis des Parsings. Es ist nicht einfach, einen optimierten Parser manuell zu erstellen. Daher können Parser-Generatoren sehr nützlich sein.

WebKit verwendet zwei bekannte Parser-Generatoren: Flex zum Erstellen eines Lexers und Bison zum Erstellen eines Parsers (möglicherweise kennen Sie sie unter den Namen Lex und Yacc). Die Flex-Eingabe ist eine Datei, die Definitionen der Tokens als reguläre Ausdrücke enthält. Die Eingabe von Bison sind die Syntaxregeln der Sprache im BNF-Format.

HTML-Parser

Die Aufgabe des HTML-Parsers besteht darin, das HTML-Markup in einen Parse-Baum zu parsen.

HTML-Grammatik

Das Vokabular und die Syntax von HTML sind in Spezifikationen definiert, die von der W3C-Organisation erstellt wurden.

Wie wir in der Einführung zum Parsen gesehen haben, kann die Grammatiksyntax formal mit Formaten wie BNF definiert werden.

Leider gelten die herkömmlichen Parser-Themen nicht für HTML. Ich habe sie nicht nur zum Spaß erwähnt, sondern weil sie beim Parsen von CSS und JavaScript verwendet werden. HTML lässt sich nicht einfach durch eine kontextfreie Grammatik definieren, die Parser benötigen.

Es gibt ein formales Format für die Definition von HTML – DTD (Document Type Definition) –, aber es ist keine kontextfreie Grammatik.

Das mag auf den ersten Blick seltsam erscheinen, da HTML und XML sich sehr ähnlich sind. Es gibt viele XML-Parser. Es gibt eine XML-Variante von HTML – XHTML. Was ist also der große Unterschied?

Der Unterschied besteht darin, dass der HTML-Ansatz „nachsichtiger“ ist: Sie können bestimmte Tags weglassen (die dann implizit hinzugefügt werden) oder manchmal Start- oder End-Tags weglassen usw. Im Großen und Ganzen ist es eine „weiche“ Syntax, im Gegensatz zur starren und anspruchsvollen Syntax von XML.

Dieses scheinbar kleine Detail macht einen großen Unterschied. Einerseits ist dies der Hauptgrund, warum HTML so beliebt ist: Es verzeiht Fehler und macht Webautoren das Leben leicht. Andererseits ist es dadurch schwierig, eine formale Grammatik zu schreiben. Zusammenfassend lässt sich sagen, dass HTML von herkömmlichen Parsern nicht einfach geparst werden kann, da die Grammatik nicht kontextfrei ist. HTML kann nicht von XML-Parsern geparst werden.

HTML-DTD

Die HTML-Definition ist im DTD-Format. Dieses Format wird verwendet, um Sprachen der SGML-Familie zu definieren. Das Format enthält Definitionen für alle zulässigen Elemente, ihre Attribute und die Hierarchie. Wie wir bereits gesehen haben, bildet die HTML-DTD keine kontextfreie Grammatik.

Es gibt verschiedene Varianten der DTD. Der strikte Modus entspricht ausschließlich den Spezifikationen, andere Modi unterstützen jedoch auch Markup, das in der Vergangenheit von Browsern verwendet wurde. Der Zweck ist die Abwärtskompatibilität mit älteren Inhalten. Die aktuelle strikte DTD finden Sie hier: www.w3.org/TR/html4/strict.dtd

DOM

Der Ausgabe-Baum (der „Parse-Baum“) ist ein Baum aus DOM-Element- und Attributknoten. DOM steht für „Document Object Model“. Es ist die Objektdarstellung des HTML-Dokuments und die Schnittstelle von HTML-Elementen zur Außenwelt, z. B. JavaScript.

Die Wurzel des Baums ist das Objekt „Document“.

Das DOM hat eine fast 1:1-Beziehung zum Markup. Beispiel:

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

Dieses Markup würde in den folgenden DOM-Baum übersetzt werden:

DOM-Baum des Beispiel-Markups
Abbildung 8: DOM-Baum des Beispiel-Markups

Wie HTML wird auch DOM von der W3C-Organisation spezifiziert. Weitere Informationen finden Sie unter www.w3.org/DOM/DOMTR. Es ist eine generische Spezifikation zum Bearbeiten von Dokumenten. Ein bestimmtes Modul beschreibt HTML-spezifische Elemente. Die HTML-Definitionen finden Sie hier: www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html.

Wenn ich sage, dass der Baum DOM-Knoten enthält, meine ich, dass der Baum aus Elementen besteht, die eine der DOM-Schnittstellen implementieren. Browser verwenden konkrete Implementierungen, die andere Attribute haben, die vom Browser intern verwendet werden.

Der Parsing-Algorithmus

Wie wir in den vorherigen Abschnitten gesehen haben, kann HTML nicht mit den regulären Top-Down- oder Bottom-Up-Parsern geparst werden.

Hierfür gibt es folgende Gründe:

  1. Die Sprache ist sehr fehlertolerant.
  2. Die Tatsache, dass Browser eine herkömmliche Fehlertoleranz haben, um bekannte Fälle von ungültigem HTML zu unterstützen.
  3. Der Parsing-Prozess ist wiedereintrittsfähig. Bei anderen Sprachen ändert sich die Quelle während des Parsens nicht. In HTML kann dynamischer Code (z. B. Skriptelemente mit document.write()-Aufrufen) jedoch zusätzliche Tokens hinzufügen, sodass der Parsing-Prozess die Eingabe tatsächlich ändert.

Da die regulären Parsing-Techniken nicht verwendet werden können, erstellen Browser benutzerdefinierte Parser zum Parsen von HTML.

Der Parsing-Algorithmus wird in der HTML5-Spezifikation ausführlich beschrieben. Der Algorithmus besteht aus zwei Phasen: Tokenisierung und Baumerstellung.

Die Tokenisierung ist die lexikalische Analyse, bei der die Eingabe in Tokens geparst wird. Zu den HTML-Tokens gehören Start-Tags, End-Tags, Attributnamen und Attributwerte.

Der Tokenizer erkennt das Token, übergibt es an den Baumkonstruktor und verarbeitet das nächste Zeichen, um das nächste Token zu erkennen, und so weiter bis zum Ende der Eingabe.

HTML-Parsing-Ablauf (aus der HTML5-Spezifikation)
Abbildung 9: HTML-Parsing-Ablauf (aus der HTML5-Spezifikation)

Der Tokenisierungsalgorithmus

Die Ausgabe des Algorithmus ist ein HTML-Token. Der Algorithmus wird als Zustandsautomat dargestellt. Jeder Zustand verbraucht ein oder mehrere Zeichen des Eingabestroms und aktualisiert den nächsten Zustand entsprechend diesen Zeichen. Die Entscheidung wird durch den aktuellen Tokenisierungsstatus und den Status der Baumkonstruktion beeinflusst. Das bedeutet, dass dasselbe verarbeitete Zeichen je nach aktuellem Zustand unterschiedliche Ergebnisse für den korrekten nächsten Zustand liefert. Der Algorithmus ist zu komplex, um ihn vollständig zu beschreiben. Sehen wir uns daher ein einfaches Beispiel an, das uns hilft, das Prinzip zu verstehen.

Einfaches Beispiel – Tokenisierung des folgenden HTML-Codes:

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

Der Anfangszustand ist der „Datenzustand“. Wenn das Zeichen < gefunden wird, ändert sich der Status in Tag open state. Wenn ein a-z-Zeichen verwendet wird, wird ein „Start-Tag-Token“ erstellt und der Status wird in „Tag-Name-Status“ geändert. Wir bleiben in diesem Zustand, bis das Zeichen > verarbeitet wurde. Jedes Zeichen wird an den neuen Tokennamen angehängt. In unserem Fall ist das erstellte Token ein html-Token.

Wenn das Tag > erreicht wird, wird das aktuelle Token ausgegeben und der Status ändert sich wieder in Datenstatus. Das <body>-Tag wird mit denselben Schritten behandelt. Bisher wurden die Tags html und body ausgegeben. Wir sind jetzt wieder beim Datenstatus. Durch die Verarbeitung des H-Zeichens von Hello world wird ein Zeichen-Token erstellt und ausgegeben. Dies wird fortgesetzt, bis das < von </body> erreicht ist. Wir geben für jedes Zeichen von Hello world ein Zeichen-Token aus.

Wir sind jetzt wieder im geöffneten Zustand des Tags. Wenn die nächste Eingabe / verarbeitet wird, wird ein end tag token erstellt und der Tag-Namensstatus aufgerufen. Wir bleiben in diesem Zustand, bis wir > erreichen. Dann wird das neue Tag-Token ausgegeben und wir kehren zum Datenzustand zurück. Die Eingabe </html> wird wie im vorherigen Fall behandelt.

Beispieleingabe tokenisieren
Abbildung 10: Tokenisierung der Beispieleingabe

Baumerstellungsalgorithmus

Wenn der Parser erstellt wird, wird das Document-Objekt erstellt. In der Phase der Baumkonstruktion wird der DOM-Baum mit dem Dokument als Wurzel geändert und es werden Elemente hinzugefügt. Jeder vom Tokenizer ausgegebene Knoten wird vom Baumkonstruktor verarbeitet. Für jedes Token wird in der Spezifikation definiert, welches DOM-Element dafür relevant ist und für dieses Token erstellt wird. Das Element wird dem DOM-Baum und dem Stack der offenen Elemente hinzugefügt. Dieser Stack wird verwendet, um Verschachtelungsfehler und nicht geschlossene Tags zu korrigieren. Der Algorithmus wird auch als Zustandsautomat beschrieben. Die Zustände werden als „Einfügemodi“ bezeichnet.

Sehen wir uns den Prozess der Baumerstellung für die Beispieleingabe an:

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

Die Eingabe für die Phase der Baumkonstruktion ist eine Sequenz von Tokens aus der Tokenisierungsphase. Der erste Modus ist der Anfangsmodus. Wenn das Token „html“ empfangen wird, wird in den Modus before html gewechselt und das Token in diesem Modus noch einmal verarbeitet. Dadurch wird das HTMLHtmlElement-Element erstellt, das an das Stamm-Document-Objekt angehängt wird.

Der Status wird in before head geändert. Anschließend wird das „body“-Token empfangen. Ein HTMLHeadElement wird implizit erstellt, obwohl wir kein „head“-Token haben, und dem Baum hinzugefügt.

Wir wechseln jetzt zum Modus Im Head und dann zu Nach dem Head. Das Body-Token wird noch einmal verarbeitet, ein HTMLBodyElement wird erstellt und eingefügt und der Modus wird auf „in body“ umgestellt.

Die Zeichen-Tokens des Strings „Hello world“ werden jetzt empfangen. Das erste Zeichen führt zur Erstellung und Einfügung eines „Text“-Knotens und die anderen Zeichen werden an diesen Knoten angehängt.

Der Empfang des End-Tokens für den Body führt zu einem Wechsel in den Modus after body. Wir erhalten jetzt das HTML-End-Tag, das uns in den Modus after after body versetzt. Wenn das Token für das Dateiende empfangen wird, wird das Parsen beendet.

Baumstruktur des HTML-Beispiels.
Abbildung 11: Baumstruktur des HTML-Beispiels

Aktionen nach Abschluss des Parsens

In dieser Phase markiert der Browser das Dokument als interaktiv und beginnt mit dem Parsen von Skripts, die sich im Modus „Verzögert“ befinden, also Skripts, die nach dem Parsen des Dokuments ausgeführt werden sollen. Der Dokumentstatus wird dann auf „complete“ gesetzt und ein „load“-Ereignis wird ausgelöst.

Die vollständigen Algorithmen für die Tokenisierung und die Baumerstellung finden Sie in der HTML5-Spezifikation.

Fehlertoleranz von Browsern

Auf einer HTML-Seite wird nie der Fehler „Ungültige Syntax“ angezeigt. Browser korrigieren ungültige Inhalte und fahren fort.

Sehen wir uns zum Beispiel diesen HTML-Code an:

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

Ich habe wahrscheinlich gegen eine Million Regeln verstoßen („mytag“ ist kein Standard-Tag, die Elemente „p“ und „div“ sind falsch verschachtelt usw.), aber der Browser zeigt den Text trotzdem korrekt an und meldet keine Fehler. Ein Großteil des Parser-Codes dient also dazu, die Fehler der HTML-Autoren zu beheben.

Die Fehlerbehandlung ist in Browsern recht einheitlich, aber erstaunlicherweise war sie bisher nicht Teil der HTML-Spezifikationen. Wie Lesezeichen und die Schaltflächen „Zurück“ und „Weiter“ ist es einfach etwas, das sich im Laufe der Jahre in Browsern entwickelt hat. Es gibt bekannte ungültige HTML-Konstrukte, die auf vielen Websites wiederholt werden. Die Browser versuchen, diese so zu korrigieren, dass sie mit anderen Browsern kompatibel sind.

In der HTML5-Spezifikation sind einige dieser Anforderungen definiert. (WebKit fasst dies im Kommentar am Anfang der HTML-Parser-Klasse gut zusammen.)

Der Parser parst die tokenisierte Eingabe in das Dokument und erstellt den Dokumentbaum. Wenn das Dokument wohlgeformt ist, ist das Parsen unkompliziert.

Leider müssen wir viele HTML-Dokumente verarbeiten, die nicht korrekt formatiert sind. Der Parser muss also tolerant gegenüber Fehlern sein.

Wir müssen mindestens die folgenden Fehlerbedingungen berücksichtigen:

  1. Das hinzugefügte Element ist in einem äußeren Tag explizit verboten. In diesem Fall sollten wir alle Tags bis zu dem Tag schließen, das das Element verbietet, und es danach hinzufügen.
  2. Wir dürfen das Element nicht direkt hinzufügen. Es kann sein, dass die Person, die das Dokument geschrieben hat, ein Tag dazwischen vergessen hat (oder dass das Tag dazwischen optional ist). Das kann bei den folgenden Tags der Fall sein: HTML, HEAD, BODY, TBODY, TR, TD, LI (habe ich welche vergessen?).
  3. Wir möchten ein Blockelement in ein Inline-Element einfügen. Schließen Sie alle Inline-Elemente bis zum nächsthöheren Blockelement.
  4. Wenn das nicht hilft, schließe Elemente, bis wir das Element hinzufügen dürfen, oder ignoriere das Tag.

Hier sind einige Beispiele für die Fehlertoleranz von WebKit:

</br> statt <br>

Einige Websites verwenden </br> anstelle von <br>. Damit die Kompatibilität mit IE und Firefox gewährleistet ist, behandelt WebKit dies wie <br>.

Der Code:

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

Die Fehlerbehandlung erfolgt intern und wird dem Nutzer nicht angezeigt.

Ein einzelner Tisch

Eine verwaiste Tabelle ist eine Tabelle innerhalb einer anderen Tabelle, die sich jedoch nicht in einer Tabellenzelle befindet.

Beispiel:

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

WebKit ändert die Hierarchie in zwei gleichgeordnete Tabellen:

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

Der Code:

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

WebKit verwendet einen Stack für den aktuellen Elementinhalt: Die innere Tabelle wird aus dem Stack der äußeren Tabelle entfernt. Die Tabellen sind jetzt gleichgeordnet.

Verschachtelte Formularelemente

Wenn der Nutzer ein Formular in ein anderes Formular einfügt, wird das zweite Formular ignoriert.

Der Code:

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

Eine zu tiefe Tag-Hierarchie

Der Kommentar spricht für sich selbst.

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

Falsch platzierte HTML- oder Body-End-Tags

Auch hier spricht der Kommentar für sich selbst.

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

Webautoren sollten also aufpassen – es sei denn, sie möchten als Beispiel in einem WebKit-Code-Snippet zur Fehlertoleranz erscheinen – und wohlgeformtes HTML schreiben.

CSS-Parsing

Erinnern Sie sich an die Parsing-Konzepte in der Einführung? Im Gegensatz zu HTML ist CSS eine kontextfreie Grammatik und kann mit den in der Einführung beschriebenen Parsern geparst werden. Tatsächlich werden in der CSS-Spezifikation die lexikalische und die Syntaxgrammatik von CSS definiert.

Sehen wir uns einige Beispiele an:

Die lexikalische Grammatik (Vokabular) wird durch reguläre Ausdrücke für jedes Token definiert:

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“ ist die Abkürzung für „identifier“ (Kennung), z. B. ein Klassenname. „name“ ist eine Element-ID (auf die mit „#“ verwiesen wird)

Die Syntaxgrammatik wird in BNF beschrieben.

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*] ')' ]
  ;

Erklärung:

Ein Regelsatz hat die folgende Struktur:

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

div.error und a.error sind Selektoren. Der Teil innerhalb der geschweiften Klammern enthält die Regeln, die von diesem Regelsatz angewendet werden. Diese Struktur wird in der folgenden Definition formal definiert:

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

Das bedeutet, dass ein Regelsatz ein Selektor oder optional eine Reihe von Selektoren ist, die durch ein Komma und Leerzeichen getrennt sind (S steht für Leerzeichen). Ein Regelsatz enthält geschweifte Klammern und darin eine Deklaration oder optional mehrere Deklarationen, die durch ein Semikolon getrennt sind. „declaration“ und „selector“ werden in den folgenden BNF-Definitionen definiert.

WebKit-CSS-Parser

WebKit verwendet die Parser-Generatoren Flex und Bison, um Parser automatisch aus den CSS-Grammatikdateien zu erstellen. Wie Sie aus der Einführung in Parser wissen, erstellt Bison einen Bottom-up-Shift-Reduce-Parser. Firefox verwendet einen manuell geschriebenen Top-Down-Parser. In beiden Fällen wird jede CSS-Datei in ein StyleSheet-Objekt geparst. Jedes Objekt enthält CSS-Regeln. Die CSS-Regelobjekte enthalten Selektor- und Deklarationsobjekte sowie andere Objekte, die der CSS-Grammatik entsprechen.

CSS wird geparst.
Abbildung 12: CSS parsen

Verarbeitungsreihenfolge für Skripts und Stylesheets

Scripts

Das Modell des Webs ist synchron. Autoren erwarten, dass Skripts sofort geparst und ausgeführt werden, wenn der Parser ein <script>-Tag erreicht. Das Parsen des Dokuments wird angehalten, bis das Script ausgeführt wurde. Wenn das Skript extern ist, muss die Ressource zuerst aus dem Netzwerk abgerufen werden. Dies geschieht ebenfalls synchron und das Parsen wird angehalten, bis die Ressource abgerufen wurde. Dies war viele Jahre lang das Modell und ist auch in den HTML4- und HTML5-Spezifikationen festgelegt. Autoren können einem Script das Attribut „defer“ hinzufügen. In diesem Fall wird das Parsen des Dokuments nicht angehalten und das Script wird ausgeführt, nachdem das Dokument geparst wurde. In HTML5 gibt es eine Option, mit der das Script als asynchron gekennzeichnet werden kann, sodass es von einem anderen Thread geparst und ausgeführt wird.

Spekulatives Parsen

Sowohl WebKit als auch Firefox führen diese Optimierung durch. Während die Scripts ausgeführt werden, parst ein anderer Thread den Rest des Dokuments, ermittelt, welche anderen Ressourcen aus dem Netzwerk geladen werden müssen, und lädt sie. So können Ressourcen über parallele Verbindungen geladen werden und die Gesamtgeschwindigkeit wird verbessert. Hinweis: Der spekulative Parser parst nur Verweise auf externe Ressourcen wie externe Skripts, Stylesheets und Bilder. Er ändert nicht den DOM-Baum, das bleibt dem Hauptparser überlassen.

Style sheets

Stylesheets haben dagegen ein anderes Modell. Da Stylesheets den DOM-Baum nicht ändern, gibt es eigentlich keinen Grund, auf sie zu warten und das Parsen des Dokuments zu stoppen. Es gibt jedoch ein Problem, wenn Skripts während der Dokumentanalyse Stilinformationen anfordern. Wenn der Stil noch nicht geladen und geparst wurde, erhält das Skript falsche Antworten, was offenbar zu vielen Problemen geführt hat. Es scheint sich um einen Sonderfall zu handeln, ist aber recht häufig. Firefox blockiert alle Skripts, wenn ein Stylesheet noch geladen und geparst wird. WebKit blockiert Skripts nur, wenn sie versuchen, auf bestimmte Stileigenschaften zuzugreifen, die von nicht geladenen Stylesheets betroffen sein könnten.

Renderbaum-Konstruktion

Während der DOM-Baum erstellt wird, erstellt der Browser einen weiteren Baum, den Renderbaum. Diese Baumstruktur enthält visuelle Elemente in der Reihenfolge, in der sie angezeigt werden. Es ist die visuelle Darstellung des Dokuments. Der Zweck dieses Baums besteht darin, die Inhalte in der richtigen Reihenfolge darzustellen.

Firefox nennt die Elemente im Renderbaum „Frames“. WebKit verwendet den Begriff „Renderer“ oder „Renderobjekt“.

Ein Renderer weiß, wie er sich selbst und seine untergeordneten Elemente anordnet und darstellt.

Die RenderObject-Klasse von WebKit, die Basisklasse der Renderer, hat die folgende Definition:

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
}

Jeder Renderer stellt einen rechteckigen Bereich dar, der normalerweise der CSS-Box eines Knotens entspricht, wie in der CSS2-Spezifikation beschrieben. Er enthält geometrische Informationen wie Breite, Höhe und Position.

Der Boxtyp wird durch den Wert „display“ des Stilattributs beeinflusst, das für den Knoten relevant ist (siehe Abschnitt Stilberechnung). Hier ist der WebKit-Code, mit dem anhand des display-Attributs entschieden wird, welcher Typ von Renderer für einen DOM-Knoten erstellt werden soll:

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

Der Elementtyp wird ebenfalls berücksichtigt: Formularsteuerelemente und Tabellen haben beispielsweise spezielle Rahmen.

Wenn ein Element in WebKit einen speziellen Renderer erstellen möchte, überschreibt es die Methode createRenderer(). Die Renderer verweisen auf Stilobjekte, die nicht geometrische Informationen enthalten.

Die Beziehung des Renderbaums zum DOM-Baum

Die Renderer entsprechen DOM-Elementen, aber die Beziehung ist nicht eins zu eins. Nicht visuelle DOM-Elemente werden nicht in die Renderstruktur eingefügt. Ein Beispiel ist das „head“-Element. Auch Elemente, deren Anzeigewert „none“ zugewiesen wurde, werden nicht in der Struktur angezeigt. Elemente mit der Sichtbarkeit „hidden“ werden dagegen in der Struktur angezeigt.

Es gibt DOM-Elemente, die mehreren visuellen Objekten entsprechen. Dies sind in der Regel Elemente mit einer komplexen Struktur, die nicht durch ein einzelnes Rechteck beschrieben werden können. Das Element „select“ hat beispielsweise drei Renderer: einen für den Anzeigebereich, einen für das Drop-down-Listenfeld und einen für die Schaltfläche. Wenn Text in mehrere Zeilen aufgeteilt wird, weil die Breite für eine Zeile nicht ausreicht, werden die neuen Zeilen als zusätzliche Renderer hinzugefügt.

Ein weiteres Beispiel für mehrere Renderer ist fehlerhafter HTML-Code. Gemäß der CSS-Spezifikation darf ein Inline-Element entweder nur Blockelemente oder nur Inline-Elemente enthalten. Bei gemischten Inhalten werden anonyme Block-Renderer erstellt, um die Inline-Elemente zu umschließen.

Einige Renderobjekte entsprechen einem DOM-Knoten, befinden sich aber nicht an derselben Stelle im Baum. Floats und absolut positionierte Elemente befinden sich außerhalb des Flusses, werden in einem anderen Teil des Baums platziert und dem realen Frame zugeordnet. An der Stelle, an der sie sein sollten, ist ein Platzhalter-Frame.

Der Renderbaum und der entsprechende DOM-Baum.
Abbildung 13: Der Renderbaum und der entsprechende DOM-Baum. Der „Darstellungsbereich“ ist der ursprüngliche Containerblock. In WebKit ist es das „RenderView“-Objekt.

Ablauf der Baumkonstruktion

In Firefox wird die Präsentation als Listener für DOM-Updates registriert. Die Präsentation delegiert die Frame-Erstellung an den FrameConstructor und der Konstruktor löst den Stil auf (siehe Stilberechnung) und erstellt einen Frame.

In WebKit wird der Prozess des Auflösens des Stils und des Erstellens eines Renderers als „Anhängen“ bezeichnet. Jeder DOM-Knoten hat eine „attach“-Methode. Das Anhängen erfolgt synchron. Beim Einfügen eines Knotens in den DOM-Baum wird die Methode „attach“ des neuen Knotens aufgerufen.

Durch die Verarbeitung der HTML- und Body-Tags wird die Wurzel des Renderbaums erstellt. Das Stamm-Render-Objekt entspricht dem, was in der CSS-Spezifikation als „enthaltender Block“ bezeichnet wird: der oberste Block, der alle anderen Blöcke enthält. Die Abmessungen entsprechen dem Darstellungsbereich, also den Abmessungen des Anzeigebereichs des Browserfensters. In Firefox heißt es ViewPortFrame und in WebKit RenderView. Dies ist das Renderobjekt, auf das das Dokument verweist. Der Rest des Baums wird als DOM-Knoteneinfügung erstellt.

Weitere Informationen finden Sie in der CSS2-Spezifikation zum Verarbeitungsmodell.

Stilberechnung

Zum Erstellen der Renderstruktur müssen die visuellen Eigenschaften jedes Renderobjekts berechnet werden. Dazu werden die Stileigenschaften der einzelnen Elemente berechnet.

Der Stil umfasst Stylesheets verschiedener Herkunft, Inline-Stilelemente und visuelle Attribute im HTML-Code (z. B. das Attribut „bgcolor“). Letzteres wird in entsprechende CSS-Stilattribute übersetzt.

Die Stylesheets stammen aus den Standard-Stylesheets des Browsers, den vom Seitenautor bereitgestellten Stylesheets und den Nutzer-Stylesheets. Letztere werden vom Browsernutzer bereitgestellt (in Browsern können Sie Ihre bevorzugten Stile definieren. In Firefox wird dazu beispielsweise ein Stylesheet im Ordner „Firefox Profile“ abgelegt.

Die Stilberechnung bringt einige Schwierigkeiten mit sich:

  1. Stildaten sind ein sehr großes Konstrukt, das die zahlreichen Stilattribute enthält. Dies kann zu Speicherproblemen führen.
  2. Das Suchen der übereinstimmenden Regeln für jedes Element kann zu Leistungsproblemen führen, wenn es nicht optimiert ist. Das Durchlaufen der gesamten Regelliste für jedes Element, um Übereinstimmungen zu finden, ist eine aufwendige Aufgabe. Selektoren können eine komplexe Struktur haben, die dazu führen kann, dass der Abgleichprozess auf einem scheinbar vielversprechenden Pfad beginnt, der sich als erfolglos erweist, sodass ein anderer Pfad ausprobiert werden muss.

    Beispiel für einen zusammengesetzten Selektor:

    div div div div{
    ...
    }
    

    Bedeutet, dass die Regeln für ein <div> gelten, das der Nachkomme von 3 divs ist. Angenommen, Sie möchten prüfen, ob die Regel für ein bestimmtes <div>-Element gilt. Sie wählen einen bestimmten Pfad im Baum aus, um ihn zu prüfen. Möglicherweise müssen Sie den Knotenbaum nach oben durchlaufen, nur um festzustellen, dass es nur zwei Divs gibt und die Regel nicht gilt. Sie müssen dann andere Pfade im Baum ausprobieren.

  3. Die Anwendung der Regeln umfasst recht komplexe Kaskadenregeln, die die Hierarchie der Regeln definieren.

Sehen wir uns an, wie die Browser mit diesen Problemen umgehen:

Stildaten teilen

WebKit-Knoten verweisen auf Stilobjekte (RenderStyle). Diese Objekte können unter bestimmten Bedingungen von Knoten gemeinsam genutzt werden. Die Knoten sind Geschwister oder Cousins und:

  1. Die Elemente müssen sich im selben Mausstatus befinden (z. B. kann sich nicht ein Element im Status „:hover“ befinden, während das andere nicht in diesem Status ist).
  2. Keines der Elemente darf eine ID haben.
  3. Die Tag-Namen sollten übereinstimmen
  4. Die Klassenattribute müssen übereinstimmen
  5. Die Gruppe der zugeordneten Attribute muss identisch sein
  6. Die Linkstatus müssen übereinstimmen
  7. Die Fokusstatus müssen übereinstimmen
  8. Keines der Elemente sollte von Attributselektoren betroffen sein. „Betroffen“ bedeutet hier, dass ein Selektor mit einem Attributselektor an einer beliebigen Position innerhalb des Selektors übereinstimmt.
  9. Die Elemente dürfen kein Inline-Stilattribut haben.
  10. Es dürfen keine Geschwisterselektoren verwendet werden. WebCore legt einfach einen globalen Schalter um, wenn ein Geschwisterselektor gefunden wird, und deaktiviert die Stilfreigabe für das gesamte Dokument, wenn solche Selektoren vorhanden sind. Dazu gehören der Selektor „+“ und Selektoren wie „:first-child“ und „:last-child“.

Firefox-Regelbaum

Firefox hat zwei zusätzliche Bäume für eine einfachere Stilberechnung: den Regelbaum und den Stilkontextbaum. WebKit hat auch Stilobjekte, die jedoch nicht in einem Baum wie dem Stilkontextbaum gespeichert werden. Nur der DOM-Knoten verweist auf den relevanten Stil.

Kontextbaum im Firefox-Stil.
Abbildung 14: Firefox-Stilkontextbaum.

Die Stilkontexte enthalten Endwerte. Die Werte werden berechnet, indem alle übereinstimmenden Regeln in der richtigen Reihenfolge angewendet und Manipulationen durchgeführt werden, die sie von logischen in konkrete Werte umwandeln. Wenn der logische Wert beispielsweise ein Prozentsatz des Bildschirms ist, wird er berechnet und in absolute Einheiten umgewandelt. Die Idee mit dem Regelbaum ist wirklich clever. Diese Werte können zwischen Knoten geteilt werden, um eine erneute Berechnung zu vermeiden. Außerdem wird so Platz gespart.

Alle übereinstimmenden Regeln werden in einem Baum gespeichert. Die unteren Knoten in einem Pfad haben eine höhere Priorität. Die Baumstruktur enthält alle Pfade für gefundene Regelübereinstimmungen. Die Regeln werden verzögert gespeichert. Der Baum wird nicht am Anfang für jeden Knoten berechnet, sondern immer dann, wenn ein Knotenstil berechnet werden muss. Die berechneten Pfade werden dann dem Baum hinzugefügt.

Die Idee ist, die Baumpfade als Wörter in einem Lexikon zu betrachten. Angenommen, wir haben diesen Regelbaum bereits berechnet:

Berechneter Regelbaum
Abbildung 15: Berechneter Regelbaum.

Angenommen, wir müssen Regeln für ein anderes Element im Inhaltsbaum abgleichen und stellen fest, dass die übereinstimmenden Regeln (in der richtigen Reihenfolge) B-E-I sind. Dieser Pfad ist bereits im Baum vorhanden, da wir den Pfad A-B-E-I-L bereits berechnet haben. Wir haben jetzt weniger Arbeit.

Sehen wir uns an, wie uns der Baum Arbeit erspart.

Aufteilung in Structs

Die Stilkontexte sind in Strukturen unterteilt. Diese Strukturen enthalten Stilinformationen für eine bestimmte Kategorie, z. B. Rahmen oder Farbe. Alle Attribute in einer Struktur sind entweder übernommen oder nicht übernommen. Übernommene Properties sind Properties, die vom übergeordneten Element übernommen werden, sofern sie nicht vom Element definiert werden. Nicht übernommene Attribute (sogenannte „Reset“-Attribute) verwenden Standardwerte, wenn sie nicht definiert sind.

Der Baum hilft uns, indem er ganze Strukturen (mit den berechneten Endwerten) im Baum zwischenspeichert. Die Idee dahinter ist, dass ein im Cache gespeichertes Struct in einem oberen Knoten verwendet werden kann, wenn der untere Knoten keine Definition für ein Struct bereitgestellt hat.

Berechnen der Stilkontexte mithilfe des Regelbaums

Wenn der Stilkontext für ein bestimmtes Element berechnet wird, wird zuerst ein Pfad im Regelbaum berechnet oder ein vorhandener Pfad verwendet. Anschließend wenden wir die Regeln im Pfad an, um die Strukturen in unserem neuen Stilkontext zu füllen. Wir beginnen am untersten Knoten des Pfads, also dem mit der höchsten Priorität (normalerweise dem spezifischsten Selektor), und gehen den Baum nach oben, bis unsere Struktur voll ist. Wenn es in diesem Regelknoten keine Spezifikation für die Struktur gibt, können wir die Struktur erheblich optimieren. Wir gehen im Baum nach oben, bis wir einen Knoten finden, der sie vollständig spezifiziert, und verweisen darauf. Das ist die beste Optimierung, da die gesamte Struktur gemeinsam genutzt wird. So werden Berechnungen von Endwerten und Speicherplatz gespart.

Wenn wir Teildefinitionen finden, gehen wir im Baum nach oben, bis die Struktur gefüllt ist.

Wenn wir keine Definitionen für unsere Struktur finden, verweisen wir, falls die Struktur ein „geerbter“ Typ ist, auf die Struktur des übergeordneten Elements im Kontextbaum. In diesem Fall ist es uns auch gelungen, Structs zu teilen. Wenn es sich um eine zurückgesetzte Struktur handelt, werden Standardwerte verwendet.

Wenn der spezifischste Knoten Werte hinzufügt, müssen wir zusätzliche Berechnungen durchführen, um ihn in tatsächliche Werte umzuwandeln. Anschließend wird das Ergebnis im Baumknoten im Cache gespeichert, damit es von untergeordneten Knoten verwendet werden kann.

Wenn ein Element ein Geschwisterelement hat, das auf denselben Baumknoten verweist, kann der gesamte Stilkontext zwischen ihnen geteilt werden.

Sehen wir uns ein Beispiel an: Angenommen, wir haben diesen HTML-Code

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

Und die folgenden Regeln:

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

Der Einfachheit halber nehmen wir an, dass wir nur zwei Strukturen ausfüllen müssen: die Farbstruktur und die Randstruktur. Das Struct „color“ enthält nur ein Mitglied: die Farbe. Das Struct „margin“ enthält die vier Seiten.

Der resultierende Regelbaum sieht so aus (die Knoten sind mit dem Knotennamen gekennzeichnet: die Nummer der Regel, auf die sie verweisen):

Regelbaum
Abbildung 16: Der Regelbaum

Der Kontextbaum sieht so aus (Knotenname: Regelknoten, auf den sie verweisen):

Der Kontextbaum.
Abbildung 17: Der Kontextbaum

Angenommen, wir parsen den HTML-Code und gelangen zum zweiten <div>-Tag. Wir müssen einen Stilkontext für diesen Knoten erstellen und seine Stil-Strukturen füllen.

Wir gleichen die Regeln ab und stellen fest, dass die übereinstimmenden Regeln für <div> 1, 2 und 6 sind. Das bedeutet, dass es bereits einen Pfad im Baum gibt, den unser Element verwenden kann. Wir müssen nur einen weiteren Knoten für Regel 6 hinzufügen (Knoten F im Regelbaum).

Wir erstellen einen Stilkontext und fügen ihn in den Kontextbaum ein. Der neue Stilkontext verweist auf Knoten F im Regelbaum.

Jetzt müssen wir die Stil-Strukturen füllen. Zuerst füllen wir die Randstruktur aus. Da der letzte Regelknoten (F) nicht zur Grenzstruktur beiträgt, können wir den Baum nach oben gehen, bis wir eine im Cache gespeicherte Struktur finden, die in einer vorherigen Knoteneinfügung berechnet wurde, und diese verwenden. Wir finden sie auf Knoten B, dem obersten Knoten, für den Randregeln angegeben sind.

Wir haben eine Definition für das Farb-Struct, daher können wir kein im Cache gespeichertes Struct verwenden. Da „Farbe“ nur ein Attribut hat, müssen wir nicht im Baum nach oben gehen, um andere Attribute zu füllen. Wir berechnen den Endwert (String in RGB umwandeln usw.) und speichern die berechnete Struktur auf diesem Knoten im Cache.

Die Arbeit am zweiten <span>-Element ist noch einfacher. Wir gleichen die Regeln ab und kommen zu dem Schluss, dass sie wie der vorherige Span auf Regel G verweist. Da wir Geschwisterelemente haben, die auf denselben Knoten verweisen, können wir den gesamten Stilkontext freigeben und einfach auf den Kontext des vorherigen Span-Elements verweisen.

Bei Strukturen, die Regeln enthalten, die vom übergeordneten Element übernommen werden, erfolgt das Caching im Kontextbaum. Die Farbeigenschaft wird zwar übernommen, aber Firefox behandelt sie als zurückgesetzt und speichert sie im Regelbaum.

Wenn wir beispielsweise Regeln für Schriftarten in einem Absatz hinzufügen:

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

Das Absatz-Element, das im Kontextbaum ein untergeordnetes Element des div-Elements ist, hätte dann dieselbe Schriftartstruktur wie das übergeordnete Element haben können. Das ist der Fall, wenn für den Absatz keine Schriftartregeln festgelegt wurden.

In WebKit, das keinen Regelbaum hat, werden die übereinstimmenden Deklarationen viermal durchlaufen. Zuerst werden nicht wichtige Eigenschaften mit hoher Priorität angewendet (Eigenschaften, die zuerst angewendet werden sollten, weil andere von ihnen abhängen, z. B. „display“), dann wichtige Eigenschaften mit hoher Priorität, dann nicht wichtige Eigenschaften mit normaler Priorität und schließlich wichtige Regeln mit normaler Priorität. Das bedeutet, dass Properties, die mehrmals vorkommen, gemäß der richtigen Kaskadenreihenfolge aufgelöst werden. Der letzte gewinnt.

Zusammenfassend lässt sich sagen, dass die Probleme 1 und 3 durch die gemeinsame Nutzung der Stilobjekte (vollständig oder einige der darin enthaltenen Strukturen) gelöst werden. Der Firefox-Regelbaum hilft auch dabei, die Properties in der richtigen Reihenfolge anzuwenden.

Manipulieren der Regeln für ein einfaches Spiel

Es gibt mehrere Quellen für Stilregeln:

  1. CSS-Regeln, entweder in externen Stylesheets oder in Stilelementen. css p {color: blue}
  2. Inline-Stilattribute wie html <p style="color: blue" />
  3. Visuelle HTML-Attribute (die relevanten Stilregeln zugeordnet sind) html <p bgcolor="blue" /> Die letzten beiden können leicht dem Element zugeordnet werden, da es die Stilattribute besitzt und HTML-Attribute mit dem Element als Schlüssel zugeordnet werden können.

Wie bereits in Problem 2 erwähnt, kann der Abgleich von CSS-Regeln schwieriger sein. Um das Problem zu lösen, werden die Regeln für einen einfacheren Zugriff manipuliert.

Nach dem Parsen des Stylesheets werden die Regeln entsprechend dem Selector einer von mehreren Hash-Maps hinzugefügt. Es gibt Zuordnungen nach ID, nach Klassenname, nach Tag-Name und eine allgemeine Zuordnung für alles, was nicht in diese Kategorien passt. Wenn der Selektor eine ID ist, wird die Regel der ID-Zuordnung hinzugefügt. Wenn es sich um eine Klasse handelt, wird sie der Klassenzuordnung hinzugefügt usw.

Diese Manipulation erleichtert den Abgleich von Regeln erheblich. Es ist nicht erforderlich, jede Deklaration zu prüfen: Wir können die relevanten Regeln für ein Element aus den Zuordnungen extrahieren. Durch diese Optimierung werden mehr als 95 % der Regeln eliminiert, sodass sie beim Abgleich nicht einmal berücksichtigt werden müssen(4.1).

Sehen wir uns zum Beispiel die folgenden Stilregeln an:

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

Die erste Regel wird in die Klassenkarte eingefügt. Die zweite in die ID-Zuordnung und die dritte in die Tag-Zuordnung.

Für das folgende HTML-Fragment:

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

Wir versuchen zuerst, Regeln für das p-Element zu finden. Die Klassenzuordnung enthält einen „error“-Schlüssel, unter dem die Regel für „p.error“ zu finden ist. Das div-Element hat relevante Regeln in der ID-Zuordnung (der Schlüssel ist die ID) und der Tag-Zuordnung. Es muss also nur noch herausgefunden werden, welche der von den Schlüsseln extrahierten Regeln wirklich übereinstimmen.

Wenn die Regel für das div-Element beispielsweise so lautet:

table div {margin: 5px}

Es wird weiterhin aus der Tag-Zuordnung extrahiert, da der Schlüssel der Selektor ganz rechts ist. Es würde jedoch nicht mit unserem div-Element übereinstimmen, das kein übergeordnetes table-Element hat.

Sowohl WebKit als auch Firefox nehmen diese Änderung vor.

Kaskadenreihenfolge von Stylesheets

Das Stilobjekt hat Attribute, die jedem visuellen Attribut entsprechen (alle CSS-Attribute, aber allgemeiner). Wenn die Eigenschaft durch keine der übereinstimmenden Regeln definiert ist, können einige Eigenschaften vom Stilobjekt des übergeordneten Elements übernommen werden. Andere Attribute haben Standardwerte.

Das Problem beginnt, wenn es mehr als eine Definition gibt – hier kommt die Kaskadenreihenfolge ins Spiel, um das Problem zu lösen.

Eine Deklaration für eine Stileigenschaft kann in mehreren Stylesheets und mehrmals innerhalb eines Stylesheets vorkommen. Die Reihenfolge, in der die Regeln angewendet werden, ist also sehr wichtig. Dies wird als „Kaskadenreihenfolge“ bezeichnet. Gemäß der CSS2-Spezifikation ist die Kaskadenreihenfolge (von niedrig nach hoch):

  1. Browserdeklarationen
  2. Normale Deklarationen des Nutzers
  3. Normale Deklarationen erstellen
  4. Wichtige Erklärungen verfassen
  5. Wichtige Erklärungen des Nutzers

Die Browserdeklarationen sind am wenigsten wichtig und der Nutzer überschreibt den Autor nur, wenn die Deklaration als wichtig markiert wurde. Deklarationen mit derselben Reihenfolge werden nach Spezifität und dann nach der Reihenfolge sortiert, in der sie angegeben sind. Die visuellen HTML-Attribute werden in entsprechende CSS-Deklarationen übersetzt . Sie werden als Autorenregeln mit niedriger Priorität behandelt.

Spezifität

Die Selektorspezifität wird in der CSS2-Spezifikation so definiert:

  1. 1, wenn die Deklaration, aus der sie stammt, ein „style“-Attribut ist, und nicht eine Regel mit einem Selektor, andernfalls 0 (= a)
  2. Anzahl der ID-Attribute im Selektor (= b)
  3. Zählen Sie die Anzahl der anderen Attribute und Pseudoklassen im Selektor (= c).
  4. Anzahl der Elementnamen und Pseudoelemente im Selektor (= d)

Die Verkettung der vier Zahlen a-b-c-d (in einem Zahlensystem mit einer großen Basis) ergibt die Spezifität.

Die zu verwendende Zahlenbasis wird durch die höchste Anzahl in einer der Kategorien definiert.

Wenn beispielsweise a=14 ist, können Sie die hexadezimale Basis verwenden. In dem unwahrscheinlichen Fall, dass a=17 ist, benötigen Sie eine 17-stellige Zahlenbasis. Letzteres kann bei einem Selektor wie diesem passieren: html body div div p… (17 Tags in Ihrem Selektor… nicht sehr wahrscheinlich).

Beispiele:

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

Regeln sortieren

Nachdem die Regeln abgeglichen wurden, werden sie gemäß den Kaskadenregeln sortiert. WebKit verwendet Bubble Sort für kleine Listen und Merge Sort für große Listen. WebKit implementiert die Sortierung, indem der Operator > für die Regeln überschrieben wird:

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

Allmählicher Prozess

WebKit verwendet ein Flag, das angibt, ob alle Stylesheets der obersten Ebene (einschließlich @imports) geladen wurden. Wenn der Stil beim Anhängen nicht vollständig geladen ist, werden Platzhalter verwendet und im Dokument markiert. Sie werden neu berechnet, sobald die Stylesheets geladen wurden.

Layout

Wenn der Renderer erstellt und dem Baum hinzugefügt wird, hat er keine Position und Größe. Die Berechnung dieser Werte wird als Layout oder Reflow bezeichnet.

HTML verwendet ein flussbasiertes Layoutmodell. Das bedeutet, dass die Geometrie in den meisten Fällen in einem einzigen Durchgang berechnet werden kann. Elemente, die später im Fluss sind, wirken sich in der Regel nicht auf die Geometrie von Elementen aus, die früher im Fluss sind. Das Layout kann also von links nach rechts und von oben nach unten durch das Dokument verlaufen. Es gibt Ausnahmen: HTML-Tabellen können beispielsweise mehr als einen Durchlauf erfordern.

Das Koordinatensystem ist relativ zum Stammframe. Es werden die Koordinaten für oben und links verwendet.

Das Layout ist ein rekursiver Prozess. Sie beginnt beim Stamm-Renderer, der dem <html>-Element des HTML-Dokuments entspricht. Das Layout wird rekursiv durch einen Teil der oder die gesamte Frame-Hierarchie fortgesetzt, wobei für jeden Renderer, der sie benötigt, geometrische Informationen berechnet werden.

Die Position des Stamm-Renderers ist 0,0 und seine Abmessungen entsprechen dem Darstellungsbereich, also dem sichtbaren Teil des Browserfensters.

Alle Renderer haben eine „Layout“- oder „Reflow“-Methode. Jeder Renderer ruft die Layoutmethode seiner untergeordneten Elemente auf, die ein Layout benötigen.

Modifiziertes Bitsystem

Damit nicht bei jeder kleinen Änderung ein vollständiges Layout erstellt werden muss, verwenden Browser ein „Dirty Bit“-System. Ein Renderer, der geändert oder hinzugefügt wird, markiert sich selbst und seine untergeordneten Elemente als „dirty“, d. h. sie benötigen ein Layout.

Es gibt zwei Flags: „dirty“ und „children are dirty“. Letzteres bedeutet, dass der Renderer selbst zwar in Ordnung ist, aber mindestens ein untergeordnetes Element ein Layout benötigt.

Globales und inkrementelles Layout

Das Layout kann für den gesamten Rendering-Baum ausgelöst werden. Dies ist das „globale“ Layout. Das kann folgende Gründe haben:

  1. Eine globale Stiländerung, die sich auf alle Renderer auswirkt, z. B. eine Änderung der Schriftgröße.
  2. Als Ergebnis der Größenänderung eines Bildschirms

Das Layout kann inkrementell sein, d. h., nur die fehlerhaften Renderer werden angeordnet. Dies kann zu Schäden führen, die zusätzliche Layouts erfordern.

Das inkrementelle Layout wird (asynchron) ausgelöst, wenn Renderer fehlerhaft sind. Beispiel: Wenn neue Renderer an die Renderstruktur angehängt werden, nachdem zusätzliche Inhalte aus dem Netzwerk abgerufen und dem DOM-Baum hinzugefügt wurden.

Inkrementelles Layout.
Abbildung 18: Inkrementelles Layout – nur geänderte Renderer und ihre untergeordneten Elemente werden angeordnet

Asynchrones und synchrones Layout

Das inkrementelle Layout wird asynchron ausgeführt. Firefox reiht „Reflow-Befehle“ für inkrementelle Layouts in eine Warteschlange ein und ein Planer löst die Batchausführung dieser Befehle aus. WebKit hat auch einen Timer, der ein inkrementelles Layout ausführt – die Baumstruktur wird durchlaufen und „dirty“ Renderer werden angeordnet.

Skripts, die Stilinformationen wie „offsetHeight“ anfordern, können ein inkrementelles Layout synchron auslösen.

Das globale Layout wird normalerweise synchron ausgelöst.

Manchmal wird das Layout nach einem ersten Layout als Callback ausgelöst, weil sich einige Attribute wie die Scrollposition geändert haben.

Optimierungen

Wenn ein Layout durch eine Größenänderung oder eine Änderung der Renderer-Position(und nicht der Größe) ausgelöst wird, werden die Rendergrößen aus einem Cache übernommen und nicht neu berechnet…

In einigen Fällen wird nur ein Teilbaum geändert und das Layout beginnt nicht beim Stamm. Das kann passieren, wenn die Änderung lokal ist und sich nicht auf die Umgebung auswirkt, z. B. wenn Text in Textfelder eingefügt wird. Andernfalls würde jeder Tastendruck ein Layout auslösen, das beim Stamm beginnt.

Der Layoutprozess

Das Layout hat normalerweise das folgende Muster:

  1. Der übergeordnete Renderer bestimmt seine eigene Breite.
  2. Der Elternteil geht zu den Kindern und:
    1. Platziert den untergeordneten Renderer (legt seine x- und y-Koordinaten fest).
    2. Ruft bei Bedarf das untergeordnete Layout auf, wenn es fehlerhaft ist, wir uns in einem globalen Layout befinden oder aus einem anderen Grund. Dadurch wird die Höhe des untergeordneten Layouts berechnet.
  3. Das übergeordnete Element verwendet die kumulierten Höhen der untergeordneten Elemente sowie die Höhen von Rändern und Abständen, um seine eigene Höhe festzulegen. Diese wird vom übergeordneten Element des übergeordneten Renderers verwendet.
  4. Setzt das modifizierte Bit auf „false“.

Firefox verwendet ein „state“-Objekt(nsHTMLReflowState) als Parameter für das Layout (als „Reflow“ bezeichnet). Der Status umfasst unter anderem die Breite des übergeordneten Elements.

Die Ausgabe des Firefox-Layouts ist ein „metrics“-Objekt(nsHTMLReflowMetrics). Sie enthält die vom Renderer berechnete Höhe.

Breitenberechnung

Die Breite des Renderers wird anhand der Breite des Containerblocks, der „width“-Stileigenschaft des Renderers, der Ränder und der Rahmen berechnet.

Beispiel: Die Breite des folgenden div-Elements:

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

Würde von WebKit so berechnet werden(Klasse RenderBox, Methode calcWidth):

  • Die Containerbreite ist der Maximalwert aus der verfügbaren Breite des Containers und 0. Die „availableWidth“ ist in diesem Fall die „contentWidth“, die so berechnet wird:
clientWidth() - paddingLeft() - paddingRight()

clientWidth und clientHeight stellen das Innere eines Objekts ohne Rahmen und Scrollleiste dar.

  • Die Breite des Elements ist das Stilattribut „width“. Er wird als absoluter Wert berechnet, indem der Prozentsatz der Containerbreite berechnet wird.

  • Die horizontalen Rahmenlinien und Abstände wurden hinzugefügt.

Bisher haben wir die „bevorzugte Breite“ berechnet. Jetzt werden die minimale und die maximale Breite berechnet.

Wenn die bevorzugte Breite größer als die maximale Breite ist, wird die maximale Breite verwendet. Wenn sie kleiner als die Mindestbreite (die kleinste nicht umbrechbare Einheit) ist, wird die Mindestbreite verwendet.

Die Werte werden im Cache gespeichert, falls ein Layout erforderlich ist, aber die Breite ändert sich nicht.

Zeilenumbruch

Wenn ein Renderer mitten in einem Layout entscheidet, dass er unterbrochen werden muss, stoppt er und teilt dem übergeordneten Element des Layouts mit, dass er unterbrochen werden muss. Das übergeordnete Element erstellt die zusätzlichen Renderer und ruft das Layout für sie auf.

Malerei

In der Malphase wird der Renderbaum durchlaufen und die „paint()“-Methode des Renderers wird aufgerufen, um Inhalte auf dem Bildschirm anzuzeigen. Für das Zeichnen wird die UI-Infrastrukturkomponente verwendet.

Global und inkrementell

Wie das Layout kann auch das Painting global (der gesamte Baum wird gezeichnet) oder inkrementell sein. Beim inkrementellen Zeichnen ändern sich einige der Renderer so, dass nicht die gesamte Struktur betroffen ist. Der geänderte Renderer macht sein Rechteck auf dem Bildschirm ungültig. Dadurch wird der Bereich vom Betriebssystem als „dirty region“ (geänderter Bereich) erkannt und ein „paint“-Ereignis generiert. Das Betriebssystem geht dabei clever vor und fasst mehrere Regionen zu einer zusammen. In Chrome ist das komplizierter, weil sich der Renderer in einem anderen Prozess als der Hauptprozess befindet. Chrome simuliert das Betriebssystemverhalten bis zu einem gewissen Grad. Die Präsentation überwacht diese Ereignisse und leitet die Nachricht an den Render-Root weiter. Der Baum wird durchlaufen, bis der relevante Renderer erreicht ist. Es wird sich selbst (und normalerweise auch seine untergeordneten Elemente) neu zeichnen.

Die Malreihenfolge

CSS2 definiert die Reihenfolge des Zeichenprozesses. Dies ist die Reihenfolge, in der die Elemente in den Stapelkontexten gestapelt werden. Diese Reihenfolge wirkt sich auf das Zeichnen aus, da die Stapel von hinten nach vorne gezeichnet werden. Die Stapelreihenfolge eines Block-Renderers ist:

  1. Hintergrundfarbe
  2. Hintergrundbild
  3. border
  4. Kinder
  5. Outline

Firefox-Anzeigeliste

Firefox geht den Renderbaum durch und erstellt eine Anzeigeliste für das gezeichnete Rechteck. Sie enthält die für das Rechteck relevanten Renderer in der richtigen Zeichenreihenfolge (Hintergründe der Renderer, dann Rahmen usw.).

So muss der Baum für ein Repaint nur einmal durchlaufen werden, anstatt mehrmals – also alle Hintergründe, dann alle Bilder, dann alle Rahmen usw.

Firefox optimiert den Prozess, indem Elemente, die ausgeblendet werden, nicht hinzugefügt werden, z. B. Elemente, die sich vollständig unter anderen undurchsichtigen Elementen befinden.

WebKit-Rechteckspeicher

Vor dem erneuten Zeichnen speichert WebKit das alte Rechteck als Bitmap. Anschließend wird nur der Unterschied zwischen dem neuen und dem alten Rechteck gezeichnet.

Dynamische Änderungen

Die Browser versuchen, als Reaktion auf eine Änderung so wenig wie möglich zu tun. Änderungen an der Farbe eines Elements führen also nur dazu, dass das Element neu gezeichnet wird. Änderungen an der Elementposition führen zu einem Layout und einem Repaint des Elements, seiner untergeordneten Elemente und möglicherweise seiner gleichgeordneten Elemente. Das Hinzufügen eines DOM-Knotens führt zu einem Layout und einem Repaint des Knotens. Größere Änderungen, wie das Erhöhen der Schriftgröße des „html“-Elements, führen dazu, dass Caches ungültig werden und der gesamte Baum neu angeordnet und neu gezeichnet wird.

Die Threads der Rendering-Engine

Die Rendering-Engine ist Single-Threaded. Fast alles, mit Ausnahme von Netzwerkvorgängen, geschieht in einem einzigen Thread. In Firefox und Safari ist dies der Hauptthread des Browsers. In Chrome ist es der Hauptthread des Tab-Prozesses.

Netzwerkvorgänge können von mehreren parallelen Threads ausgeführt werden. Die Anzahl der parallelen Verbindungen ist begrenzt (normalerweise 2–6 Verbindungen).

Ereignisschleife

Der Hauptthread des Browsers ist eine Ereignisschleife. Es ist eine Endlosschleife, die den Prozess am Laufen hält. Er wartet auf Ereignisse (z. B. Layout- und Paint-Ereignisse) und verarbeitet sie. Dies ist der Firefox-Code für die Haupt-Ereignisschleife:

while (!mExiting)
    NS_ProcessNextEvent(thread);

Visuelles Modell von CSS2

Der Canvas

Gemäß der CSS2-Spezifikation beschreibt der Begriff „Canvas“ den Bereich, in dem die Formatierungsstruktur gerendert wird, also den Bereich, in dem der Browser den Inhalt darstellt.

Der Canvas ist für jede Dimension des Raums unendlich, aber Browser wählen eine anfängliche Breite basierend auf den Dimensionen des Darstellungsbereichs aus.

Laut www.w3.org/TR/CSS2/zindex.html ist der Canvas transparent, wenn er in einem anderen Canvas enthalten ist. Andernfalls wird ihm eine vom Browser definierte Farbe zugewiesen.

CSS-Box-Modell

Das CSS-Box-Modell beschreibt die rechteckigen Boxen, die für Elemente im Dokumentbaum generiert und gemäß dem visuellen Formatierungsmodell angeordnet werden.

Jede Box hat einen Inhaltsbereich (z.B. Text, ein Bild usw.) und optionale umgebende Padding-, Border- und Margin-Bereiche.

CSS2-Box-Modell
Abbildung 19: CSS2-Box-Modell

Jeder Knoten generiert 0 bis n solcher Boxen.

Alle Elemente haben ein „display“-Attribut, das den Typ der generierten Box bestimmt.

Beispiele:

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

Der Standardwert ist „inline“, aber im Stylesheet des Browsers können andere Standardwerte festgelegt sein. Beispiel: Die Standardanzeige für das Element „div“ ist „block“.

Ein Beispiel für ein Standard-Stylesheet finden Sie hier: www.w3.org/TR/CSS2/sample.html.

Positionierungsschema

Es gibt drei Schemas:

  1. Normal: Das Objekt wird entsprechend seiner Position im Dokument positioniert. Das bedeutet, dass seine Position im Renderbaum der Position im DOM-Baum entspricht und gemäß dem Box-Typ und den Abmessungen angeordnet wird.
  2. Float: Das Objekt wird zuerst wie im normalen Fluss angeordnet und dann so weit wie möglich nach links oder rechts verschoben.
  3. Absolut: Das Objekt wird an einer anderen Stelle im Renderbaum als im DOM-Baum platziert.

Das Positionierungsschema wird durch die Eigenschaft „position“ und das Attribut „float“ festgelegt.

  • static und relative verursachen einen normalen Fluss
  • „absolute“ und „fixed“ führen zu einer absoluten Positionierung

Bei der statischen Positionierung wird keine Position definiert und die Standardpositionierung verwendet. Bei den anderen Schemas gibt der Autor die Position an: oben, unten, links, rechts.

Das Layout der Box wird durch Folgendes bestimmt:

  • Box-Typ
  • Boxabmessungen
  • Positionierungsschema
  • Externe Informationen wie Bildgröße und Bildschirmgröße

Box-Typen

Blockbox: bildet einen Block – hat ein eigenes Rechteck im Browserfenster.

Block box.
Abbildung 20: Blockbox

Inline-Box: hat keinen eigenen Block, sondern befindet sich in einem Containerblock.

Inline-Boxen.
Abbildung 21: Inline-Boxen

Blöcke werden vertikal nacheinander formatiert. Inlines werden horizontal formatiert.

Block- und Inline-Formatierung.
Abbildung 22: Block- und Inline-Formatierung

Inline-Boxen werden in Zeilen oder „Zeilenboxen“ platziert. Die Linien sind mindestens so hoch wie die höchste Box, können aber auch höher sein, wenn die Boxen „baseline“ ausgerichtet sind. Das bedeutet, dass der untere Teil eines Elements an einem anderen Punkt einer anderen Box als dem unteren ausgerichtet ist. Wenn die Containerbreite nicht ausreicht, werden die Inline-Elemente auf mehrere Zeilen verteilt. Das ist normalerweise das, was in einem Absatz passiert.

Linien.
Abbildung 23: Linien

Positionierung

Relativ

Relative Positionierung: Das Element wird wie gewohnt positioniert und dann um das erforderliche Delta verschoben.

Relative Positionierung.
Abbildung 24: Relative Positionierung

Float

Eine Float-Box wird nach links oder rechts von einer Zeile verschoben. Das Interessante daran ist, dass die anderen Boxen um sie herum fließen. Der HTML-Code:

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

Sieht so aus:

Gleitkommazahl.
Abbildung 25: Float

Absolut und fest

Das Layout wird unabhängig vom normalen Fluss genau definiert. Das Element ist nicht Teil des normalen Ablaufs. Die Abmessungen beziehen sich auf den Container. Bei „fixed“ ist der Container der Darstellungsbereich.

Feste Positionierung.
Abbildung 26: Feste Positionierung

Darstellung in Ebenen

Dies wird durch die CSS-Eigenschaft „z-index“ angegeben. Sie stellt die dritte Dimension der Box dar: ihre Position entlang der „z-Achse“.

Die Boxen sind in Stapel (sogenannte Stapelkontexte) unterteilt. In jedem Stapel werden die hinteren Elemente zuerst und die vorderen Elemente darüber, also näher am Nutzer, dargestellt. Bei einer Überlappung wird das vordere Element das hintere Element verdecken.

Die Stapel werden nach dem Z-Index-Attribut sortiert. Boxen mit der Eigenschaft „z-index“ bilden einen lokalen Stapel. Der Darstellungsbereich enthält den äußeren Stack.

Beispiel:

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

Das Ergebnis sieht so aus:

Feste Positionierung.
Abbildung 27: Feste Positionierung

Obwohl das rote div-Element im Markup vor dem grünen div-Element steht und im regulären Ablauf davor gezeichnet worden wäre, ist die z-index-Eigenschaft höher, sodass es im Stapel der Stammbox weiter vorne liegt.

Ressourcen

  1. Browserarchitektur

    1. Grosskurth, Alan. A Reference Architecture for Web Browsers (PDF)
    2. Gupta, Vineet. How Browsers Work - Part 1 - Architecture
  2. Parsen

    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (auch bekannt als „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, Schnelleres HTML und CSS: Layout-Engine-Interna für Webentwickler.
    2. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Webentwickler (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. W3C-Spezifikationen

    1. HTML 4.01-Spezifikation
    2. W3C-HTML5-Spezifikation
    3. Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification
  6. Build-Anleitung für Browser

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

Übersetzungen

Diese Seite wurde zweimal ins Japanische übersetzt:

Sie können die extern gehosteten Übersetzungen für Koreanisch und Türkisch aufrufen.

Vielen Dank an alle!