Funktionsweise von Browsern: Hinter den Kulissen moderner Web-Browser

HTML5 Rocks

Vorwort

Dieser umfassende Leitfaden zu den internen Abläufen von WebKit und Gecko ist das Ergebnis intensiver Arbeit der israelischen Entwicklerin Tali Garsiel. Sie hat einige Jahre lang alle Veröffentlichungen zu Browser-Interna gesichtet (siehe Ressourcen) und eine Menge Zeit damit verbracht, Quellcodes von Webbrowsern zu lesen. Sie hat dazu Folgendes geschrieben:

In Zeiten, als der Internet Explorer noch eine Dominanz von 90 % hatte, war der Browser so eine Art "Black Box". Aber jetzt, wo mehr als die Hälfte der Nutzer Open Source-Browser verwenden, können wir einen Blick in ihr Innenleben werfen und uns ansehen, woraus ein Webbrowser eigentlich besteht. Nun, im Prinzip besteht er aus Millionen von C++-Zeilen ...
Tali hat ihre Arbeit auf ihrer Website veröffentlicht, aber wir waren der Meinung, dass sie ein größeres Publikum verdient, deshalb haben wir ihren Bericht etwas überarbeitet und hier nochmals veröffentlicht.

Webentwickler können mit dem Wissen über die internen Abläufe von Browsern fundiertere Entscheidungen treffen und die Hintergründe von Best Practices für die Entwicklung kennenlernen. Nehmen Sie sich genug Zeit für dieses ausführliche Dokument. Wir garantieren, es lohnt sich. Paul Irish, Chrome Developer Relations


Einführung

Webbrowser sind wahrscheinlich die am häufigsten verwendete Software. In diesem Leitfaden möchte ich beschreiben, was hinter den Kulissen von Webbrowsern passiert. Wir werden uns ansehen, was eigentlich in der Zeit zwischen der Eingabe von google.com in die Adressleiste und der Darstellung der Google-Seite auf dem Browserbildschirm geschieht.

Inhaltsverzeichnis

  1. Einführung
    1. Die behandelten Browser
    2. Hauptfunktion des Browsers
    3. Die High-Level-Struktur des Browsers
  2. Das Rendering-Modul
    1. Rendering-Module
    2. Der Hauptablauf
    3. Beispiele für den Hauptablauf
  3. Konstruktion der Parsing- und DOM-Baumstruktur
    1. Parsing - allgemein
      1. Grammatik
      2. Parser-Lexer-Kombination
      3. Übersetzung
      4. Parsing-Beispiel
      5. Formale Definitionen für Vokabular und Syntax
      6. Parser-Typen
      7. Automatisches Erstellen von Parsern
    2. HTML-Parser
      1. Definition der HTML-Grammatik
      2. Keine kontextfreie Grammatik
      3. HTML-DTD
      4. DOM
      5. Der Parsing-Algorithmus
      6. Der Tokenisierungs-Algorithmus
      7. Algorithmus zur Konstruktion der Baumstruktur
      8. Aktionen nach dem Parsing
      9. Fehlertoleranz von Browsern
    3. CSS-Parsing
      1. WebKit-CSS-Parser
    4. Die Organisation von Verarbeitungsskripts und Stylesheets
      1. Skripts
      2. Spekulatives Parsing
      3. Stylesheets
  4. Konstruktion der Rendering-Baumstruktur
    1. Die Rendering-Struktur im Verhältnis zur DOM-Struktur
    2. Ablauf der Baumstruktur-Konstruktion
    3. Stilberechnung
      1. Weitergabe von Stildaten
      2. Firefox-Regelbaum
        1. Unterteilung in Strukturen
        2. Berechnen der Stilkontexte mit dem Regelbaum
      3. Bearbeiten der Regeln für eine einfache Zuordnung
      4. Anwendung der Regeln in der richtigen Kaskadenreihenfolge
        1. Kaskadierung von Stylesheets
        2. Spezifität
        3. Sortieren der Regeln
    4. Schrittweiser Prozess
  5. Layout
    1. Dirty Bit-System
    2. Globales und inkrementelles Layout
    3. Asynchrones und synchrones Layout
    4. Optimierungen
    5. Der Layout-Prozess
    6. Breitenberechnung
    7. Zeilenumbruch
  6. Painting
    1. Global und inkrementell
    2. Painting-Reihenfolge
    3. Firefox-Displayliste
    4. WebKit-Rechteckspeicher
  7. Dynamische Änderungen
  8. Rendering-Modul-Threads
    1. Ereignisschleife
  9. Visuelles CSS2-Modell
    1. Canvas
    2. CSS-Boxmodell
    3. Positionierungsschema
    4. Boxtypen
    5. Positionierung
      1. Relativ
      2. Floats
      3. Absolut und fest
    6. Ebenendarstellung
  10. Ressourcen

Die behandelten Browser

Zurzeit werden hauptsächlich fünf Browser verwendet: Internet Explorer, Firefox, Safari, Chrome und Opera. Ich möchte Beispiele von den Open Source-Browsern Firefox, Chrome und Safari (der teilweise Open Source ist) geben. Laut den StatCounter-Browserstatistiken verfügen Firefox, Safari und Chrome derzeit gemeinsam über einen Nutzungsanteil von fast 60 % (Stand August 2011). Open Source-Browser sind heutzutage also ein wesentlicher Teil der Browserwelt.

Hauptfunktion des Browsers

Die Hauptfunktion des Browsers besteht darin, Ihnen Ihre ausgewählte Webressource zu zeigen, indem er sie vom Server abruft und im Browserfenster darstellt. Die Ressource ist normalerweise ein HTML-Dokument, es kann sich jedoch auch um PDF-Dateien, Bilder oder andere Formate handeln. Der Standort der Ressource wird vom Nutzer mithilfe einer URI (Uniform Resource Identifier) angegeben.

Die Art, wie der Browser HTML-Dateien interpretiert und darstellt, ist in den HTML- und CSS-Spezifikationen angegeben. Diese Spezifikationen werden vom World Wide Web Consortium W3C, der Normungsorganisation für das Web, verwaltet.
Jahrelang haben Browser nur einen Teil der Spezifikationen befolgt und ihre eigenen Erweiterungen entwickelt. Dies führte zu ernsthaften Kompatibilitätsproblemen für Webautoren. Heute richten sich die meisten Browser mehr oder weniger nach den Spezifikationen.

Die Benutzeroberflächen der Browser haben viel gemeinsam. Zu den gängigen Benutzeroberflächen-Elementen gehören folgende:

  • Adressleiste zur Eingabe der URI
  • Zurück- und Vorwärts-Schaltflächen
  • Lesezeichenoptionen
  • Schaltflächen zum Aktualisieren und Anhalten, mit denen das Laden der aktuellen Dokumente neu gestartet bzw. beendet werden kann
  • Startseiten-Schaltfläche, über die Sie zu Ihrer Startseite gelangen

Seltsamerweise ist die Benutzeroberfläche von Browsern in keiner formalen Spezifikation definiert, sie hat sich mit den Jahren einfach aus bewährten Vorgehensweisen und gegenseitiger Nachahmung der Browser entwickelt. In der HTML5-Spezifikation werden keine Benutzeroberflächen-Elemente von Browsern definiert, aber einige häufig verwendete Elemente aufgeführt. Zu diesen gehören die Adressleiste, die Statusleiste und die Symbolleiste. Es gibt natürlich auch browserspezifische Funktionen wie etwa den Download-Manager von Firefox.

Die High-Level-Struktur des Browsers

Dies sind die Hauptkomponenten eines Browsers (1.1):

  1. Die Benutzeroberfläche (User Interface, UI) - diese enthält die Adressleiste, Zurück- und Vorwärts-Schaltflächen, ein Lesezeichenmenü usw. Hierzu zählt jeder Teil der Browserdarstellung mit Ausnahme des Hauptfensters, in dem die angeforderte Seite angezeigt wird.
  2. Das Browser-Modul - arrangiert die Aktionen zwischen der Benutzeroberfläche und dem Rendering-Modul.
  3. Das Rendering-Modul- zuständig für die Darstellung des angeforderten Inhalts. Handelt es sich bei dem angeforderten Inhalt beispielsweise um HTML, ist das Modul dafür verantwortlich, den HTML- und CSS-Inhalt zu parsen und den geparsten Inhalt auf dem Bildschirm darzustellen.
  4. Netzwerk - wird für Netzwerk-Aufrufe wie HTTP-Anforderungen verwendet. Es verfügt über eine plattformunabhängige Schnittstelle und zugrunde liegende Implementierungen für jede Plattform.
  5. UI-Backend - wird zur Darstellung grundlegender Widgets wie Kombinationsfelder und Fenster verwendet. Es verfügt über eine generische Schnittstelle, die nicht plattformspezifisch ist. Darunter verwendet es die Benutzeroberflächenmethoden des Betriebssystems.
  6. JavaScript-Interpreter - wird zum Parsen und Ausführen des JavaScript-Codes verwendet.
  7. Datenspeicher - dies ist eine Persistenzebene. Der Browser muss alle möglichen Arten von Daten auf der Festplatte speichern, zum Beispiel Cookies. In der neuen HTML-Spezifikation (HTML5) wird die "Webdatenbank" definiert, bei der es sich um eine vollständige und dennoch schlanke Datenbank im Browser handelt.
Abbildung : Hauptkomponenten eines Browsers

Beachten Sie, dass Chrome im Gegensatz zu den meisten Browsern über mehrere Instanzen des Rendering-Moduls verfügt - eine für jeden Tab. Jeder Tab stellt einen eigenen Prozess dar.

Das Rendering-Modul

Die Aufgabe des Rendering-Moduls ist ... naja, das Rendern, also die angeforderten Inhalte auf dem Browserbildschirm darzustellen.

Das Rendering-Modul kann standardmäßig HTML- und XML-Dokumente sowie Bilder darstellen. Mithilfe eines Plug-ins oder einer Browsererweiterung kann es auch andere Formate darstellen, zum Beispiel PDF-Dokumente über ein PDF Viewer-Plug-in. In diesem Kapitel werden wir uns jedoch auf den Hauptanwendungsfall konzentrieren: die Darstellung von HTML und Bildern, die mit CSS formatiert wurden.

Rendering-Module

Die Browser, mit denen wir uns beschäftigen - Firefox, Chrome und Safari - basieren auf zwei Rendering-Modulen. Firefox verwendet Gecko - ein "hausgemachtes" Rendering-Modul von Mozilla. Safari und Chrome verwenden beide WebKit.

WebKit ist ein Open Source-Rendering-Modul, das zunächst als Modul für die Linux-Plattform entwickelt und von Apple für den Support von Mac und Windows modifiziert wurde. Weitere Einzelheiten finden Sie unter webkit.org.

Der Hauptablauf

Das Rendering-Modul beginnt mit dem Abrufen der Inhalte des angeforderten Dokuments aus der Netzwerkebene. Dies erfolgt üblicherweise in 8 K-Blöcken.

Anschließend sieht der grundlegende Ablauf des Rendering-Moduls wie folgt aus:

Abbildung : Grundlegender Ablauf des Rendering-Moduls

Das Rendering-Modul beginnt mit dem Parsen des HTML-Dokuments und wandelt die Tags in DOM-Knoten in einem sogenannten "Inhaltsbaum" um. Die Stildaten sowohl in externen CSS-Dateien als auch in Stilelementen werden geparst. Die Stilinformationen werden zusammen mit den visuellen Anweisungen im HTML-Code zum Erstellen einer weiteren Baumstruktur verwendet - der Rendering-Struktur.

Die Rendering-Struktur enthält Rechtecke mit visuellen Attributen wie Farbe und Abmessungen. Die Rechtecke befinden sich in der richtigen Reihenfolge für die Darstellung auf dem Bildschirm.

Nach der Konstruktion der Rendering-Struktur folgt der "Layout"-Prozess. Dabei werden jedem Knoten die genauen Koordinaten zugewiesen, an denen er auf dem Bildschirm erscheinen soll. Die nächste Phase ist das Painting. Dabei wird die Rendering-Struktur durchlaufen und jeder Knoten mithilfe der UI-Backend-Ebene dargestellt.

Wichtig ist hierbei, dass es sich um einen schrittweisen Prozess handelt. Für eine bessere Nutzererfahrung versucht das Rendering-Modul, Inhalte so schnell wie möglich auf dem Bildschirm darzustellen. Es wartet nicht, bis das gesamte HTML-Dokument geparst wurde, bevor es mit dem Aufbau und dem Layout der Rendering-Struktur beginnt. Teile des Inhalts werden geparst und dargestellt, während der Prozess mit den übrigen Inhalten, die vom Netzwerk eintreffen, weiterläuft.

Beispiele für den Hauptablauf

Abbildung : Hauptablauf bei WebKit
Abbildung : Hauptablauf bei Mozillas Rendering-Modul Gecko (3.6)

Anhand der Abbildungen 3 und 4 können Sie erkennen, dass WebKit und Gecko zwar eine leicht unterschiedliche Terminologie verwenden, der Ablauf jedoch im Prinzip gleich ist.

Bei Gecko wird die Baumstruktur der visuell formatierten Elemente "Frame Tree" genannt. Jedes Element ist ein Frame. WebKit verwendet den Begriff "Render Tree", welcher aus "Render Objects" besteht. WebKit bezeichnet das Platzieren der Elemente als "Layout", während Gecko diesen Vorgang "Reflow" nennt. "Attachment" heißt bei WebKit die Verbindung von DOM-Knoten und visuellen Informationen zum Erstellen der Rendering-Struktur. Ein kleiner nicht semantischer Unterschied ist, dass Gecko zwischen dem HTML-Code und der DOM-Struktur über eine zusätzliche Ebene verfügt. Diese wird "Content Sink", also in etwa "Inhaltsbecken", genannt und dient der Herstellung von DOM-Elementen. Wir besprechen jede Phase des Ablaufs einzeln:

Parsing - allgemein

Da das Parsing ein sehr wichtiger Prozess innerhalb des Rendering-Moduls ist, werden wir uns etwas genauer damit beschäftigen. Fangen wir mit einer kurzen Einführung zum Parsing an.

Beim Parsen eines Dokuments wird dieses in eine sinnvolle Struktur übersetzt, die der Code verstehen und verwenden kann. Das Ergebnis des Parsings ist normalerweise ein Knotenbaum, der die Struktur des Dokuments wiedergibt. Dieser wird auch Analyse- oder Syntaxbaum genannt.

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

Abbildung : Knotenbaum des mathematischen Ausdrucks

Grammatik

Das Parsing basiert auf den Syntaxregeln, die das Dokument befolgt - die Sprache bzw. das Format, in der/dem es erstellt wurde. Jedes Format, das geparst werden kann, muss über eine deterministische Grammatik aus Vokabular und Syntaxregeln verfügen. Diese wird als kontextfreie Grammatik bezeichnet. Die menschlichen Sprachen erfüllen diese Voraussetzungen nicht und können daher nicht mit konventionellen Parsing-Techniken analysiert werden.

Parser-Lexer-Kombination

Das Parsing kann in zwei Unterprozesse unterteilt werden: die lexikalische Analyse und die Syntaxanalyse.

Bei der lexikalischen Analyse wird die Eingabe in Tokens aufgeschlüsselt. Bei Tokens handelt es sich um das Vokabular der Sprache - die Sammlung gültiger Bausteine. In der menschlichen Sprache besteht dieses aus allen Wörtern, die im Wörterbuch dieser Sprache vorkommen.

Bei der Syntaxanalyse werden die Syntaxregeln der Sprache angewendet.

Parser teilen die Arbeit üblicherweise zwischen zwei Komponenten auf: dem Lexer (gelegentlich auch Tokenizer genannt), der für das Aufschlüsseln in gültige Tokens zuständig ist, und dem Parser, der für die Konstruktion der Parsing-Struktur verantwortlich ist. Dazu analysiert er die Dokumentstruktur gemäß den Syntaxregeln der Sprache. Der Lexer kann irrelevante Zeichen wie Leerzeichen und Zeilenumbrüche isolieren.

Abbildung : Vom Quelldokument zur Parsing-Struktur

Der Parsing-Prozess ist iterativ. Der Parser fordert vom Lexer normalerweise ein neues Token an und versucht, das Token einer Syntaxregel zuzuordnen. Wird eine passende Regel gefunden, wird ein dem Token entsprechender Knoten zur Parsing-Struktur hinzugefügt und der Parser fordert ein weiteres Token an.

Falls keine übereinstimmende Regel gefunden wird, speichert der Parser das Token intern und fordert weiter Tokens an, bis eine passende Regel für alle intern gespeicherten Tokens gefunden wird. Wird keine Regel gefunden, löst der Parser eine Ausnahme aus. Das bedeutet, dass das Dokument ungültig war und Syntaxfehler enthielt.

Übersetzung

Häufig ist die Parsing-Struktur nicht das Endprodukt. Parsing wird oft für die Übersetzung verwendet - die Umwandlung des Eingabedokuments in ein anderes Format. Ein Beispiel hierfür ist die Kompilierung. Der Compiler, der einen Quellcode in Computercode kompiliert, erstellt zunächst eine Parsing-Struktur und übersetzt diese dann in ein Computercode-Dokument.

Abbildung : Ablauf einer Kompilierung

Parsing-Beispiel

In Abbildung 5 haben wir eine Parsing-Struktur auf der Grundlage eines mathematischen Ausdrucks erstellt. Nun versuchen wir, eine einfache mathematische Sprache zu definieren und uns den Parsing-Vorgang anzusehen.

Vokabular: Unsere Sprache kann Ganzzahlen, Pluszeichen und Minuszeichen enthalten.

Syntax:

  1. Die Bausteine der Sprachsyntax sind "expressions" (Ausdrücke), "terms" (Terme) und "operations" (Operationen).
  2. Unsere Sprache kann beliebig viele Ausdrücke enthalten.
  3. Ein Ausdruck wird als Term gefolgt von einer Operation gefolgt von einem weiteren Term definiert.
  4. Eine Operation ist ein Plus-Token oder ein Minus-Token.
  5. Ein Term ist ein Ganzzahl-Token oder ein Ausdruck.

Analysieren wir nun die Eingabe 2 + 3 - 1.
Der erste Unterstring, der einer Regel entspricht, ist 2. Gemäß Regel Nr. 5 ist dies ein Term. Die zweite Übereinstimmung ist 2 + 3. Diese entspricht der dritten Regel: ein Term gefolgt von einer Operation gefolgt von einem weiteren Term. Die nächste Übereinstimmung folgt erst am Ende der Eingabe. 2 + 3 - 1 ist ein Ausdruck, da wir bereits wissen, dass 2+3 ein Term ist. Das heißt, wir haben einen Term gefolgt von einer Operation gefolgt von einem weiteren Term. 2 + + entspricht keiner Regel und ist daher keine gültige Eingabe.

Formale Definitionen für Vokabular und Syntax

Vokabular wird normalerweise durch reguläre Ausdrücke repräsentiert.

Unsere Sprache wird beispielsweise folgendermaßen definiert:

INTEGER :0|[1-9][0-9]*
PLUS : +
MINUS: -
Wie Sie sehen, werden die Ganzzahlen durch einen regulären Ausdruck definiert.

Syntax wird üblicherweise im Format BNF definiert. Die Definition unserer Sprache sieht so aus:

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

Wir haben bereits festgestellt, dass eine Sprache von regulären Parsern analysiert werden kann, wenn sie eine kontextfreie Grammatik besitzt. 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 zur kontextfreien Grammatik.

Parser-Typen

Es gibt zwei grundlegende Typen von Parsern: Top-down-Parser und Bottom-up-Parser. Eine intuitive Erklärung ist, dass Top-down-Parser die High-Level-Struktur der Syntax erkennen und diese zuzuordnen versuchen. Bottom-up-Parser beginnen mit der Eingabe und wandeln diese schrittweise in die Syntaxregeln um. Dabei beginnen sie mit den Regeln auf unterster Ebene und fahren fort, bis die Regeln auf oberster Ebene erfüllt sind.

Sehen wir uns an, wie die beiden Parser in unserem Beispiel arbeiten:

Der Top-down-Parser beginnt mit der Regel auf oberster Ebene: Er identifiziert 2 + 3 als Ausdruck. Anschließend erkennt er 2 + 3 - 1 als Ausdruck. Der Vorgang der Identifizierung des Ausdrucks führt zur Zuordnung der anderen Regeln, Ausgangspunkt ist aber die Regel auf oberster Ebene.

Der Bottom-up-Parser scannt die Eingabe, bis eine passende Regel gefunden wird, und ersetzt dann die übereinstimmende Eingabe durch die Regel. Dieser Vorgang wird bis zum Ende der Eingabe fortgesetzt. Der teilweise zugeordnete Ausdruck wird im Stapel des Parsers abgelegt.

Stapel Eingabe
  2 + 3 - 1
Term + 3 - 1
Term - Operation 3 - 1
Ausdruck - 1
Ausdruck - Operation 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 am Beginn der Eingabe angezeigt wird und dann nach rechts wandert) und schrittweise auf Syntaxregeln reduziert wird.

Automatisches Erstellen von Parsern

Es gibt Tools, die einen Parser für Sie erstellen können. Diese werden Parser-Generatoren genannt. Sie füttern sie mit der Grammatik Ihrer Sprache - dem Vokabular und den Syntaxregeln - und sie erstellen einen funktionierenden Parser. Das Erstellen eines Parsers erfordert tief greifende Kenntnisse des Parsings und es ist nicht einfach, manuell einen optimierten Parser zu erstellen. Parser-Generatoren können daher sehr nützlich sein.

WebKit verwendet zwei bekannte Parser-Generatoren: Flex zum Erstellen eines Lexers und Bison zum Erstellen eines Parsers (sie begegnen Ihnen möglicherweise auch unter den Namen "Lex" und "Yacc"). Bei der Flex-Eingabe handelt es sich um eine Datei, die reguläre Ausdrucksdefinitionen der Tokens enthält. Bei der Bison-Eingabe handelt es sich um die Syntaxregeln der Sprache im BNF-Format.

HTML-Parser

HTML-Parser haben die Aufgabe, die HTML-Auszeichnungssprache zu analysieren und in eine Parsing-Struktur zu bringen.

Definition der HTML-Grammatik

Das Vokabular und die Syntax von HTML sind in Spezifikationen der W3C-Organisation definiert. Die aktuelle Version ist HTML4. HTML5 ist bereits in Arbeit.

Keine kontextfreie Grammatik

Wie in der Einführung zum Parsing bereits erwähnt, kann die Grammatiksyntax formal mithilfe von Formaten wie BNF definiert werden.

Leider sind die konventionellen Parser-Themen nicht auf HTML anwendbar (ich habe diese aber nicht nur aus Spaß erwähnt, sie kommen beim Parsen von CSS und JavaScript zum Einsatz). HTML kann nicht einfach durch eine kontextfreie Grammatik definiert werden, die Parser benötigen.

Es gibt ein formales Format zur Definition von HTML - DTD (Document Type Definition) - dies ist jedoch keine kontextfreie Grammatik.

Dies hört sich zunächst seltsam an. HTML ähnelt doch relativ stark XML. XML-Parser sind viele verfügbar. Es gibt eine XML-Variation von HTML - XHTML - wo also ist der Unterschied?

Der Unterschied ist, dass der HTML-Ansatz "nachsichtiger" ist. Sie können bestimmte Tags weglassen, die implizit hinzugefügt werden, oder manchmal sogar den Beginn oder das Ende von Tags auslassen. Insgesamt ist es eine "weiche" Syntax, im Gegensatz zur steifen und anspruchsvollen XML-Syntax.

Diese zunächst klein erscheinende Abweichung macht augenscheinlich einen Riesenunterschied. Einerseits ist dies der Hauptgrund für die Beliebtheit von HTML: HTML verzeiht Ihnen Ihre Fehler und macht Webautoren das Leben einfacher. Andererseits wird es dadurch schwierig, eine formale Grammatik zu schreiben. Zusammengefasst lässt sich also festhalten, dass HTML nicht einfach so geparst werden kann: nicht von konventionellen Parsern, da seine Grammatik nicht kontextfrei ist, und auch nicht von XML-Parsern.

HTML-DTD

Die HTML-Definition erfolgt in einem DTD-Format. Dieses Format wird für die Definition von Sprachen der SGML-Familie verwendet. Das Format enthält Definitionen für alle zulässigen Elemente, ihre Attribute und die Hierarchie. Wie bereits erwähnt, bildet die HTML-DTD keine kontextfreie Grammatik.

Die DTD verfügt über verschiedene Variationen. Der strenge Modus hält sich ausschließlich an die Spezifikationen, aber andere Modi unterstützen auch Auszeichnungssprachen, die von Browsern in der Vergangenheit verwendet wurden. Sinn und Zweck ist hier die Abwärtskompatibilität mit älteren Inhalten. Die aktuelle strenge DTD finden Sie hier: www.w3.org/TR/html4/strict.dtd.

DOM

Die Ausgabestruktur - der "Parsing-Baum" - ist eine Baumstruktur aus DOM-Element- und Attributknoten. DOM steht für Document Object Model. Dies ist die Objektdarstellung des HTML-Dokuments und die Schnittstelle von HTML-Elementen mit der Außenwelt, zum Beispiel JavaScript.
Stamm des Baums ist das "Document"-Objekt.

Die DOM-Struktur steht nahezu in einem Eins-zu-eins-Verhältnis zur Auszeichnungssprache. Nehmen wir beispielsweise diese Auszeichnungssprache:

<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>
Diese würde in den folgenden DOM-Baum übersetzt:

Abbildung : DOM-Baum der Beispiel-Auszeichnungssprache

Wie HTML wird DOM von der W3C-Organisation definiert. Die Spezifikation finden Sie unter www.w3.org/DOM/DOMTR. Es handelt sich um eine allgemeine Spezifikation zum Bearbeiten von Dokumenten. HTML-spezifische Elemente werden in einem speziellen Modul beschrieben. Die HTML-Definitionen finden Sie hier: www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html.

Mit der Aussage, dass der Baum DOM-Knoten enthält, ist gemeint, dass er aus Elementen besteht, die eine der DOM-Schnittstellen implementieren. Browser verwenden konkrete Implementierungen, die über andere, vom Browser intern verwendete Attribute verfügen.

Der Parsing-Algorithmus

Wie in den vorherigen Abschnitten dargestellt wurde, kann HTML nicht mit den regulären Top-down- oder Bottom-up-Parsern analysiert werden.

Aus folgenden Gründen:

  1. Der nachsichtige Charakter der Sprache
  2. Die Tatsache, dass Browser über eine traditionelle Fehlertoleranz zur Unterstützung bekannter Fälle von ungültiger HTML verfügen
  3. Der Parsing-Prozess ist eintrittsinvariant. Normalerweise ändert sich die Quelle während des Parsings nicht. In HTML können jedoch durch Skript-Tags, die document.write enthalten, zusätzliche Tokens hinzugefügt werden. In diesem Fall wird die Eingabe durch den Parsing-Prozess geändert.

Da sie die regulären Parsing-Techniken nicht verwenden können, erstellen Browser benutzerdefinierte Parser für das HTML-Parsing.

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

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

Der Tokenizer erkennt das Token, gibt es an den Konstruktor weiter, analysiert das nächste Zeichen zur Erkennung des nächsten Tokens usw. - bis zum Ende der Eingabe.

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

Der Tokenisierungs-Algorithmus

Der Algorithmus gibt ein HTML-Token aus. Der Algorithmus wird als Zustandsautomat ausgedrückt. Jeder Zustand liest eines oder mehrere Zeichen des Eingabestreams und aktualisiert den nächsten Zustand entsprechend diesen Zeichen. Die Entscheidung wird vom aktuellen Tokenisierungszustand und vom Zustand der Baumkonstruktion beeinflusst. Das bedeutet, dass dasselbe Zeichen je nach aktuellem Zustand unterschiedliche Ergebnisse für den nächsten richtigen Zustand erzeugen kann. Der Algorithmus ist zu komplex, um ihn vollständig zu beschreiben. Sehen wir uns darum ein einfaches Beispiel an, das das Prinzip des Algorithmus verdeutlicht.

Einfaches Beispiel - Tokenisierung des folgenden HTML-Codes:

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

Der Anfangszustand ist der "Data"-Zustand. Wenn das Zeichen < auftaucht, ändert sich der Zustand in "Tag open". Das Lesen eines Zeichens von a-z führt zur Erstellung eines Start-Tag-Tokens und der Zustand ändert sich in "Tag name". Dieser Zustand bleibt bis zum Auftauchen des >-Zeichens erhalten. Jedes Zeichen wird an den Namen des neuen Tokens angehängt. In unserem Fall ist das erstellte Token ein html-Token.

Sobald das >-Tag erreicht wird, wird das aktuelle Token ausgegeben und der Zustand ändert sich wieder in "Data". Das <body>-Tag wird ebenso gehandhabt. Bisher wurden das html- und das body-Tag ausgegeben. Wir befinden uns jetzt wieder im "Data"-Zustand. Das Lesen des H-Zeichens in Hello world löst die Erstellung und Ausgabe eines Zeichen-Tokens aus. Dies wird solange fortgesetzt, bis < von </body> erreicht wird. Für jedes Zeichen in Hello world wird ein Zeichen-Token ausgegeben.

Wir befinden uns jetzt wieder im Zustand "Tag open". Die Analyse der nächsten Eingabe, /, führt zum Erstellen eines end tag token. Dabei ändert sich der Zustand in "Tag name". Wir bleiben wieder in diesem Zustand, bis wir > erreichen. Dann wird das neue Tag-Token ausgegeben und wir kehren zum "Data"-Zustand zurück. Die </html>-Eingabe wird ebenso gehandhabt.

Abbildung : Tokenisierung der Beispieleingabe

Algorithmus zur Konstruktion der Baumstruktur

Wenn der Parser erstellt wird, wird auch das "Document"-Objekt erstellt. Während der Baumkonstruktion wird der DOM-Baum mit dem "Document"-Objekt im Stamm geändert und es werden Elemente hinzugefügt. Jeder vom Tokenizer ausgegebene Knoten wird vom Baumkonstruktor verarbeitet. Die Spezifikation definiert für jedes Token, welches DOM-Element für dieses Token relevant ist und dafür erstellt wird. Das Element wird nicht nur zum DOM-Baum, sondern auch zu einem Stapel offener Elemente hinzugefügt. Dieser Stapel wird zur Korrektur verschachtelter Abweichungen und nicht geschlossener Tags verwendet. Der Algorithmus wird auch als Zustandsautomat beschrieben. Die Zustände werden Einfügemodi genannt.

Sehen wir uns den Ablauf der Baumkonstruktion für unsere Beispieleingabe an:

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

Die Eingabe in der Phase der Baumkonstruktion ist eine Abfolge von Tokens aus der Tokenisierungsphase. Der erste Zustand ist "initial mode". Der Empfang des "html"-Tokens löst einen Wechsel in den Modus "before html" und eine erneute Verarbeitung des Tokens in diesem Modus aus. Dabei entsteht das "HTMLHtmlElement", das an das "Document"-Objekt im Stamm angehängt wird.

Der Zustand ändert sich in "before head". Das "body"-Token wird empfangen. Obwohl kein "head"-Token verfügbar ist, wird ein "HTMLHeadElement" implizit erstellt und zum Baum hinzugefügt.

Es folgen der "in head"-Modus und anschließend der "after head"-Modus. Das "body"-Token wird erneut verarbeitet, ein "HTMLBodyElement" wird erstellt und eingefügt und der Modus ändert sich in "in body".

Nun werden die Zeichen-Tokens des "Hello world"-Strings empfangen. Das erste löst das Erstellen und Einfügen eines "Text"-Knotens aus. Die anderen Zeichen werden an diesen Knoten angehängt.

Durch den Empfang des "body"-End-Tokens ändert sich der Modus in "after body". Nun wird das "html"-End-Tag empfangen, mit dem sich der Modus in "after after body" ändert. Mit dem Empfang des Dateiende-Tokens wird das Parsing beendet.

Abbildung : Baumkonstruktion des HTML-Beispiels

Aktionen nach dem Parsing

In dieser Phase markiert der Browser das Dokument als interaktiv und beginnt mit dem Parsing von Skripts im "deferred"-Modus. Das sind Skripts, die nach dem Parsen des Dokuments ausgeführt werden sollen. Der Dokumentzustand wird auf "complete" gesetzt und ein "load"-Ereignis wird ausgelöst.

Sie können sich die vollständigen Algorithmen für die Tokenisierung und Baumkonstruktion in der HTML5-Spezifikation ansehen.

Fehlertoleranz von Browsern

Sie erhalten auf einer HTML-Seite nie einen Fehler über eine ungültige Syntax. Die Browser korrigieren ungültigen Inhalt einfach und fahren fort.

HTML-Beispiel:

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

Ich habe hier sicherlich unzählige Regeln verletzt. So ist "mytag" zum Beispiel kein Standard-Tag und die "p"- und "div"-Elemente sind falsch verschachtelt. Der Browser zeigt das Ganze dennoch richtig an, ohne sich zu beschweren. Im Parser-Code wird also ein Großteil der Fehler des HTML-Autors korrigiert.

Die Fehlerbehandlung erfolgt in allen Browsern relativ ähnlich, ist jedoch erstaunlicherweise nicht Teil der aktuellen HTML-Spezifikation. Wie das Setzen von Lesezeichen und die Zurück-/Vorwärts-Schaltflächen ist es eine Eigenschaft, die sich mit der Zeit in Browsern entwickelt hat. Es gibt bekannte ungültige HTML-Konstrukte, die auf vielen Websites vorkommen, und die Browser versuchen diese in Übereinstimmung mit anderen Browsern zu beheben.

In der HTML5-Spezifikation werden allerdings einige dieser Voraussetzungen definiert. In WebKit wird dies im Kommentar zu Beginn der HTML-Parser-Klasse gut zusammengefasst:

Der Parser analysiert die tokenisierte Eingabe im Dokument und erstellt die Dokumentstruktur. Wenn das Dokument wohlgeformt ist, ist das Parsing einfach.

Leider treffen wir auf viele HTML-Dokumente, die nicht wohlgeformt sind, darum muss der Parser über eine gewisse Fehlertoleranz verfügen.

Wir müssen mindestens die folgenden Fehlerzustände berücksichtigen:

  1. Das hinzugefügte Element ist innerhalb eines äußeren Tags ausdrücklich nicht zulässig. In diesem Fall schließen wir alle Tags bis zu dem Tag, in dem das Element nicht zulässig ist, und fügen es danach hinzu.
  2. Das direkte Hinzufügen eines Elements ist nicht zulässig. Es ist möglich, dass der Autor des Dokuments ein Tag dazwischen vergessen hat (oder dass das Tag dazwischen optional ist). Dies könnte bei folgenden Tags der Fall sein: HTML HEAD BODY TBODY TR TD LI (habe ich eines vergessen?).
  3. Wir möchten ein Block-Element innerhalb eines Inline-Elements hinzufügen. Alle Inline-Elemente bis zum nächsthöheren Block-Element werden geschlossen.
  4. Falls dies nicht hilft, schließen wir solange Elemente, bis wir das Element hinzufügen oder das Tag ignorieren können.

Beispiele für die WebKit-Fehlertoleranz:

</br> anstelle von <br>

Manche Websites verwenden </br> anstelle von <br>. Für die Kompatibilität mit IE und Firefox behandelt WebKit diese Elemente wie <br>.
Der Code:

if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
     reportError(MalformedBRError);
     t->beginTag = true;
}
Hinweis: Die Fehlerbehandlung erfolgt intern und wird dem Nutzer nicht angezeigt.

Verirrte Tabelle

Eine verirrte Tabelle ist eine Tabelle innerhalb anderer Tabelleninhalte, aber nicht innerhalb einer Tabellenzelle.
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 Stapel für die aktuellen Elementinhalte und löst die innere Tabelle aus dem Stapel der äußeren Tabelle heraus. Die Tabellen sind jetzt gleichgeordnet.

Verschachtelte Formularelemente

Falls der Nutzer ein Formular innerhalb eines anderen Formulars platziert, wird das zweite Formular ignoriert.
Der Code:

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

Zu tiefe Tag-Hierarchie

Der Kommentar sagt alles:

www.liceo.edu.mx ist ein Beispiel für eine Website, auf der bis zu 1500 Tags verschachtelt sind, alle über ein Bündel aus <b>s. Wir erlauben nur maximal zwanzig verschachtelte Tags desselben Typs, bevor wir sie einfach alle ignorieren.
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 sagt der Kommentar alles:

Unterstützung wirklich fehlerhafter "html"-Tags. Wir schließen das "body"-Tag nie, da einige begriffsstutzige Webseiten es vor dem eigentlichen Ende des Dokuments schließen. Verlassen wir uns doch auf den "end()"-Aufruf zum endgültigen Schließen.
if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;

Webautoren also aufgepasst: Solange Sie nicht als Beispiel in einem Code-Snippet zur Fehlertoleranz von WebKit auftauchen möchten, empfiehlt es sich, wohlgeformten HTML-Code zu schreiben.

CSS-Parsing

Erinnern Sie sich noch an die Parsing-Konzepte aus der Einführung? Im Gegensatz zu HTML handelt es sich bei CSS um eine kontextfreie Grammatik, die mit den in der Einführung beschriebenen Parsertypen analysiert werden kann. Tatsächlich werden in der CSS-Spezifikation die lexikalische Grammatik und die Syntax-Grammatik von CSS definiert.

Sehen wir uns einige Beispiele an:
Die lexikalische Grammatik (das 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" steht für Identifier, etwa einen Klassennamen. "name" ist eine Element-ID (auf die mit "#" verwiesen wird).

Die Syntax-Grammatik ist 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: Diese Struktur ist ein Regelsatz (ruleset):
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 heißt, ein Regelsatz besteht aus einem Selektor oder optional mehreren Selektoren, getrennt durch Komma und Leerzeichen. "S" steht hierbei für Leerzeichen (White Space). Ein Regelsatz enthält geschweifte Klammern, in denen sich eine Deklaration oder optional mehrere Deklarationen getrennt durch ein Semikolon befinden. "declaration" und "selector" werden in den nachfolgenden BNF-Definitionen definiert.

WebKit-CSS-Parser

WebKit verwendet die Parser-Generatoren Flex und Bison, um aus den CSS-Grammatikdateien automatisch Parser zu erstellen. Wie in der Einführung zu Parsern bereits erläutert, erstellt Bison einen Bottom-up-Shift-Reduce-Parser. Firefox verwendet einen manuell erstellten Top-down-Parser. In beiden Fällen wird jede CSS-Datei in ein Stylesheet-Objekt geparst und jedes Objekt enthält CSS-Regeln. Die CSS-Regelobjekte enthalten Selektor- und Deklarationsobjekte sowie weitere Objekte, die der CSS-Grammatik entsprechen.

Abbildung : CSS-Parsing

Die Organisation von Verarbeitungsskripts und Stylesheets

Skripts

Das Modell des Web ist synchron. Autoren erwarten, dass Skripts sofort geparst und ausgeführt werden, wenn der Parser ein <script>-Tag erreicht. Das Parsing des Dokuments wird angehalten, bis das Skript ausgeführt wurde. Bei einem externen Skript muss zunächst die Ressource aus dem Netzwerk abgerufen werden. Dies erfolgt ebenfalls synchron und das Parsing wird pausiert, bis die Ressource abgerufen wurde. Dies war viele Jahre lang das Modell, das auch in den HTML4- und HTML5-Spezifikationen definiert ist. Autoren konnten ein Skript als "defer" markieren, damit das Dokument-Parsing nicht angehalten und das Skript erst nach dem Parsen ausgeführt wird. HTML5 bietet nun auch die Option, ein Skript als asynchron zu markieren, sodass es von einem anderen Thread geparst und ausgeführt wird.

Spekulatives Parsing

Sowohl WebKit als auch Firefox verfügen über diese Optimierung. Beim Ausführen von Skripts wird der Rest des Dokuments von einem anderen Thread geparst, der ermittelt, welche anderen Ressourcen aus dem Netzwerk geladen werden müssen, und diese lädt. Auf diese Weise können Ressourcen auf parallelen Verbindungen geladen werden, wodurch sich die Gesamtgeschwindigkeit verbessert. Hinweis: Der spekulative Parser ändert den DOM-Baum nicht - dies überlässt er dem Hauptparser - er analysiert lediglich Verweise auf externe Ressourcen wie externe Skripts, Stylesheets und Bilder.

Stylesheets

Stylesheets weisen dagegen ein anderes Modell auf. Da Stylesheets die DOM-Struktur nicht verändern, scheint es zunächst keinen Grund zu geben, auf sie zu warten und das Parsen des Dokuments anzuhalten. Es gibt jedoch das Problem, dass Skripts während des Dokument-Parsings Stilinformationen anfordern. Wurde der Stil noch nicht geladen und geparst, erhält das Skript falsche Antworten und dies führt offensichtlich zu einer Reihe von Problemen. Dies hört sich nach einer Randerscheinung an, tritt aber tatsächlich ziemlich häufig auf. Firefox blockiert alle Skripts, wenn ein Stylesheet noch geladen und geparst wird. WebKit blockiert Skripts nur, wenn sie auf bestimmte Stileigenschaften zugreifen möchten, die von nicht geladenen Stylesheets ausgeführt werden könnten.

Konstruktion der Rendering-Baumstruktur

Während der Konstruktion des DOM-Baums konstruiert der Browser eine weitere Baumstruktur: die Rendering-Struktur. Diese Baumstruktur enthält visuelle Elemente in der Reihenfolge, in der sie später angezeigt werden. Dies ist die visuelle Darstellung des Dokuments. Zweck dieser Struktur ist es, das Darstellen der Inhalte in der richtigen Reihenfolge zu ermöglichen.

Bei Firefox werden die Elemente in der Rendering-Struktur als "Frames" bezeichnet. WebKit verwendet den Ausdruck "Renderer" oder "Render Object".
Ein Renderer weiß, wie er sich und seine untergeordneten Elemente anordnet und darstellt.
Die "RenderObject"-Klasse von WebKit, die Renderer-Basisklasse, wird folgendermaßen definiert:

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 steht für einen rechteckigen Bereich, der normalerweise der CSS-Box des Knotens entspricht, wie in der CSS2-Spezifikation beschrieben. Er enthält geometrische Informationen wie Breite, Höhe und Position.
Der Boxtyp richtet sich nach dem "display"-Stilattribut, das für den Knoten relevant ist (siehe Abschnitt Stilberechnung). Hier ist der WebKit-Code, mit dem entschieden wird, welche Art von Renderer gemäß dem "display"-Attribut 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. So verfügen beispielsweise Formularsteuerelemente und Tabellen über spezielle Frames.
Falls in WebKit ein Element einen speziellen Renderer erstellten möchte, wird die createRenderer-Methode überschrieben. Die Renderer verweisen auf Stilobjekte, die die nicht geometrischen Informationen enthalten.

Die Rendering-Struktur im Verhältnis zur DOM-Struktur
Die Renderer entsprechen den DOM-Elementen, es handelt es sich jedoch nicht um ein Eins-zu-eins-Verhältnis. Nicht visuelle DOM-Elemente werden nicht in die Rendering-Struktur aufgenommen. Ein Beispiel hierfür ist das "head"-Element. Elemente, deren "display"-Attribut auf "none" gesetzt wurde, erscheinen ebenfalls nicht in der Struktur. Elemente mit dem Sichtbarkeitsattribut "hidden" werden dagegen aufgenommen.

Es gibt DOM-Elemente, die verschiedenen visuellen Objekten entsprechen. Dies sind üblicherweise Elemente mit einer komplexen Struktur, die nicht durch ein einzelnes Rechteck beschrieben werden können. Das Auswahlelement "select" verfügt beispielsweise über drei Renderer: einen für den Darstellungsbereich, einen für das Dropdown-Listenfeld und einen für die Schaltfläche. Auch 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 fehlerhafte HTML. Laut der CSS-Spezifikation darf ein Inline-Element entweder nur Block-Elemente oder nur Inline-Elemente enthalten. Werden diese Elemente gemischt, werden anonyme Block-Renderer erstellt, um die Inline-Elemente zu umschließen.

Einige Rendering-Objekte entsprechen einem DOM-Knoten, jedoch nicht an derselben Position in der Baumstruktur. Float-Elemente und absolut positionierte Elemente stehen außerhalb des Ablaufs. Sie werden an einer anderen Stelle der Struktur platziert und dem echten Frame zugeordnet. An ihrer Stelle wird ein Platzhalter-Frame angezeigt.

Abbildung : Die Rendering-Struktur und die entsprechende DOM-Struktur (3.1). "Viewport" ist der ursprüngliche übergeordnete Block. In WebKit ist dies das "RenderView"-Objekt.
Ablauf der Baumstruktur-Konstruktion

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

In WebKit werden das Auflösen des Stils und das Erstellen eines Renderers als "Attachment" (Anhängen) bezeichnet. Jeder DOM-Knoten verfügt über eine "attach"-Methode. Das Anhängen erfolgt synchron, durch das Einfügen des Knotens in die DOM-Struktur wird die "attach"-Methode des neuen Knotens aufgerufen.

Mit der Verarbeitung der "html"- und "body"-Tags wird der Stamm der Rendering-Struktur konstruiert. Das Stamm-Rendering-Objekt entspricht dem übergeordneten Block in der CSS-Spezifikation - dem obersten Block, der alle anderen Blöcke enthält. Seine Abmessungen stellen den Viewport dar: die Abmessungen des Darstellungsbereichs des Browserfensters. In Firefox wird dies als ViewPortFrame und in WebKit als RenderView bezeichnet. Dies ist das Rendering-Objekt, auf das das Dokument verweist. Der Rest der Baumstruktur wird durch das Einfügen von DOM-Knoten erstellt.

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

Stilberechnung

Zum Erstellen der Rendering-Struktur müssen die visuellen Eigenschaften jedes Rendering-Objekts berechnet werden. Dies erfolgt durch die Berechnung der Stileigenschaften jedes Elements.

Der Stil umfasst Stylesheets verschiedener Ursprünge, Inline-Stilelemente und visuelle Eigenschaften in der HTML, etwa die "bgcolor"-Eigenschaft. Letztere wird so übersetzt, dass sie den CSS-Stileigenschaften entspricht.

Die Stylesheets stammen aus den Standard-Stylesheets des Browsers, den vom Seitenautor bereitgestellten Stylesheets und Nutzer-Stylesheets - diese werden vom Nutzer des Browsers bereitgestellt (Sie können in Browsern Ihren bevorzugten Stil festlegen. In Firefox platzieren Sie dazu beispielsweise ein Stylesheet im Firefox-Profilordner ).

Die Stilberechnung birgt einige Schwierigkeiten:

  1. Stildaten sind ein sehr umfangreiches Konstrukt, da sie die zahlreichen Stileigenschaften umfassen, und können daher zu Speicherproblemen führen.
  2. Das Finden der entsprechenden Regeln für jedes Element kann zu Leistungsproblemen führen, wenn dieser Vorgang nicht optimiert ist. Es erfordert einen großen Aufwand, die gesamte Regelliste für jedes Element zu durchlaufen, um Übereinstimmungen zu finden. Selektoren können über eine komplexe Struktur verfügen, aufgrund derer der Zuordnungsprozess auf einem vielversprechenden Pfad beginnt, der sich dann jedoch als nutzlos erweist, sodass ein neuer Pfad getestet werden muss.

    Zum Beispiel dieser Verbundselektor:

    div div div div{
      ...
    }
    
    Dies bedeutet, dass die Regel für ein <div>-Element gilt, das auf drei andere "div"-Elemente folgt. Angenommen, Sie möchten prüfen, ob die Regel auf ein bestimmtes <div>-Element zutrifft. Sie wählen zur Überprüfung einen Pfad aus, der die Baumstruktur hinaufführt. Sie müssen möglicherweise den Knotenbaum nach oben durchlaufen, nur um festzustellen, dass nur zwei "div"-Elemente vorhanden sind und die Regel nicht zutrifft. Sie müssten dann andere Pfade in der Baumstruktur überprüfen.
  3. Das Anwenden der Regeln beinhaltet ziemlich komplexe Kaskadenregeln, die die Hierarchie der Regeln definieren.
So gehen die Browser mit diesen Problemen um:
Weitergabe von Stildaten

WebKit-Knoten verweisen auf Stilobjekte (RenderStyle). Diese Objekte können unter bestimmten Umständen von Knoten geteilt werden. Die Knoten sind gleichgeordnet oder verwandt und Folgendes trifft zu:

  1. Die Elemente weisen denselben Mauszustand auf. (Das heißt beispielsweise, dass nicht ein Element sich im Zustand ":hover" befindet und das andere nicht.)
  2. Keines der Elemente darf über eine ID verfügen.
  3. Die Tag-Namen stimmen überein.
  4. Die Klassen-Attribute stimmen überein.
  5. Der Satz zugeordneter Attribute ist identisch.
  6. Die Linkzustände stimmen überein.
  7. Die Fokuszustände stimmen überein.
  8. Kein Element darf von Attributselektoren betroffen sein. Dabei bedeutet "betroffen", dass eine beliebige Selektorübereinstimmung vorliegt, bei der ein Attributselektor an einer beliebigen Position innerhalb des Selektors verwendet wird.
  9. Es dürfen keine Inline-Stilattribute bei den Elementen vorhanden sein.
  10. Es dürfen keine gleichgeordneten Selektoren in Verwendung sein. WebCore löst einfach einen globalen Wechsel aus, wenn ein gleichgeordneter Selektor gefunden wird, und deaktiviert bei deren Vorkommen die Stilweitergabe für das gesamte Dokument. Dazu gehören auch der +-Selektor und Selektoren wie ":first-child" und ":last-child".
Firefox-Regelbaum

Firefox verfügt über zwei zusätzliche Baumstrukturen für eine einfachere Stilberechnung: den Regelbaum und den Stilkontextbaum. WebKit verfügt ebenfalls über Stilobjekte, diese sind jedoch nicht in einer Baumstruktur wie dem Stilkontextbaum gespeichert. Der DOM-Knoten verweist nur auf seinen relevanten Stil.

Abbildung : Stilkontextbaum bei Firefox (2.2)

Die Stilkontexte enthalten Endwerte. Die Werte werden berechnet, indem alle zutreffenden Regeln in der richtigen Reihenfolge angewendet und Änderungen vorgenommen werden, die sie von logischen in konkrete Werte umwandeln. Ist der logische Wert beispielsweise ein Prozentsatz des Bildschirms, wird er berechnet und in absolute Einheiten umgewandelt. Die Idee eines Regelbaums ist wirklich clever. Sie ermöglicht die Weitergabe dieser Werte zwischen Knoten, um eine erneute Berechnung zu vermeiden. Dies spart auch Speicherplatz.

Alle zutreffenden Regeln werden in einer Baumstruktur gespeichert. Die unteren Knoten in einem Pfad besitzen eine höhere Priorität. Der Baum enthält alle Pfade für gefundene Regelübereinstimmungen. Das Speichern der Regeln erfolgt verzögert. Die Baumstruktur wird nicht zu Beginn für jeden Knoten berechnet, aber immer, wenn ein Knotenstil berechnet werden muss, werden die berechneten Pfade zu dem Baum hinzugefügt.

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

Nehmen wir an, wir müssen Regeln für ein anderes Element im Inhaltsbaum zuordnen und finden heraus, dass die zutreffenden Regeln (in der richtigen Reihenfolge) B - E - I sind. Dieser Pfad ist in unserem Baum bereits vorhanden, da wir schon den Pfad A - B - E - I - L berechnet haben. Nun haben wir weniger Arbeit.

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

Unterteilung in Strukturen

Die Stilkontexte werden in Strukturen unterteilt. Diese Strukturen enthalten Stilinformationen für eine bestimmte Kategorie wie Rahmen oder Farbe. Alle Eigenschaften in einer Struktur sind entweder geerbt oder nicht geerbt. Geerbte Eigenschaften sind Eigenschaften, die vom übergeordneten Element übernommen werden, sofern sie nicht vom Element selbst definiert werden. Nicht geerbte Eigenschaften, auch "reset"-Eigenschaften genannt, verwenden Standardwerte, wenn sie nicht definiert werden.

Der Baum hilft uns, indem ganze Strukturen (mit den berechneten Endwerten) darin zwischengespeichert werden. Die Idee dahinter: Falls der untere Knoten keine Definition für eine Struktur bereitgestellt hat, kann eine zwischengespeicherte Struktur in einem oberen Knoten verwendet werden.

Berechnen der Stilkontexte mit dem Regelbaum

Beim Berechnen des Stilkontexts für ein bestimmtes Element berechnen wir zunächst einen Pfad im Regelbaum oder verwenden einen vorhandenen Pfad. Anschließend beginnen wir damit, die Regeln in dem Pfad anzuwenden, um die Strukturen in unserem neuen Stilkontext zu füllen. Wir beginnen mit dem untersten Knoten im Pfad - dem mit der höchsten Priorität (normalerweise der spezifischste Selektor) - und durchlaufen den Baum nach oben, bis unsere Struktur vollständig ist. Falls für die Struktur in diesem Regelknoten keine Spezifikation vorhanden ist, können wir optimieren: Wir wandern den Baum nach oben, bis wir einen Knoten mit einer vollständigen Spezifikation finden, und verweisen dann einfach darauf. Dies ist die bestmögliche Optimierung - die gesamte Struktur wird geteilt. So müssen wir weniger Endwerte berechnen und sparen Speicherplatz.
Falls wir Teildefinitionen finden, klettern wir den Baum nach oben, bis die Struktur vollständig ausgefüllt ist.

Falls wir gar keine Definitionen für unsere Struktur finden, verweisen wir auf die Struktur des übergeordneten Elements im Kontextbaum, falls es sich bei unserer Struktur um eine geerbte Struktur handelt. So können wir ebenfalls Strukturen weitergeben. Im Fall einer "reset"-Struktur werden Standardwerte verwendet.

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

Falls ein Element über ein gleichgeordnetes oder verwandtes Element verfügt, das auf denselben Baumknoten verweist, kann der gesamte Stilkontext zwischen ihnen weitergegeben werden.

Beispiel: Angenommen, uns liegt folgender HTML-Code vor:

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

Gehen wir der Einfachheit halber davon aus, dass wir nur zwei Strukturen vervollständigen müssen: die Farbstruktur und die Randstruktur. Die Farbstruktur enthält nur ein Mitglied - die Farbe. Die Randstruktur umfasst die vier Seiten.
Der entstehende Regelbaum sieht folgendermaßen aus (die Knoten sind mit den Knotennamen gekennzeichnet : die Nummer der Regel, auf die sie verweisen):

Abbildung : Regelbaum

Der Kontextbaum sieht folgendermaßen aus (Knotenname : Regelknoten, auf den verwiesen wird):
Abbildung : Kontextbaum

Angenommen, wir parsen den HTML-Code und gelangen zum zweiten <div>-Tag. Wir müssen einen Stilkontext für diesen Knoten erstellen und dessen Stilstrukturen ausfüllen.
Wir ordnen die Regeln zu und ermitteln, dass auf das <div>-Element die Regeln 1, 2 und 6 zutreffen. Das bedeutet, dass in dem Baum bereits ein Pfad vorhanden ist, den unser Element nutzen kann, und wir nur einen weiteren Knoten für Regel 6 hinzufügen müssen (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.

Nun müssen wir die Stilstrukturen füllen. Wir beginnen mit der Randstruktur. Da der letzte Regelknoten (F) nicht zur Randstruktur gehört, können wir den Baum hinaufwandern, bis wir eine zwischengespeicherte Struktur in einer vorherigen Knoteneinfügung finden, und diese verwenden. Wir finden diese auf Knoten B, dem obersten Knoten, der Randregeln angibt.

Wir verfügen über eine Definition für die Farbstruktur und können deshalb keine zwischengespeicherte Struktur verwenden. Da die Farbe nur ein einziges Attribut besitzt, müssen wir den Baum nicht durchlaufen, um weitere Attribute zu füllen. Wir berechnen den Endwert (String in RGB umwandeln usw.) und speichern die berechnete Struktur auf diesem Knoten zwischen.

Die Arbeit am zweiten <span>-Element ist sogar noch einfacher. Wir ordnen die Regeln zu und kommen zu dem Schluss, dass es wie das vorherige "span"-Element auf Regel G verweist. Da wir hier über gleichgeordnete Elemente verfügen, die auf denselben Knoten verweisen, können wir den gesamten Stilkontext teilen und einfach auf den Kontext des vorherigen "span"-Elements verweisen.

Für Strukturen, die von dem übergeordneten Element geerbte Regeln enthalten, erfolgt das Zwischenspeichern auf dem Kontextbaum. Die Farbeigenschaft wird eigentlich vererbt, aber Firefox behandelt sie als "reset"-Eigenschaft und speichert sie auf dem Regelbaum zwischen.
Angenommen, wir würden Regeln für Schriftarten in einem Absatz hinzufügen:

p {font-family:Verdana;font size:10px;font-weight:bold}
Dann könnte das Absatzelement, das ein untergeordnetes Element des "div"-Elements im Kontextbaum ist, dieselbe Schriftartstruktur wie sein übergeordnetes Element verwendet haben. Dies ist der Fall, wenn keine Schriftartregeln für den Absatz angegeben wurden.

In WebKit, das nicht über einen Regelbaum verfügt, werden die zugeordneten Deklarationen viermal durchlaufen. Zunächst werden nicht wichtige Eigenschaften mit hoher Priorität angewendet. Dies sind Eigenschaften, die zuerst angewendet werden sollten, weil andere von ihnen abhängen, zum Beispiel die "display"-Eigenschaft. Danach folgen wichtige Regeln mit hoher Priorität, nicht wichtige Regeln mit normaler Priorität und schließlich wichtige Regeln mit normaler Priorität. Eigenschaften, die mehrfach auftreten, werden somit gemäß der korrekten Kaskadenreihenfolge aufgelöst. Der Letzte gewinnt.

Zusammengefasst löst also das Weitergeben der Stilobjekte (aller oder einiger darin vorhandener Strukturen) die Probleme 1 und 3. Der Firefox-Regelbaum hilft außerdem dabei, die Eigenschaften in der richtigen Reihenfolge anzuwenden.

Bearbeiten der Regeln für eine einfache Zuordnung

Es gibt verschiedene Quellen für Stilregeln:

  • CSS-Regeln, entweder in externen Stylesheets oder in Stilelementen
    p {color:blue}
    
  • Inline-Stilattribute wie
    <p style="color:blue" />
    
  • Visuelle HTML-Attribute (die relevanten Stilregeln zugeordnet werden)
    <p bgcolor="blue" />
    

Die beiden letzten können dem Element leicht zugeordnet werden, da es die Stilattribute enthält und HTML-Attribute mithilfe des Elements als Schlüssel zugeordnet werden können.

Wie in Problem Nr. 2 bereits erwähnt, kann sich die Zuordnung der CSS-Regeln etwas schwieriger gestalten. Um diese Schwierigkeit zu beheben, werden die Regeln für einen einfacheren Zugriff bearbeitet.

Nach dem Parsen des Stylesheets werden die Regeln dem Selektor entsprechend zu einer von mehreren Hashmaps hinzugefügt. Es gibt Hashmaps für ID, Klassenname und Tag-Name sowie eine allgemeine Hashmap für alles, das nicht in diese Kategorien passt. Handelt es sich bei dem Selektor um eine ID, wird die Regel zur ID-Map hinzugefügt, bei einer Klasse wird sie zur Klassen-Map hinzugefügt usw.
Diese Bearbeitung erleichtert die Zuordnung von Regeln enorm. Es muss nicht jede Deklaration überprüft werden. Die relevanten Regeln für ein Element können aus den Hashmaps extrahiert werden. Mit dieser Optimierung können mehr als 95 % der Regeln aussortiert werden, sodass sie während des Zuordnungsvorgangs nicht einmal berücksichtigt werden müssen (4.1).

Sehen wir uns beispielsweise folgende Stilregeln an:

p.error {color:red}
#messageDiv {height:50px}
div {margin:5px}
Die erste Regel wird in die Klassen-Hashmap eingefügt. Die zweite Regel wird in die ID-Map und die dritte in die Tag-Map eingefügt.
Bei dem folgenden HTML-Fragment
<p class="error">an error occurred </p>
<div id=" messageDiv">this is a message</div>

suchen wir zunächst nach Regeln für das "p"-Element. Die Klassen-Hashmap enthält einen Fehlerschlüssel, unter dem die Regel für "p.error" zu finden ist. Dem "div"-Element entsprechen relevante Regeln in der ID-Map (Schlüssel ist die ID) und der Tag-Map. Wir müssen nun nur noch herausfinden, welche der durch die Schlüssel extrahierten Regeln tatsächlich zutreffen.
Lautete die Regel für das "div"-Element zum Beispiel

table div {margin:5px}
würde sie dennoch aus der Tag-Map extrahiert, da der Schlüssel der Selektor ganz rechts ist. Sie würde aber nicht mit unserem "div"-Element übereinstimmen, da dieses nicht über einen Tabellenvorgänger verfügt.

Sowohl WebKit als auch Firefox nehmen diese Bearbeitung vor.

Anwendung der Regeln in der richtigen Kaskadenreihenfolge

Das Stilobjekt verfügt über Eigenschaften, die jedem visuellen Attribut (allen CSS-Attributen, aber allgemeiner) entsprechen. Wird die Eigenschaft durch keine der zugeordneten Regeln definiert, können einige Eigenschaften vom Stilobjekt des übergeordneten Elements geerbt werden. Andere Eigenschaften verfügen über Standardwerte.

Das Problem entsteht, wenn es mehr als eine Definition gibt. Dieses Problem kann mithilfe der Kaskadenreihenfolge gelöst werden.

Kaskadierung von Stylesheets
Eine Deklaration für eine Stileigenschaft kann in verschiedenen Stylesheets vorkommen sowie mehrere Male innerhalb eines Stylesheets. Die Reihenfolge, in der die Regeln angewendet werden, spielt demnach eine wichtige Rolle. Diese wird Kaskadenreihenfolge genannt. Gemäß der CSS2-Spezifikation sieht die Kaskadenreihenfolge folgendermaßen aus (von unten nach oben):
  1. Browserdeklarationen
  2. Normale Nutzerdeklarationen
  3. Normale Autorendeklarationen
  4. Wichtige Autorendeklarationen
  5. Wichtige Nutzerdeklarationen

Die Browserdeklarationen sind am wenigsten wichtig und Nutzer haben nur Vorrang vor Autoren, wenn die Deklaration als wichtig gekennzeichnet wurde. Deklarationen mit derselben Reihenfolge werden anhand der Spezifität und dann anhand ihrer angegebenen Reihenfolge sortiert. Die visuellen HTML-Attribute werden in passende CSS-Deklarationen übersetzt. Sie werden als Autorenregeln mit niedriger Priorität behandelt.

Spezifität

Die Selektorspezifität wird in der CSS2-Spezifikation folgendermaßen definiert:

  • 1, wenn die Deklaration eher von einem "style"-Attribut statt einer Regel mit einem Selektor stammt, ansonsten 0 (= a)
  • Anzahl der ID-Attribute im Selektor (=b)
  • Anzahl der anderen Attribute und Pseudoklassen im Selektor (=c)
  • 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) liefert die Spezifität.

Die Zahlenbasis, die Sie verwenden müssen, wird von Ihrem höchsten Wert in einer der Kategorien definiert.
Beispiel: Wenn a=14 ist, können Sie eine Hexadezimalbasis verwenden. Im unwahrscheinlichen Fall, dass a=17 ist, benötigen Sie eine siebzehnstellige Zahlenbasis. Diese Situation kann bei einem Selektor wie diesem auftreten: html body div div p ... (17 Tags in Ihrem Selektor ... nicht sehr wahrscheinlich)

Einige 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 */

Sortieren der Regeln

Nachdem die Regeln zugeordnet wurden, werden sie entsprechend den Kaskadierungsregeln sortiert. WebKit verwendet Bubblesort für kurze und Mergesort für lange Listen. WebKit implementiert die Sortierung durch Überschreiben des ">"-Operators für die Regeln:

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

Schrittweiser Prozess

WebKit verwendet eine Markierung, um zu kennzeichnen, ob alle Top-Level-Stylesheets (einschließlich @imports) geladen wurden. Wurde der Stil beim Anhängen noch nicht vollständig geladen, werden Platzhalter verwendet und dies wird im Dokument markiert. Sobald die Stylesheets geladen wurden, erfolgt eine erneute Berechnung.

Layout

Wenn der Renderer erstellt und zur Baumstruktur hinzugefügt wird, verfügt er weder über Position noch Größe. Die Berechnung dieser Werte wird als Layout oder Reflow bezeichnet.

HTML verwendet ein Flusslayout-Modell, mit dem es meistens möglich ist, die Geometrie in einem einzigen Durchlauf zu berechnen. Elemente, die später "im Fluss" auftauchen, beeinflussen typischerweise nicht die Geometrie von Elementen, die früher "im Fluss" aufgetreten sind. So kann das Layout von links nach rechts und oben nach unten das Dokument durchlaufen. Es gibt Ausnahmen - so können HTML-Tabellen beispielsweise mehr als einen Durchlauf erfordern (3.5).

Das Koordinatensystem ist relativ zum Stamm-Frame. Es werden obere und linke Koordinaten verwendet.

Das Layout ist ein rekursiver Prozess. Es beginnt beim Stamm-Renderer, der dem <html>-Element des HTML-Dokuments entspricht. Das Layout durchläuft rekursiv einen Teil der oder die gesamte Frame-Hierarchie und berechnet geometrische Informationen für jeden Renderer, der sie anfordert.

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

Alle Renderer verfügen über eine Layout- oder Reflow-Methode und jeder Renderer ruft die Layout-Methode seiner untergeordneten Elemente auf, die ein Layout benötigen.

Dirty Bit-System

Damit Browser nicht für jede kleine Änderung ein komplettes Layout vornehmen müssen, verwenden sie ein sogenanntes "Dirty Bit-System". Ein Renderer, der geändert oder hinzugefügt wurde, markiert sich selbst und seine untergeordneten Elemente als "dirty" = layoutbedürftig.

Es gibt zwei Markierungen: "dirty" und "children are dirty". "Children are dirty" bedeutet, dass zwar der Renderer selbst in Ordnung sein kann, aber mindestens ein untergeordnetes Element layoutbedürftig ist.

Globales und inkrementelles Layout

Das Layout kann auf der gesamten Rendering-Struktur ausgelöst werden. In diesem Fall handelt es sich um ein "globales" Layout. Dies kann folgende Ursachen haben:

  1. Eine globale Stiländerung, die alle Renderer betrifft, z. B. die Änderung der Schriftgröße
  2. Eine Änderung der Bildschirmgröße

Das Layout kann auch inkrementell sein. Dabei werden nur die "dirty" Renderer einem Layout unterzogen (dies kann Auswirkungen haben, die wiederum zusätzliches Layout erforderlich machen).
Das inkrementelle Layout wird (asynchron) ausgelöst, wenn Renderer als "dirty" markiert sind. Dies ist zum Beispiel der Fall, wenn neue Renderer an die Rendering-Struktur angehängt werden, nachdem zusätzliche Inhalte aus dem Netzwerk empfangen und zum DOM-Baum hinzugefügt wurden.

Abbildung : Inkrementelles Layout - nur layoutbedürftige Renderer und ihre untergeordneten Elemente erhalten ein neues Layout (3.6).

Asynchrones und synchrones Layout

Das inkrementelle Layout erfolgt asynchron. Firefox stellt "Reflow-Befehle" für inkrementelle Layouts in eine Warteschlange und ein Planer löst die Ausführung dieser Befehle im Batch aus. WebKit verfügt ebenfalls über einen Timer, der ein inkrementelles Layout ausführt. Die Baumstruktur wird durchlaufen und das Layout von Renderern, die als "dirty" markiert sind, wird aktualisiert.
Skripts wie "offsetHeight", die Stilinformationen anfordern, können ein inkrementelles Layout auch synchron auslösen.
Das globale Layout erfolgt normalerweise synchron.
Manchmal wird das Layout auch als Rückruf nach einem Anfangslayout ausgelöst, da einige Attribute wie etwa die Scrolling-Position sich geändert haben.

Optimierungen

Wenn ein Layout durch eine Größenanpassung oder eine Änderung der Rendererposition (nicht der Größe) ausgelöst wird, werden die Renderergrößen aus einem Cache abgerufen und nicht neu berechnet.
In einigen Fällen wird nur eine Unterstruktur bearbeitet und das Layout startet nicht vom Stamm aus. Dies kann vorkommen, wenn die Änderung lokal ist und keinen Einfluss auf ihre Umgebung hat, zum Beispiel Text, der in Textfelder eingefügt wird (andernfalls würde jeder Tastendruck ein Layout vom Stamm aus hervorrufen).

Der Layout-Prozess

Das Layout weist normalerweise folgendes Muster auf:

  1. Der übergeordnete Renderer bestimmt seine eigene Breite.
  2. Das übergeordnete Element hat Vorrang vor untergeordneten Elementen und:
    1. platziert den untergeordneten Renderer (legt seine x- und y-Koordinaten fest).
    2. fordert gegebenenfalls ein Layout der untergeordneten Elemente an (falls sie z. B. als "dirty" markiert sind oder ein globales Layout durchgeführt wird) - so wird die Höhe der untergeordneten Elemente bestimmt.
  3. Das übergeordnete Element verwendet die gesammelte Höhe der untergeordneten Elemente und die Höhe der Ränder und Abstände, um seine eigene Höhe festzulegen. Diese wird vom übergeordneten Element des übergeordneten Renderers verwendet.
  4. Die "Dirty Bit"-Markierung wird auf "false" gesetzt.

Firefox verwendet ein "state"-Zustandsobjekt (nsHTMLReflowState) als Parameter für das Layout ("Reflow" genannt). Der Zustand umfasst unter anderem die Breite des übergeordneten Elements.
Das Firefox-Layout gibt ein "metrics"-Objekt aus (nsHTMLReflowMetrics). Dieses enthält die berechnete Renderer-Höhe.

Breitenberechnung

Die Breite des Renderers wird mithilfe der Breite des Containerblocks, der "width"-Stileigenschaft des Renderers sowie den Rändern und Rahmen berechnet.
Die Breite des folgenden "div"-Elements:

<div style="width:30%"/>
würde von WebKit beispielsweise folgendermaßen berechnet (Klasse "RenderBox", Methode "calcWidth"):
  • Die Containerbreite ist das Maximum aus der "availableWidth" des Containers und 0. Die "availableWidth" ist in diesem Fall die "contentWidth", die folgendermaßen berechnet wird:
    clientWidth() - paddingLeft() - paddingRight()
    
    "clientWidth" und "clientHeight" stehen für das Innere eines Objekts ohne Rahmen und Bildlaufleiste.
  • Die Breite der Elemente wird im "width"-Stilattribut angegeben. Diese wird als absoluter Wert berechnet, indem der Prozentsatz der Containerbreite ermittelt wird.
  • Die horizontalen Rahmen und Abstände werden jetzt hinzugefügt.
Bislang haben wir die "bevorzugte Breite" berechnet. Nun werden die minimale und die maximale Breite berechnet.
Ist die bevorzugte Breite größer als die maximale Breite, wird die maximale Breite verwendet. Ist sie kleiner als die minimale Breite (die kleinste feststehende Einheit), wird die minimale Breite verwendet.

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

Zeilenumbruch

Dieser erfolgt, wenn ein Renderer mitten im Layout entscheidet, dass eine Aufteilung erforderlich ist. In diesem Fall stoppt er und teilt seinem übergeordneten Element mit, dass er aufgeteilt werden muss. Das übergeordnete Element erstellt die zusätzlichen Renderer und fordert ein Layout für diese an.

Painting

In der Painting-Phase wird die Rendering-Struktur durchlaufen und die "paint"-Methode der Renderer aufgerufen, um deren Inhalt auf dem Bildschirm darzustellen. Painting verwendet die UI-Infrastrukturkomponente.

Global und inkrementell

Wie das Layout kann auch das Painting global, also auf die gesamte Struktur bezogen, oder inkrementell sein. Beim inkrementellen Painting ändern sich einige Renderer auf eine Art, die sich nicht auf die gesamte Struktur auswirkt. Durch die Änderung des Renderers wird sein Rechteck auf dem Bildschirm ungültig. Das Betriebssystem sieht dies dann als "dirty region" an und löst ein "paint"-Ereignis aus. Das Betriebssystem geht intelligent vor und fasst mehrere Regionen in einer zusammen. In Chrome ist dies etwas komplizierter, da der Renderer sich in einem anderen Vorgang als der Hauptprozess befindet. Chrome simuliert das Betriebssystemverhalten in gewissem Maß. Die Darstellung erkennt diese Ereignisse und delegiert die Meldung an den Rendering-Stamm. Die Baumstruktur wird durchlaufen, bis der relevante Renderer erreicht ist. Dieser aktualisiert seine Darstellung (und normalerweise die seiner untergeordneten Elemente) selbst.

Painting-Reihenfolge

In CSS2 wird die Reihenfolge des Painting-Prozesses definiert. Dabei handelt es sich tatsächlich um die Reihenfolge, in der die Elemente in den Stapelkontexten gestapelt sind. Diese Reihenfolge wirkt sich auf das Painting aus, da die Stapel von hinten nach vorne dargestellt werden. Die Stapelreihenfolge eines Block-Renderers sieht so aus:
  1. Hintergrundfarbe
  2. Hintergrundbild
  3. Rahmen
  4. Untergeordnete Elemente
  5. Umriss

Firefox-Displayliste

Firefox durchläuft die Rendering-Struktur und erstellt eine Display-Liste für das dargestellte Rechteck. Diese enthält die relevanten Renderer für das Rechteck in der richtigen Painting-Reihenfolge (Hintergründe der Renderer, dann Rahmen usw.). Auf diese Weise muss die Struktur für eine Darstellungsaktualisierung nur ein Mal durchlaufen werden. Dabei werden alle Hintergründe, dann alle Bilder, dann alle Rahmen usw. dargestellt.

Firefox optimiert den Ablauf, indem ausgeblendete Elemente nicht hinzugefügt werden. Dies sind zum Beispiel Elemente, die vollständig von anderen, undurchsichtigen Elementen verdeckt sind.

WebKit-Rechteckspeicher

Vor der Darstellungsaktualisierung speichert WebKit das alte Rechteck als Bitmap. Anschließend wird nur das Delta zwischen den neuen und den alten Rechtecken dargestellt.

Dynamische Änderungen

Die Browser versuchen, in Reaktion auf eine Änderung möglichst geringen Aufwand zu betreiben. Bei Änderungen an der Farbe eines Elements wird daher nur die Darstellung des Elements aktualisiert. Bei Änderungen an der Position des Elements werden das Layout und die Darstellung des Elements, seiner untergeordneten Elemente und möglicher gleichgeordneter Elemente aktualisiert. Beim Hinzufügen eines DOM-Knotens werden das Layout und die Darstellung des Knotens aktualisiert. Bei großen Änderungen, wie das Vergrößern der Schrift des "html"-Elements, werden die Caches ungültig und Layout sowie Darstellung der gesamten Struktur müssen aktualisiert werden.

Rendering-Modul-Threads

Das Rendering-Modul ist ein Single-Thread-Modul. Fast alles, bis auf die Netzwerkvorgänge, spielt sich in einem einzelnen Thread ab. Bei Firefox und Safari ist dies der Haupt-Thread des Browsers. In Chrome ist es der Haupt-Thread des Tab-Prozesses.
Netzwerkvorgänge können von mehreren parallelen Threads durchgeführt werden. Die Anzahl der parallelen Verbindung ist beschränkt (normalerweise zwei bis sechs Verbindungen. Firefox 3 verwendet zum Beispiel sechs).

Ereignisschleife

Beim Haupt-Thread des Browsers handelt es sich um eine Ereignisschleife. Dies ist eine Endlosschleife, die den Prozess am Leben erhält. Sie wartet auf Ereignisse, zum Beispiel Layout- oder Paint-Ereignisse, und verarbeitet diese. Dies ist der Firefox-Code für die Hauptereignisschleife:
while (!mExiting)
    NS_ProcessNextEvent(thread);

Visuelles CSS2-Modell

Canvas

Laut 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. Die Canvas-Größe ist für jede Abmessung des Bereichs unendlich, aber die Browser wählen eine Anfangsbreite basierend auf den Abmessungen des Viewport aus.

Gemäß www.w3.org/TR/CSS2/zindex.html ist der Canvas-Bereich transparent, wenn er sich innerhalb eines anderen Canvas-Elements befindet. Andernfalls wird eine vom Browser definierte Farbe verwendet.

CSS-Boxmodell

Im CSS-Boxmodell werden die rechteckigen Felder beschrieben, die für Elemente in der Dokumentstruktur erstellt und gemäß dem visuellen Formatierungsmodell dargestellt werden.
Jede Box verfügt über einen Inhaltsbereich (z. B. Text, ein Bild usw.) und optional umliegende Abstände, Rahmen und Ränder.

Abbildung : CSS2-Boxmodell

Jeder Knoten generiert 0..n solcher Boxen.
Alle Elemente verfügen über eine "display"-Eigenschaft, die den Boxtyp bestimmt, der für sie erstellt wird. Beispiele:

block  - generates a block box.
inline - generates one or more inline boxes.
none - no box is generated.
Das Standardelement ist "inline", aber das Browser-Stylesheet legt andere Standards fest. Die Standard-Display-Eigenschaft für das "div"-Element ist zum Beispiel "block".
Hier finden Sie ein Beispiel für ein Standard-Stylesheet: www.w3.org/TR/CSS2/sample.html.

Positionierungsschema

Es gibt drei Schemas:

  1. Normal - das Objekt wird entsprechend seiner Platzierung im Dokument positioniert, das heißt, sein Platz im Rendering-Baum entspricht seinem Platz im DOM-Baum und es wird in Übereinstimmung mit seinem Boxtyp und seinen Abmessungen dargestellt.
  2. Float - das Objekt wird zuerst wie im normalen Ablauf dargestellt und dann so weit nach links bzw. rechts wie möglich verschoben.
  3. Absolut - dem Objekt wird im Rendering-Baum ein anderer Platz zugewiesen als im DOM-Baum.

Das Positionierungsschema wird von der "position"-Eigenschaft und dem "float"-Attribut bestimmt.

  • "Statisch" und "relativ" lösen einen normalen Ablauf aus.
  • "Absolut" und "fest" führen zu einer absoluten Positionierung.

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

Die Darstellungsart der Box wird von folgenden Elementen bestimmt:

  • Boxtyp
  • Boxabmessungen
  • Positionierungsschema
  • Externe Informationen wie Bildgröße und Bildschirmgröße

Boxtypen

Block-Box: bildet einen Block mit einem eigenen Rechteck im Browserfenster

Abbildung : Block-Box

Inline-Box: verfügt nicht über einen eigenen Block, sondern befindet sich innerhalb eines übergeordneten Blocks

Abbildung : Inline-Boxen

Blöcke werden vertikal nacheinander formatiert. Inline-Boxen werden horizontal formatiert.

Abbildung : Block- und Inline-Formatierung

Inline-Boxen werden innerhalb von Zeilen oder "Zeilenboxen" platziert. Die Zeilen sind mindestens so hoch wie die höchste Box, können aber auch höher sein, wenn die Boxen an der "Basislinie" ausgerichtet sind. Das bedeutet, der untere Teil eines Elements ist an einem Punkt einer anderen Box ausgerichtet, bei dem es sich nicht um den unteren Teil handelt. Falls die Containerbreite nicht ausreicht, werden die Inline-Boxen in mehrere Zeilen aufgeteilt. Dies passiert zum Beispiel in einem Absatz.

Abbildung : Zeilen

Positionierung

Relativ

Relative Positionierung - positioniert wie gewöhnlich und dann um das erforderliche Delta verschoben

Abbildung : Relative Positionierung

Floats

Eine Float-Box wird an die linke oder rechte Seite einer Zeile verschoben. Interessant ist, dass die anderen Boxen darum 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>
Dies sieht dann so aus:

Abbildung : Float

Absolut und fest

Das Layout wird genau definiert, unabhängig von dem normalen Fluss. Das Element wird nicht in den normalen Fluss integriert. Die Abmessungen sind relativ zum Container. Bei der festen Positionierung ist der Container der Viewport.

Abbildung : Feste Positionierung

Hinweis: Die feste Box bewegt sich selbst dann nicht, wenn das Dokument gescrollt wird!

Ebenendarstellung

Diese wird von der CSS-Eigenschaft "z-index" angegeben. Sie stellt die dritte Dimension der Box dar, ihre Position entlang der z-Achse.

Die Boxen werden in Stapel aufgeteilt (Stapelkontexte genannt). In jedem Stapel werden zuerst die hinteren Elemente und dann darüber die vorderen Elemente dargestellt, da sie näher am Nutzer sind. Im Falle einer Überlappung wird das vorangegangene Element ausgeblendet.
Die Stapel werden entsprechend der "z-index"-Eigenschaft angeordnet. Boxen mit "z-index"-Eigenschaft bilden einen lokalen Stapel. Der Viewport umfasst den äußeren Stapel.

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:

Abbildung : Feste Positionierung

Obwohl das rote "div"-Element in der Auszeichnungssprache vor dem grünen steht und im regulären Ablauf davor dargestellt worden wäre, ist die "z-index"-Eigenschaft höher. Darum wird es im Stapel der Stammbox weiter nach vorne verschoben.

Ressourcen

Übersetzungen

Diese Seite wurde ins Japanische übersetzt - zweimal! Funktionsweise von Browsern: Hinter den Kulissen moderner Webbrowser (ja) von @_kosei_ sowie ブラウザってどうやって動いてるの?(モダンWEBブラウザシーンの裏側 von @ikeike443 und @kiyoto01. Vielen Dank an alle!

Comments

0