Funktionsweise von Browsern

Moderne Webbrowser – ein Blick hinter die Kulissen

Vorwort

Diese umfassende Einführung in die internen Abläufe von WebKit und Gecko ist das Ergebnis umfangreicher Recherchen der israelischen Entwicklerin Tali Garsiel. Im Laufe der Jahre hat sie alle veröffentlichten Daten zu den internen Funktionen von Browsern überprüft und viel Zeit damit verbracht, den Quellcode von Webbrowsern zu lesen. Sie schrieb:

Als Webentwickler können Sie durch das Erlernen der internen Abläufe von Browsern bessere Entscheidungen treffen und die Begründungen für Best Practices bei der Entwicklung kennen. Dies ist zwar ein ziemlich langes Dokument, aber wir empfehlen dir, dich etwas genauer mit ihnen zu beschäftigen. Sie werden froh sein, dass Sie es getan haben.

Paul Irish, Chrome Developer Relations

Einführung

Webbrowser sind die am häufigsten verwendete Software. In diesem Artikel erkläre ich, wie sie im Hintergrund funktionieren. Geben Sie google.com in die Adressleiste ein, bis die Google-Seite auf dem Browserbildschirm erscheint.

Browser, über die wir sprechen

Es gibt fünf gängige Browser für Desktop-Computer: Chrome, Internet Explorer, Firefox, Safari und Opera. Auf Mobilgeräten sind die wichtigsten Browser der Android-Browser, iPhone, Opera Mini und Opera Mobile, UC Browser, die Nokia S40/S60-Browser und Chrome. Mit Ausnahme der Opera-Browser basieren alle auf WebKit. Ich werde Beispiele aus den Open-Source-Browsern Firefox und Chrome sowie aus Safari (teilweise Open Source) nennen. Laut StatCounterstatistiken (Stand: Juni 2013) machten Firefox und Safari etwa 71% der weltweiten Desktop-Browsernutzung aus. Auf Mobilgeräten machen der Android-Browser, das iPhone und Chrome zusammen etwa 54% der Nutzung aus.

Die Hauptfunktionen des Browsers

Die Hauptfunktion eines Browsers besteht darin, die von Ihnen ausgewählte Webressource anzuzeigen, indem Sie sie vom Server anfordern und im Browserfenster anzeigen. Die Ressource ist in der Regel ein HTML-Dokument, kann aber auch eine PDF-Datei, ein Bild oder eine andere Art von Inhalt sein. Der Speicherort der Ressource wird vom Nutzer mit einem URI (Uniform Resource Identifier) angegeben.

Wie der Browser HTML-Dateien interpretiert und darstellt, wird in den HTML- und CSS-Spezifikationen festgelegt. Diese Spezifikationen werden vom W3C (World Wide Web Consortium) verwaltet, der Standardsorganisation für das Web. Jahrelang haben Browser nur einen Teil der Spezifikationen eingehalten und eigene Erweiterungen entwickelt. Das führte zu ernsthaften Kompatibilitätsproblemen für Webautoren. Heutzutage entsprechen die meisten Browser mehr oder weniger den Spezifikationen.

Die Benutzeroberflächen von Browsern haben viel gemeinsam. 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 aktuelle Dokumente zu aktualisieren oder das Laden zu beenden
  5. Startbildschirmtaste, über die Sie zur Startseite gelangen

Seltsamerweise ist die Benutzeroberfläche des Browsers in keiner formalen Spezifikation festgelegt, sondern aus bewährten Vorgehensweisen, die sich im Laufe der Jahre entwickelt haben und durch die sich gegenseitig imitierenden Browser. Die HTML5-Spezifikation definiert keine UI-Elemente, die ein Browser haben muss, sondern listet einige gängige Elemente auf. Dazu gehören die Adressleiste, die Statusleiste und die Symbolleiste. Natürlich gibt es Funktionen, die nur für einen bestimmten Browser gelten, z. B. der Downloadmanager von Firefox.

Gesamtarchitektur

Die Hauptkomponenten des Browsers sind:

  1. Benutzeroberfläche: Dazu gehören die Adressleiste, die Schaltflächen „Zurück“ und „Weiter“, das Lesezeichenmenü usw. Alle Bereiche des Browserfensters mit Ausnahme des Fensters, in dem die angeforderte Seite angezeigt wird.
  2. Browser-Engine: Übermittelt Aktionen zwischen der Benutzeroberfläche und der Rendering-Engine.
  3. Das Rendering-Modul: verantwortlich für die Anzeige des angeforderten Inhalts Wenn der angeforderte Inhalt beispielsweise HTML ist, parset das Rendering-Engine HTML und CSS und zeigt den geparsten Inhalt auf dem Bildschirm an.
  4. Netzwerk: Für Netzwerkaufrufe wie HTTP-Anfragen mit unterschiedlichen Implementierungen für unterschiedliche Plattformen hinter einer plattformunabhängigen Schnittstelle.
  5. UI-Backend: Wird zum Zeichnen einfacher Widgets wie Drop-down-Menüs und Fenster verwendet. Dieses Backend stellt eine generische Schnittstelle bereit, die nicht plattformspezifisch ist. Darunter werden Benutzeroberflächenmethoden des Betriebssystems verwendet.
  6. JavaScript-Interpreter Wird zum Parsen und Ausführen von JavaScript-Code verwendet.
  7. Datenspeicherung: Dies ist eine Persistenzschicht. Der Browser muss möglicherweise alle Arten von Daten lokal speichern, etwa Cookies. Browser unterstützen auch Speichermechanismen wie localStorage, IndexedDB, WebSQL und FileSystem.
Browserkomponenten
Abbildung 1: Browserkomponenten

Beachten Sie, dass in Browsern wie Chrome mehrere Instanzen der Rendering-Engine ausgeführt werden: eine für jeden Tab. Jeder Tab wird in einem separaten Prozess ausgeführt.

Rendering-Module

Die Aufgabe des Rendering-Moduls ist naja... das Rendern, d. h. die Anzeige der angeforderten Inhalte auf dem Browserbildschirm.

Standardmäßig kann das 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: das Anzeigen von HTML und Bildern, die mit CSS formatiert sind.

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

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

Der Hauptfluss

Die Rendering-Engine ruft den Inhalt des angeforderten Dokuments von der Netzwerkschicht ab. Dies geschieht in der Regel in 8-KB-Chunks.

Danach erfolgt der grundlegende Ablauf der Rendering-Engine:

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

Die Rendering-Engine beginnt mit dem Parsen des HTML-Dokuments und wandelt Elemente in DOM-Knoten in einem Baum um, der als „Inhaltsbaum“ bezeichnet wird. Die Engine analysiert die Stildaten sowohl in externen CSS-Dateien als auch in Stilelementen. Stilinformationen zusammen mit visuellen Anweisungen in der HTML-Datei werden verwendet, um einen weiteren Baum zu erstellen: den Renderbaum.

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 dem Erstellen des Renderbaums wird ein Layout erstellt. Das bedeutet, dass Sie jedem Knoten die genauen Koordinaten angeben müssen, 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-Back-End-Ebene dargestellt.

Es ist wichtig zu verstehen, dass dies ein allmählicher Prozess ist. Für eine bessere Nutzerfreundlichkeit versucht die Rendering-Engine, Inhalte so schnell wie möglich auf dem Bildschirm anzuzeigen. Es wird nicht gewartet, bis das gesamte HTML geparst wurde, bevor mit dem Erstellen und Layouten des Renderbaums begonnen wird. Teile des Inhalts werden geparst und angezeigt, während der Vorgang mit dem Rest des Inhalts fortgesetzt wird, der kontinuierlich vom Netzwerk kommt.

Beispiele für den Hauptablauf

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

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

Gecko nennt den Baum der visuell formatierten Elemente einen „Frame-Baum“. Jedes Element ist ein Frame. In WebKit wird der Begriff „Render Tree“ verwendet. Er besteht aus „Renderobjekten“. WebKit verwendet den Begriff „Layout“ für das Platzieren von Elementen, während Gecko „Neuformatierung“ verwendet. „Attachment“ ist der Begriff von WebKit für die Verbindung von DOM-Knoten und visuellen Informationen zum Erstellen des Renderbaums. Ein kleiner, nicht semantischer Unterschied besteht darin, dass Gecko eine zusätzliche Schicht zwischen dem HTML- und dem DOM-Baum hat. Er wird als „Content-Sink“ bezeichnet und ist eine Fabrik für die Erstellung von DOM-Elementen. Wir werden jeden Teil des Ablaufs besprechen:

Parsing – allgemein

Da das Parsen ein sehr wichtiger Prozess in der Rendering-Engine ist, gehen wir etwas näher darauf ein. Beginnen wir mit einer kurzen Einführung in das Parsen.

Das Parsen eines Dokuments bedeutet, es in eine Struktur zu übersetzen, die vom Code verwendet werden kann. Das Ergebnis des Parsings ist in der Regel ein Knotenbaum, der die Struktur des Dokuments darstellt. Dies wird als Parsebaum oder Syntaxbaum bezeichnet.

Das Parsen des Ausdrucks 2 + 3 - 1 könnte beispielsweise diesen Baum zurückgeben:

Knoten der Struktur für mathematische Ausdrücke.
Abbildung 5: Baumknoten für mathematischen Ausdruck

Grammatik

Das Parsen basiert auf den Syntaxregeln, die für das Dokument gelten: der Sprache oder dem Format, in dem es geschrieben wurde. Jedes Format, das Sie parsen können, muss eine deterministische Grammatik mit Wortschatz- und Syntaxregeln haben. Sie wird als kontextfreie Grammatik bezeichnet. Menschliche Sprachen sind keine solchen Sprachen und können daher nicht mit herkömmlichen 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 unterteilt. Tokens sind das Vokabular der Sprache: die Sammlung gültiger Bausteine. In der menschlichen Sprache besteht er aus allen Wörtern, die im Wörterbuch der entsprechenden Sprache vorkommen.

Bei der Syntaxanalyse werden die Syntaxregeln der Sprache angewendet.

Parser teilen die Arbeit normalerweise auf zwei Komponenten auf: den Lexer (manchmal auch Tokenizer genannt), der für die Aufteilung der Eingabe in gültige Tokens zuständig ist, und den Parser, der für die Erstellung des Parserbaums verantwortlich ist. Dazu wird die Dokumentstruktur gemäß den Syntaxregeln der Sprache analysiert.

Der Lexer kann irrelevante Zeichen wie Leerzeichen und Zeilenumbrüche entfernen.

Vom Quelldokument zur Parsing-Struktur
Abbildung 6: Vom Quelldokument zur Parsing-Struktur

Der Parsevorgang 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 Parsebaum ein dem Token entsprechender Knoten hinzugefügt und der Parser fordert ein weiteres Token an.

Wenn keine Regel übereinstimmt, speichert der Parser das Token intern und fragt weiter nach Tokens, 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 Dokument war also ungültig und enthielt Syntaxfehler.

Übersetzung

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

Ablauf der Zusammenstellung
Abbildung 7: Kompilierungsablauf

Beispiel für das Parsen

In Abbildung 5 haben wir einen Parsebaum aus einem mathematischen Ausdruck erstellt. Versuchen wir, eine einfache mathematische Sprache zu definieren und den Parsevorgang zu sehen.

Syntax:

  1. Die Bausteine der Sprachsyntax sind Ausdrücke, Begriffe und Vorgänge.
  2. Unsere Sprache kann beliebig viele Ausdrücke enthalten.
  3. Ein Ausdruck ist definiert als „term“, gefolgt von einer „Operation“, gefolgt von einem weiteren Term
  4. Ein Vorgang ist ein Plus- oder Minuszeichen.
  5. Ein Ausdruck ist ein Ganzzahltoken oder ein Ausdruck.

Lassen Sie uns die Eingabe-2 + 3 - 1 analysieren.

Der erste Teilstring, der mit einer Regel übereinstimmt, ist 2. Gemäß Regel 5 ist dies ein Begriff. Die zweite Übereinstimmung ist 2 + 3. Dies entspricht der dritten Regel: ein Begriff gefolgt von einem Operator gefolgt von einem anderen 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 einem Vorgang, gefolgt von einem weiteren Term. 2 + + stimmt mit keiner Regel überein und ist daher eine ungültige Eingabe.

Formale Definitionen für Vokabular und Syntax

Der Wortschatz wird in der Regel durch reguläre Ausdrücke ausgedrückt.

Unsere 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 im Format BNF definiert. Unsere Sprache sieht dann so aus:

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 zur kontextfreien Grammatik.

Arten von Parsern

Es gibt zwei Arten von Parsern: Top-down-Parser und Bottom-up-Parser. Eine intuitive Erklärung ist, dass Top-Down-Parser die Syntax auf hoher Ebene prüfen und versuchen, eine Übereinstimmung mit einer Regel zu finden. Bottom-up-Parser beginnen mit der Eingabe und wandeln sie schrittweise in die Syntaxregeln um, angefangen bei den Low-Level-Regeln bis zur Erfüllung der übergeordneten Regeln.

Sehen wir uns an, wie die beiden Arten von Parsern unser Beispiel verarbeiten.

Der Top-Down-Parser beginnt mit der Regel der höheren Ebene: Er identifiziert 2 + 3 als Ausdruck. 2 + 3 - 1 wird dann als Ausdruck identifiziert. Der Prozess der Identifizierung des Ausdrucks entwickelt sich, indem die anderen Regeln abgeglichen werden, aber der Ausgangspunkt ist die Regel der obersten Ebene.

Der Bottom-up-Parser scannt die Eingabe, bis eine Regel abgeglichen wird. Dann wird die übereinstimmende Eingabe durch die Regel ersetzt. Dies wird bis zum Ende der Eingabe fortgesetzt. Der teilweise übereinstimmende Ausdruck wird auf den Stack des Parsers gelegt.

Stapeln Eingabe
2 + 3 − 1
Begriff + 3 – 1
Begriffsoperation 3–1
Ausdruck – 1
Ausdrucksoperation 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 allmählich auf Syntaxregeln reduziert wird.

Parser automatisch generieren

Es gibt Tools, die einen Parser generieren können. Sie füttern die Grammatik Ihrer Sprache - deren Vokabular und Syntaxregeln - und sie generieren einen funktionierenden Parser. Das Erstellen eines Parsers erfordert ein umfassendes Verständnis des Parsings und es ist nicht einfach, einen optimierten Parser von Hand zu erstellen. Daher können Parsergeneratoren sehr nützlich sein.

WebKit verwendet zwei bekannte Parsergeneratoren: Flex zum Erstellen eines Lexers und Bison zum Erstellen eines Parsers (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. Die Eingabe von Bison sind die Sprachsyntaxregeln im BNF-Format.

HTML-Parser

Die Aufgabe des HTML-Parsers besteht darin, das HTML-Markup in einen Parsebaum 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 formell mithilfe von Formaten wie BNF definiert werden.

Leider treffen alle konventionellen Parser-Themen nicht auf HTML (ich habe sie nicht nur aus Spaß erwähnt, sie werden beim Parsen von CSS und JavaScript verwendet). 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). Es handelt sich jedoch nicht um eine kontextfreie Grammatik.

Dies erscheint auf den ersten Blick seltsam. HTML ist XML sehr ähnlich. XML-Parser sind viele verfügbar. Es gibt eine XML-Variante von HTML – XHTML. Was ist der große Unterschied?

Der Unterschied besteht darin, dass der HTML-Ansatz eher umsichtig ist: Sie können bestimmte Tags (die dann implizit hinzugefügt werden) oder manchmal Start- oder End-Tags weglassen usw. Insgesamt handelt es sich um eine "weiche" Syntax im Gegensatz zur steifen und anspruchsvollen XML-Syntax.

Dieses vermeintlich kleine Detail macht einen großen Unterschied. Das ist einerseits der Hauptgrund, warum HTML so beliebt ist: Es verzeiht Fehler und erleichtert die Arbeit des Webautors. Andererseits erschwert es die Erstellung einer formellen Grammatik. Zusammenfassend lässt sich sagen, dass HTML nicht einfach mit herkömmlichen Parsern geparst werden kann, da seine Grammatik nicht kontextfrei ist. HTML kann nicht von XML-Parsern geparst werden.

HTML-DTD

Die HTML-Definition ist im DTD-Format. Mit diesem Format werden Sprachen der SGML-Familie definiert. Das Format enthält Definitionen für alle zulässigen Elemente, ihre Attribute und ihre Hierarchie. Wie bereits erwähnt, bildet die HTML-DTD keine kontextfreie Grammatik.

Es gibt einige Varianten der DTD. Der strenge Modus entspricht nur den Spezifikationen, andere Modi unterstützen jedoch Markup, das in der Vergangenheit von Browsern verwendet wurde. Das soll die Abwärtskompatibilität mit älteren Inhalten gewährleisten. Die aktuelle strenge DTD finden Sie hier: www.w3.org/TR/html4/strict.dtd

DOM

Der Ausgabebaum (der „Parsebaum“) ist ein Baum aus DOM-Element- und Attributknoten. DOM steht für Document Object Model. Es ist die Objektpräsentation des HTML-Dokuments und die Schnittstelle von HTML-Elementen nach außen, z. B. JavaScript.

Der Stamm der Baumstruktur ist das Document-Objekt.

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 die folgende DOM-Hierarchie umgewandelt:

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

Wie HTML wird auch das DOM von der W3C-Organisation festgelegt. Weitere Informationen finden Sie unter www.w3.org/DOM/DOMTR. Es ist eine generische Spezifikation für die Manipulation 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 mit anderen Attributen, die vom Browser intern verwendet werden.

Der Parsealgorithmus

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 fehlertolerante Natur der Sprache.
  2. Die Tatsache, dass Browser eine traditionelle Fehlertoleranz haben, um bekannte Fälle ungültiger HTML-Code zu unterstützen.
  3. Der Parsevorgang ist reentrant. Bei anderen Sprachen ändert sich die Quelle beim Parsen nicht. In HTML können jedoch durch dynamischen Code (z. B. Scriptelemente mit document.write()-Aufrufen) zusätzliche Tokens hinzugefügt werden, sodass die Eingabe durch den Parsevorgang tatsächlich geändert wird.

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

Der Parse-Algorithmus wird 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 Token zerlegt wird. Zu den HTML-Tokens gehören Start- und End-Tags, Attributnamen und Attributwerte.

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

HTML-Parsevorgang (aus der HTML5-Spezifikation)
Abbildung 9: HTML-Parseablauf (aus der HTML5-Spezifikation)

Der Tokenisierungsalgorithmus

Der Algorithmus gibt ein HTML-Token aus. Der Algorithmus wird als Zustandsautomat ausgedrückt. Jeder Status verbraucht ein oder mehrere Zeichen des Eingabestreams und aktualisiert den nächsten Status entsprechend diesen Zeichen. Die Entscheidung wird vom aktuellen Tokenisierungsstatus und vom Status der Baumkonstruktion beeinflusst. Das bedeutet, dass dasselbe konsumierte Zeichen je nach aktuellem Status unterschiedliche Ergebnisse für den korrekten nächsten Status 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 „Data“-Zustand. Wenn das Zeichen < gefunden wird, ändert sich der Status in „Tag-offen-Status“. Wenn ein a-z-Zeichen verarbeitet wird, wird ein „Start-Tag-Token“ erstellt und der Status in „Tag-Namen-Status“ geändert. Wir bleiben in diesem Status, 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 ist, wird das aktuelle Token ausgegeben und der Status wechselt zurück in "Data". Für das <body>-Tag werden dieselben Schritte ausgeführt. Bisher wurden die Tags html und body gesendet. Wir sind jetzt wieder bei Datenstatus. Wenn das Zeichen H von Hello world konsumiert wird, wird ein Zeichentoken erstellt und gesendet. Dies geschieht so lange, bis das Zeichen < von </body> erreicht wird. Für jedes Zeichen von Hello world wird ein Zeichentoken ausgegeben.

Wir sind jetzt wieder beim Status „Tag geöffnet“. Wenn die nächste Eingabe / verarbeitet wird, wird ein end tag token erstellt und der Status in „Tag-Namenstatus“ geändert. Wir bleiben in diesem Status, bis wir > erreichen.Dann wird das neue Tag-Token ausgegeben und wir kehren zum Datenstatus zurück. Die </html>-Eingabe wird wie der vorherige Fall behandelt.

Beispieleingaben tokenisieren
Abbildung 10: Tokenisierung der Beispieleingaben

Algorithmus zum Erstellen von Bäumen

Wenn der Parser erstellt wird, wird auch das Document-Objekt erstellt. Während der Baumkonstruktionsphase wird der DOM-Baum mit dem Dokument als Wurzel geändert und ihm werden Elemente hinzugefügt. Jeder vom Tokenisierer emittierte Knoten wird vom Baumkonstruktor verarbeitet. Die Spezifikation definiert für jedes Token, welches DOM-Element für dieses Token relevant ist und für dieses Token erstellt wird. Das Element wird dem DOM-Baum und dem Stapel der offenen Elemente hinzugefügt. Mit diesem Stack werden Unstimmigkeiten bei Verschachtelungen und nicht geschlossene Tags korrigiert. Der Algorithmus wird auch als Zustandsmaschine beschrieben. Diese Zustände werden als „Einfügemodi“ bezeichnet.

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

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

Die Eingabe für die Phase des Baumaufbaus ist eine Sequenz von Tokens aus der Tokenisierungsphase. Der erste Modus ist der „Anfangsmodus“. Wenn das Token „html“ empfangen wird, wird der Modus in „vor html“ geändert und das Token in diesem Modus noch einmal verarbeitet. Dadurch wird das Element „HTMLHtmlElement“ erstellt, das dem Stammdokumentobjekt angehängt wird.

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

Es folgt der "in head"-Modus und dann der "after head"-Modus. Das Body-Token wird noch einmal verarbeitet, ein HTMLBodyElement wird erstellt und eingefügt und der Modus wird in "in body" geändert.

Die Zeichentokens des Strings „Hallo Welt“ werden jetzt empfangen. Das erste Zeichen führt zum Erstellen und Einfügen eines „Text“-Knotens und die anderen Zeichen werden an diesen Knoten angehängt.

Wenn das Endtoken für den Textkörper empfangen wird, wird der Modus nach dem Textkörper aktiviert. Jetzt erhalten wir das HTML-End-Tag, wodurch wir in den Modus „after after body“ wechseln. Wenn das Token für das Ende der Datei empfangen wird, wird das Parsen beendet.

Baumkonstruktion des HTML-Beispiels
Abbildung 11: Baumstruktur der Beispiel-HTML-Datei

Aktionen nach Abschluss des Parsings

In dieser Phase kennzeichnet der Browser das Dokument als interaktiv und beginnt mit dem Parsen von Scripts, die sich im Modus „Verzögert“ befinden, d. h., 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 Baumstruktur 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.

Nehmen wir als Beispiel diesen HTML-Code:

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

Ich habe wahrscheinlich gegen etwa eine Million Regeln verstoßen („mytag“ ist kein Standard-Tag, falsche Verschachtelung der „p“- und „div“-Elemente usw.), aber der Browser zeigt es trotzdem richtig an und meldet keine Fehler. Ein Großteil des Parsercodes dient also dazu, die Fehler des HTML-Autors zu korrigieren.

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

Einige dieser Anforderungen werden in der HTML5-Spezifikation definiert. (WebKit fasst dies im Kommentar am Anfang der HTML-Parser-Klasse gut zusammen.)

Der Parser analysiert die tokenisierte Eingabe im 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. Daher muss der Parser fehlertolerant sein.

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

  1. Das hinzugefügte Element ist innerhalb eines äußeren Tags ausdrücklich nicht zulässig. In diesem Fall sollten wir alle Tags bis zu dem schließen, das das Element verbietet, und es danach hinzufügen.
  2. Wir dürfen das Element nicht direkt hinzufügen. Möglicherweise hat die Person, die das Dokument erstellt hat, ein Tag dazwischen vergessen (oder das Tag dazwischen ist optional). 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 Block-Element innerhalb eines Inline-Elements hinzufügen. Schließen Sie alle Inline-Elemente bis zum nächsten übergeordneten Blockelement.
  4. Wenn das Problem dadurch nicht behoben wird, schließen Sie Elemente, bis wir das Element hinzufügen können, oder ignorieren Sie das Tag.

Hier einige Beispiele für die WebKit-Fehlertoleranz:

</br> statt <br>

Einige Websites verwenden </br> anstelle von <br>. Zur Kompatibilität mit IE und Firefox behandelt WebKit dies wie <br>.

Der Code:

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

Die Fehlerbehandlung ist intern und wird dem Nutzer nicht angezeigt.

Eine verlorene Tabelle

Eine verirrte Tabelle ist eine Tabelle in einer anderen Tabelle, aber nicht in 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 übergeordnete 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 den Inhalt des aktuellen Elements: Die innere Tabelle wird aus dem äußeren Tabellenstapel entfernt. Die Tabellen sind jetzt Geschwister.

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.

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

Nochmals: Der Kommentar spricht für sich.

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

Webentwickler aufgepasst: Außer Sie möchten als Beispiel in einem WebKit-Fehlertoleranz-Code-Snippet erscheinen, sollten Sie korrekt formatierten HTML-Code 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 Parsertypen geparst werden. Die CSS-Spezifikation definiert die lexikalische und syntaktische Grammatik von CSS.

Sehen wir uns einige Beispiele an:

Die lexikalische Grammatik (Wörterbuch) wird für jedes Token durch reguläre Ausdrücke 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 „Kennung“, z. B. einen Klassennamen. „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:

Eine Regelgruppe hat 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 dieser Regelgruppe angewendet werden. Diese Struktur wird in dieser Definition formal definiert:

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

Das bedeutet, dass eine Regel ein Selktor oder optional mehrere Selektoren sind, die durch Kommas 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 sich in der Einführung zu Parsern erinnern, 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. Jedes Objekt enthält CSS-Regeln. Die CSS-Regelobjekte enthalten Selektor- und Deklarationsobjekte sowie andere Objekte, die der CSS-Grammatik entsprechen.

CSS-Parsing
Abbildung 12: CSS-Parsing

Verarbeitungsreihenfolge für Skripts und Stylesheets

Skripts

Das Web ist ein synchrones Modell. 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 Skript ausgeführt wurde. Wenn das Script 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. Dieses Modell wurde viele Jahre lang verwendet und ist auch in den HTML4- und 5-Spezifikationen festgelegt. Autoren können einem Script das Attribut „defer“ hinzufügen. In diesem Fall wird das Dokument nicht angehalten und das Script wird nach dem Parsen des Dokuments ausgeführt. HTML5 bietet die Möglichkeit, das Script als asynchron zu kennzeichnen, damit es von einem anderen Thread geparst und ausgeführt wird.

Spekulatives Parsing

Sowohl WebKit als auch Firefox führen diese Optimierung durch. Beim Ausführen von Skripts parst ein anderer Thread den Rest des Dokuments und findet heraus, welche anderen Ressourcen aus dem Netzwerk geladen werden müssen, und lädt sie. Auf diese Weise können Ressourcen bei parallelen Verbindungen geladen werden und die Gesamtgeschwindigkeit wird verbessert. Hinweis: Der spekulative Parser analysiert nur Verweise auf externe Ressourcen wie externe Scripts, Stylesheets und Bilder. Er ändert den DOM-Baum nicht. Das bleibt dem Hauptparser überlassen.

Style sheets

Stylesheets haben dagegen ein anderes Modell. Da Stylesheets den DOM-Baum nicht verändern, scheint es zunächst keinen Grund zu geben, auf sie zu warten und das Parsen des Dokuments zu stoppen. Es gibt jedoch ein Problem mit Scripts, die während der Dokument-Parsing-Phase nach Stilinformationen fragen. Wenn der Stil noch nicht geladen und geparst wurde, erhält das Script falsche Antworten. Dies hat offenbar viele Probleme verursacht. Dies scheint ein Grenzfall zu sein, kommt aber recht häufig vor. Firefox blockiert alle Scripts, 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önnen.

Renderbaum erstellen

Während der DOM-Baumstruktur erstellt der Browser einen weiteren Baum, die Rendering-Struktur. Dieser Baum enthält visuelle Elemente in der Reihenfolge, in der sie angezeigt werden. Sie ist die visuelle Darstellung des Dokuments. Mit diesem Baum können die Inhalte in der richtigen Reihenfolge dargestellt werden.

In Firefox werden die Elemente im Renderbaum als „Frames“ bezeichnet. In WebKit wird der Begriff „Renderer“ oder „Renderobjekt“ verwendet.

Ein Renderer weiß, wie er sich selbst und seine untergeordneten Elemente ausrichten und zeichnen muss.

Die RenderObject-Klasse von WebKit, die Basisklasse der Renderer, hat 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 dem 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 vom Wert „display“ des Stilattributs beeinflusst, das für den Knoten relevant ist (siehe Abschnitt Stilberechnung). Mit dem folgenden WebKit-Code wird entschieden, welcher Renderer-Typ für einen DOM-Knoten gemäß dem Anzeigeattribut 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 Frames.

Wenn in WebKit ein Element einen speziellen Renderer erstellen möchte, wird die Methode createRenderer() überschrieben. Die Renderer verweisen auf Stilobjekte, die keine geometrischen Informationen enthalten.

Die Beziehung des Renderbaums zum DOM-Baum

Die Renderer entsprechen DOM-Elementen, die Beziehung ist jedoch nicht eins zu eins. Nicht sichtbare DOM-Elemente werden nicht in den Renderbaum eingefügt. Ein Beispiel hierfür ist das „head“-Element. Auch Elemente, deren Anzeigewert „nicht“ zugewiesen wurde, werden nicht im Baum angezeigt. Elemente mit der Sichtbarkeit „ausgeblendet“ werden dagegen im Baum angezeigt.

Es gibt DOM-Elemente, die mehreren visuellen Objekten entsprechen. Das sind in der Regel Elemente mit komplexer 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. Auch wenn Text in mehrere Zeilen aufgeteilt wird, weil die Breite nicht für eine Zeile ausreicht, werden die neuen Zeilen als zusätzliche Renderer hinzugefügt.

Ein weiteres Beispiel für mehrere Renderer ist fehlerhaftes HTML. 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 einzubetten.

Einige Rendering-Objekte entsprechen einem DOM-Knoten, aber nicht an derselben Stelle im Baum. Elemente, die „floaten“ und absolut positioniert sind, befinden sich nicht im Fluss, werden an einer anderen Stelle im Baum platziert und dem tatsächlichen Frame zugeordnet. Ein Platzhalter-Frame ist an der Stelle, an der sie hätten sein sollen.

Der Renderbaum und der entsprechende DOM-Baum.
Abbildung 13: Der Rendering-Baum und der entsprechende DOM-Baum. Der „Viewport“ ist der ursprüngliche enthaltende Block. In WebKit ist das das „RenderView“-Objekt.

Ablauf des Erstellens des Baums

In Firefox wird die Präsentation als Listener für DOM-Aktualisierungen 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 das Auflösen des Stils und Erstellen eines Renderers als „Attachment“ bezeichnet. Jeder DOM-Knoten hat eine "attach"-Methode. Das Anhängen ist 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 der Stamm des Renderbaums erstellt. Das Stamm-Renderobjekt entspricht dem sogenannten „enthaltenden Block“ in der CSS-Spezifikation: dem obersten Block, der alle anderen Blöcke enthält. Die Abmessungen sind der Darstellungsbereich: die Abmessungen des Anzeigebereichs des Browserfensters. In Firefox wird es als ViewPortFrame und in WebKit als RenderView bezeichnet. Das ist das Renderobjekt, auf das das Dokument verweist. Der Rest der Baumstruktur wird durch Einfügen von DOM-Knoten erstellt.

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

Stilberechnung

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

Der Stil enthält Stylesheets verschiedener Herkunft, Inline-Stilelemente und visuelle Eigenschaften in der HTML-Datei (z. B. die Eigenschaft „bgcolor“). Letztere werden in entsprechende CSS-Stileigenschaften übersetzt.

Die Style Sheets sind die Standard-Style Sheets des Browsers, die vom Seitenautor bereitgestellten Style Sheets und User Style Sheets. Dabei handelt es sich um Style Sheets, die vom Nutzer des Browsers bereitgestellt werden. In Browsern können Sie Ihre bevorzugten Stile festlegen. In Firefox legen Sie dazu beispielsweise ein Stylesheet im Ordner „Firefox-Profil“ ab.

Die Stilberechnung bringt einige Schwierigkeiten mit sich:

  1. Stildaten sind ein sehr großes Konstrukt, das die zahlreichen Stileigenschaften enthält. Dies kann zu Speicherproblemen führen.
  2. Wenn die Abgleichsregeln für jedes Element nicht optimiert sind, kann das zu Leistungsproblemen führen. Es ist sehr aufwendig, die gesamte Regelliste für jedes Element zu durchsuchen, um Übereinstimmungen zu finden. Auswählen können eine komplexe Struktur haben, die dazu führen kann, dass der Abgleichsprozess auf einem scheinbar vielversprechenden Pfad beginnt, der sich als nutzlos erweist und ein anderer Pfad ausprobiert werden muss.

    Beispiel: diese Kombinationsauswahl:

    div div div div{
    ...
    }
    

    Die Regeln gelten für ein <div>, das ein Abkömmling von drei Divs ist. Angenommen, Sie möchten prüfen, ob die Regel für ein bestimmtes <div>-Element gilt. Zur Überprüfung wählen Sie einen bestimmten Pfad in der Baumstruktur aus. Möglicherweise müssen Sie den Knotenbaum nach oben durchlaufen, nur um festzustellen, dass es nur zwei div-Elemente gibt und die Regel nicht zutrifft. Sie müssen dann andere Pfade im Baum ausprobieren.

  3. Die Anwendung der Regeln erfordert ziemlich 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 Mauszustand befinden (d. h., eines der Elemente darf sich nicht in :hover befinden, das andere nicht).
  2. Keines der Elemente sollte eine ID haben.
  3. Die Tag-Namen müssen übereinstimmen.
  4. Die Klassenattribute müssen übereinstimmen.
  5. Die zugeordneten Attribute müssen identisch sein.
  6. Die Status der Verknüpfungen müssen übereinstimmen.
  7. Die Fokuszustände müssen übereinstimmen.
  8. Keines der Elemente sollte von Attributselektoren betroffen sein. Als betroffen gilt, wenn ein Selektor mit einem Attributselektor an einer beliebigen Position innerhalb des Selektors übereinstimmt.
  9. Die Elemente dürfen kein Inline-Style-Attribut haben.
  10. Es dürfen keine Geschwisterselektoren verwendet werden. WebCore löst einfach einen globalen Wechsel aus, wenn ein gleichgeordneter Selektor gefunden wird, und deaktiviert die Stilfreigabe für das gesamte Dokument, sofern diese 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 entsprechenden Stil.

Firefox-Stilkontextbaum
Abbildung 14: Kontextbaum im Firefox-Stil

Die Stilkontexte enthalten Endwerte. Die Werte werden berechnet, indem alle Abgleichsregeln 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 des Regelbaums ist wirklich clever. So können diese Werte zwischen Knoten geteilt werden, um eine erneute Berechnung zu vermeiden. Außerdem sparen Sie Platz.

Alle übereinstimmenden Regeln werden in einem Baum gespeichert. Die unteren Knoten in einem Pfad haben eine höhere Priorität. Der Baum enthält alle Pfade für Regelübereinstimmungen, die gefunden wurden. Das Speichern der Regeln erfolgt verzögert. Der Baum wird nicht zu Beginn für jeden Knoten berechnet. Wenn jedoch ein Knotenstil berechnet werden muss, werden die berechneten Pfade dem Baum hinzugefügt.

Die Baumpfade sollen als Wörter in einem Lexikon betrachtet werden. Angenommen, wir haben diesen Regelbaum bereits berechnet:

Berechneter Regelbaum
Abbildung 15: Bereitgestellter 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 enthalten, 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 spart.

Aufteilung in Strukturen

Die Stilkontexte sind 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. Übernommene Eigenschaften sind Eigenschaften, die vom Element übernommen werden, sofern sie nicht vom übergeordneten Element definiert wurden. Für nicht übernommene Eigenschaften (sogenannte „Zurücksetzen“-Eigenschaften) werden Standardwerte verwendet, wenn sie nicht definiert sind.

Der Baum hilft uns, indem er ganze Strukturen (mit den berechneten Endwerten) im Baum zwischenspeichert. Wenn der unterste Knoten keine Definition für ein Strukturobjekt liefert, kann ein im Cache gespeichertes Strukturobjekt in einem übergeordneten Knoten verwendet werden.

Stilkontexte mithilfe des Regelbaums berechnen

Beim Berechnen des Stilkontexts für ein bestimmtes Element berechnen wir zuerst einen Pfad im Regelbaum oder verwenden einen vorhandenen. 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 Pfades – dem mit der höchsten Priorität (normalerweise der spezifischeste Selektor) – und durchlaufen den Baum, bis unser Strukturelement voll ist. Wenn es keine Spezifikation für die Struktur in diesem Regelknoten gibt, können wir eine erhebliche Optimierung vornehmen. Wir wandern den Baum nach oben, bis wir einen Knoten finden, der ihn vollständig angibt, und verweisen darauf. Das ist die beste Optimierung. Die gesamte Struktur wird gemeinsam verwendet. So werden die Berechnung von Endwerten und der Arbeitsspeicher gespart.

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

Wenn wir keine Definitionen für unseren Typ gefunden haben, verweisen wir im Kontextbaum auf den Typ des übergeordneten Elements, falls es sich um einen abgeleiteten Typ handelt. In diesem Fall konnten wir auch Strukturen teilen. Bei einer zurückgesetzten Struktur werden Standardwerte verwendet.

Wenn der spezifischeste Knoten Werte hinzufügt, müssen wir einige zusätzliche Berechnungen durchführen, um sie in tatsächliche Werte umzuwandeln. Das Ergebnis wird dann 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>

Außerdem gelten 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}

Zur Vereinfachung gehen wir davon aus, dass wir nur zwei Strukturen ausfüllen müssen: die Struktur „color“ und die Struktur „margin“. Die Farbstruktur enthält nur ein Element: die Farbe. Die Randstruktur enthält die vier Seiten.

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

Regelbaum
Abbildung 16: Regelbaum

Der Kontextbaum sieht dann so aus (Knotenname: Regelknoten, auf den verwiesen wird):

Kontextbaum
Abbildung 17: 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 Stilstrukturen füllen.

Wir vergleichen die Regeln und stellen fest, dass die Regeln 1, 2 und 6 für die <div> gelten. Das bedeutet, dass im Baum bereits ein Pfad vorhanden ist, den unser Element verwenden kann. Wir müssen ihm nur noch einen weiteren Knoten für Regel 6 (Knoten F im Regelbaum) hinzufügen.

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 Stilstrukturen ausfüllen. Wir beginnen mit dem Ausfüllen des Margin-Attributs. Da der letzte Regelknoten (F) nicht zum Margin-Attribut hinzugefügt wird, können wir den Baum nach oben durchgehen, bis wir ein im Cache gespeichertes Attribut finden, das bei einer vorherigen Knoteneinfügung berechnet wurde, und es verwenden. Wir finden sie bei Knoten B, dem obersten Knoten, für den Randregeln festgelegt wurden.

Wir haben eine Definition für die Farbstruktur, daher können wir keine im Cache gespeicherte Struktur verwenden. Da Farbe nur ein Attribut hat, müssen wir nicht den Baum nach oben durchgehen, um andere Attribute auszufüllen. Wir berechnen den Endwert (String in RGB konvertieren usw.) und speichern die berechnete Struktur in diesem Knoten im Cache.

Die Arbeit am zweiten <span>-Element ist noch einfacher. Wir vergleichen die Regeln und stellen fest, dass sie wie die vorherige Spanne auf Regel G verweist. Da wir Geschwister haben, die auf denselben Knoten verweisen, können wir den gesamten Stilkontext teilen und nur auf den Kontext der vorherigen Spanne verweisen.

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

Angenommen, wir haben Regeln für Schriftarten in einem Absatz hinzugefügt:

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

Dann könnte das Absatzelement, das im Kontextbaum ein untergeordnetes Element des div-Elements ist, dieselbe Schriftstruktur wie sein übergeordnetes Element haben. Das ist der Fall, wenn für den Absatz keine Schriftschnittregeln festgelegt wurden.

In WebKit, das kein Regelbaum hat, werden die übereinstimmenden Deklarationen viermal durchlaufen. Zuerst werden nicht wichtige Properties mit hoher Priorität angewendet (Properties, die zuerst angewendet werden sollten, weil andere davon abhängen, z. B. Displayanzeigen), dann wichtige Properties mit hoher Priorität, dann nicht wichtige und dann wichtige Regeln mit normaler Priorität. Das bedeutet, dass Properties, die mehrmals vorkommen, gemäß der richtigen Kaskadenabfolge aufgelöst werden. Der Letzte gewinnt.

Zusammenfassend lässt sich sagen: Wenn Sie die Stilobjekte (vollständig oder einige der darin enthaltenen Strukturen) teilen, werden Probleme 1 und 3 behoben. Das Firefox-Regelnetzwerk hilft auch dabei, die Properties in der richtigen Reihenfolge anzuwenden.

Regeln für eine einfache Übereinstimmung anpassen

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 den relevanten Stilregeln zugeordnet sind) html <p bgcolor="blue" /> Die letzten beiden können leicht mit dem Element abgeglichen werden, da es die Stilattribute hat und HTML-Attribute mit dem Element als Schlüssel zugeordnet werden können.

Wie bereits bei Problem 2 erwähnt, kann die Übereinstimmung mit CSS-Regeln etwas kniffliger sein. Um das Problem zu lösen, werden die Regeln für einen einfacheren Zugriff manipuliert.

Nach dem Parsen des Stylesheets werden die Regeln je nach Auswahl einer von mehreren Hash-Maps hinzugefügt. Es gibt Karten nach ID, nach Klassennamen, nach Tagnamen und eine allgemeine Karte für alles, was nicht in diese Kategorien fällt. Handelt es sich bei dem Selektor um eine ID, wird die Regel zur ID-Zuordnung hinzugefügt, wenn es sich um eine Klasse handelt, wird sie der Klassen-Map hinzugefügt usw.

Diese Bearbeitung erleichtert die Zuordnung von Regeln erheblich. Es ist nicht nötig, in jeder Deklaration nachzusehen: Wir können die relevanten Regeln für ein Element aus den Zuordnungen extrahieren. Durch diese Optimierung werden über 95% der Regeln eliminiert, sodass sie beim Abgleich(4.1) nicht berücksichtigt werden müssen.

Sehen wir uns beispielsweise die folgenden Stilregeln an:

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

Die erste Regel wird in die Klassenzuordnung 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 Element „p“ zu finden. Die Klassenzuordnung enthält den Schlüssel „error“, unter dem die Regel für „p.error“ gefunden wird. Das div-Element enthält relevante Regeln in der ID-Zuordnung (Schlüssel ist die ID) und in der Tag-Zuordnung. Jetzt müssen Sie nur noch herausfinden, welche der Regeln, die anhand der Schlüssel extrahiert wurden, wirklich übereinstimmen.

Angenommen, die Regel für das DIV lautet:

table div {margin: 5px}

Es wird weiterhin aus der Tag-Map extrahiert, da der Schlüssel der rechtseste Selektor ist, aber es würde nicht mit unserem Div-Element übereinstimmen, das keinen übergeordneten Tabellenknoten hat.

Sowohl WebKit als auch Firefox führen diese Manipulation durch.

Cascading Style Sheet-Reihenfolge

Das Stilobjekt verfügt über Eigenschaften, die jedem visuellen Attribut entsprechen (allen CSS-Attributen, aber allgemeiner). Wenn die Eigenschaft von keinem der übereinstimmenden Regeln definiert ist, können einige Eigenschaften vom Stilobjekt des übergeordneten Elements übernommen werden. Andere Eigenschaften haben Standardwerte.

Das Problem beginnt, wenn es mehr als eine Definition gibt. Hier kommt die Kaskadenabfolge zur Lösung des Problems.

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

  1. Browserdeklarationen
  2. Normale Nutzerdeklarationen
  3. Normale Deklarationen von Autoren
  4. Wichtige Erklärungen des Autors
  5. Wichtige Nutzerdeklarationen

Die Browserdeklarationen sind am wenigsten wichtig und der Nutzer überschreibt die Angaben des Autors nur, wenn die Deklaration als wichtig gekennzeichnet wurde. Deklarationen mit derselben Reihenfolge werden nach Spezifität und dann nach der Reihenfolge ihrer Angabe sortiert. Die visuellen HTML-Attribute werden in übereinstimmende CSS-Deklarationen umgewandelt . Sie werden als Autorregeln mit niedriger Priorität behandelt.

Spezifität

Die Spezifität des Selektors wird in der CSS2-Spezifikation so definiert:

  1. „1“, wenn die Deklaration ein „style“-Attribut und keine Regel mit einem Selektor ist, andernfalls „0“ (= a)
  2. Anzahl der ID-Attribute im Selektor zählen (= b)
  3. Anzahl der anderen Attribute und Pseudoklassen in der Auswahl (= c)
  4. Anzahl der Elementnamen und Pseudoelemente im Selektor (= d)

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

Die Zahlenbasis, die Sie verwenden müssen, wird durch die höchste Anzahl in einer der Kategorien definiert.

Wenn a beispielsweise 14 ist, können Sie die Hexadezimalbasis verwenden. Für den unwahrscheinlichen Fall, dass a=17 ist, benötigen Sie eine 17-stellige Zahlenbasis. Letzteres kann mit einem solchen Selector passieren: html body div div p… (17 Tags in Ihrem Selector… 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

Nach der Übereinstimmung werden die Regeln gemäß den Kaskadenregeln sortiert. WebKit verwendet die Bubble-Sortierung für kleine Listen und die Zusammenführungssortierung 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;
}

Gradueller Prozess

WebKit verwendet ein Flag, das anzeigt, 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 das Dokument wird entsprechend gekennzeichnet. Die Platzhalter 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 Durchlauf 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: Für HTML-Tabellen ist beispielsweise möglicherweise mehr als ein Pass erforderlich.

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

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

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.

Dirty-Bit-System

Um nicht für jede kleine Änderung ein komplettes Layout vornehmen zu müssen, verwenden Browser ein sogenanntes „Dirty Bit“-System. Ein Renderer, der geändert oder hinzugefügt wird, markiert sich selbst und seine untergeordneten Elemente als „dirty“ (schmutzig: Layout erforderlich).

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

Globales und inkrementelles Layout

Das Layout kann für den gesamten Renderbaum ausgelöst werden – dies ist das „globale“ Layout. Das kann folgende Ursachen haben:

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

Das Layout kann inkrementell erfolgen, d. h., nur die Renderer, die geändert wurden, werden neu angeordnet. Dies kann zu Schäden führen, die zusätzliche Layouts erfordern.

Das inkrementelle Layout wird (asynchron) ausgelöst, wenn Renderer nicht mehr aktuell sind. Das ist beispielsweise der Fall, wenn dem Renderbaum neue Renderer hinzugefügt werden, nachdem zusätzliche Inhalte aus dem Netzwerk stammen und dem DOM-Baum hinzugefügt wurden.

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

Asynchrones und synchrones Layout

Das inkrementelle Layout wird asynchron ausgeführt. Firefox stellt „Neuformatierungsbefehle“ für inkrementelle Layouts in die Warteschlange und ein Scheduler löst die Batchausführung dieser Befehle aus. WebKit hat auch einen Timer, der ein inkrementelles Layout ausführt. Der Baum wird durchlaufen und „schmutzige“ Renderer werden neu layoutet.

Scripts, die nach Stilinformationen wie „offsetHeight“ fragen, können ein inkrementelles Layout synchron auslösen.

Das globale Layout wird in der Regel synchron ausgelöst.

Manchmal wird das Layout als Callback nach einem anfänglichen Layout ausgelöst, weil sich einige Attribute, z. B. die Scrollposition, geändert haben.

Optimierungen

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

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

Der Layoutprozess

Das Layout weist normalerweise das folgende Muster auf:

  1. Der übergeordnete Renderer bestimmt seine eigene Breite.
  2. Das übergeordnete Element enthält untergeordnete Elemente und:
    1. Platzieren Sie den untergeordneten Renderer (festlegen von x und y).
    2. Ruft bei Bedarf das untergeordnete Layout auf, wenn es ungültig ist oder sich in einem globalen Layout befindet oder aus einem anderen Grund, wodurch die Höhe des untergeordneten Elements berechnet wird.
  3. Das übergeordnete Element verwendet die Gesamthöhe der untergeordneten Elemente und 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 schmutzige Bit auf „false“.

Firefox verwendet ein „Status“-Objekt (nsHTMLReflowState) als Parameter für das Layout (sogenannter „Reflow“). Der Status enthält 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, des Stilattributs „width“ des Renderers sowie der Ränder und Rahmen berechnet.

Beispielsweise die Breite des folgenden Div-Elements:

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

Wird von WebKit so berechnet (Klasse RenderBox, Methode calcWidth):

  • Die Containerbreite ist das Maximum aus „availableWidth“ und 0. In diesem Fall ist „availableWidth“ die „contentWidth“, die so berechnet wird:
clientWidth() - paddingLeft() - paddingRight()

clientWidth und clientHeight geben die Innenabmessungen eines Objekts an, ohne Rahmen und Bildlaufleiste.

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

  • Die horizontalen Rahmen und Abstände werden jetzt hinzugefügt.

Bisher ging es um die Berechnung der „bevorzugten Breite“. Nun werden die minimale und maximale Breite berechnet.

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

Die Werte werden im Cache gespeichert, falls ein Layout benötigt wird, sich die Breite aber nicht ändert.

Zeilenumbrüche

Wenn ein Renderer in der Mitte eines Layouts feststellt, dass ein Seitenumbruch erforderlich ist, stoppt er und überträgt die Information an das übergeordnete Element des Layouts. Das übergeordnete Element erstellt die zusätzlichen Renderer und ruft für sie Layout auf.

Malerei

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

Global und inkrementell

Wie das Layout kann auch das Painting global, d. h. die gesamte Struktur dargestellt, oder inkrementell sein. Bei der inkrementellen Darstellung ändern sich einige der Renderer so, dass sich das nicht auf den gesamten Baum auswirkt. Durch den geänderten Renderer wird sein Rechteck auf dem Bildschirm ungültig. Das Betriebssystem erkennt dies als „unbeschriebene Region“ und generiert ein „Paint“-Ereignis. Das Betriebssystem macht das auf clevere Weise und verschmilzt mehrere Regionen zu einer. In Chrome ist es komplizierter, da sich der Renderer in einem anderen Prozess als der Hauptprozess befindet. Chrome simuliert das Betriebssystemverhalten in gewissem Maße. Die Präsentation überwacht diese Ereignisse und delegiert die Nachricht an den Render-Stamm. Der Baum wird durchlaufen, bis der entsprechende Renderer erreicht wird. Es wird neu gemalt (und in der Regel auch seine untergeordneten Elemente).

Painting-Reihenfolge

CSS2 definiert die Reihenfolge des Malvorgangs. Das ist die Reihenfolge, in der die Elemente in den Stapelungskontexten gestapelt werden. Diese Reihenfolge wirkt sich auf das Malen aus, da die Stapel von hinten nach vorne gemalt werden. Die Stapelreihenfolge eines Block-Renderers ist:

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

Firefox-Anzeigeliste

Firefox durchsucht den Renderbaum und erstellt eine Displayliste für das gemalte Rechteck. Es enthält die für das Rechteck relevanten Renderer in der richtigen Malreihenfolge (Hintergründe der Renderer, dann Rahmen usw.).

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

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

WebKit-Rechteckspeicher

Vor der Neumalung speichert WebKit das alte Rechteck als Bitmap. Es wird dann nur das Delta zwischen dem neuen und dem alten Rechteck dargestellt.

Dynamische Änderungen

Die Browser versuchen, auf eine Änderung so wenig wie möglich zu reagieren. Änderungen an der Farbe eines Elements führen also nur dazu, dass das Element neu gemalt wird. Bei Änderungen an der Position des Elements werden das Layout und die Darstellung des Elements, seiner untergeordneten Elemente und möglicherweise gleichgeordneter Elemente aktualisiert. Wenn Sie einen DOM-Knoten hinzufügen, wird das Layout des Knotens neu erstellt und neu gerendert. Größere Änderungen, z. B. die Erhöhung der Schriftgröße des „html“-Elements, führen dazu, dass Caches ungültig werden, das Layout neu erstellt und der gesamte Baum neu gerendert wird.

Die Threads der Rendering-Engine

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

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

Ereignisschleife

Der Hauptthread des Browsers ist eine Ereignisschleife. Es ist eine Endlosschleife, die den Prozess am Leben erhält. Es wartet auf Ereignisse wie Layout- und Paint-Ereignisse und verarbeitet sie. Dies ist der Firefox-Code für die Hauptschleife:

while (!mExiting)
    NS_ProcessNextEvent(thread);

Visuelles CSS2-Modell

Canvas

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

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

Gemäß www.w3.org/TR/CSS2/zindex.html ist das Canvas transparent, wenn es sich in einem anderen Canvas befindet, und hat eine vom Browser definierte Farbe, wenn dies nicht der Fall ist.

CSS-Box-Modell

Das CSS-Feld-Modell beschreibt die rechteckigen Felder, die für Elemente in der Dokumentstruktur generiert und nach dem visuellen Formatierungsmodell dargestellt werden.

Jedes Feld hat einen Inhaltsbereich (z. B. Text oder ein Bild) und optional umgebende Abstände, Rahmen und Randbereiche.

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

Jeder Knoten generiert 0…n solche Felder.

Alle Elemente haben das Attribut „display“, das den Typ des Felds bestimmt, das generiert wird.

Beispiele:

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

Standardmäßig ist „inline“ festgelegt, aber das Browser-Stylesheet kann andere Standardeinstellungen festlegen. Beispiel: Die Standardanzeige für das „div“-Element 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 an seiner Position im Dokument platziert. Das bedeutet, dass sein Platz in der Rendering-Struktur genau wie sein Platz im DOM-Baum ist und entsprechend seinem Boxtyp und den Abmessungen dargestellt wird.
  2. „Float“ (Schweben): Das Objekt wird zuerst wie beim normalen Fluss angeordnet und dann so weit wie möglich nach links oder rechts verschoben.
  3. Absolut: Das Objekt wird in der Rendering-Struktur an einer anderen Stelle als im DOM-Baum platziert

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

  • „static“ und „relative“ führen zu einem normalen Ablauf.
  • „absolute“ und „fixed“ führen zu einer absoluten Positionierung.

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

Die Anordnung des Felds wird durch Folgendes bestimmt:

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

Boxtypen

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

Blockfeld
Abbildung 20: Block-Box

Inline-Box: Hat keinen eigenen Block, sondern befindet sich in einem enthaltenden Block.

Inline-Felder
Abbildung 21: Inline-Boxen

Die Blöcke werden vertikal nacheinander formatiert. Inline-Elemente werden horizontal formatiert.

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

Inline-Boxen werden in Linien oder „Zeilen-Boxen“ eingefügt. Die Linien sind mindestens so hoch wie das höchste Feld, können aber auch höher sein, wenn die Felder „an der Basis“ ausgerichtet sind. Das bedeutet, dass der untere Teil eines Elements an einem Punkt eines anderen Felds ausgerichtet ist, der nicht der untere Rand ist. Wenn die Containerbreite nicht ausreicht, werden die Inline-Elemente auf mehrere Zeilen verteilt. Das ist in der Regel in einem Absatz der Fall.

Linien.
Abbildung 23: Linien

Positionierung

Verwandter

Relative Positionierung: Die Elemente werden wie gewohnt positioniert und dann um das erforderliche Delta verschoben.

Relative Positionierung.
Abbildung 24: Relative Positionierung

Float

Ein Floating-Box wird nach links oder rechts von einer Zeile verschoben. Das Interessante ist, dass die anderen Felder darum herumfließen. Der HTML-Code:

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

Sie sieht so aus:

Gleitkommazahl.
Abbildung 25: Float

Absolut und fix

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

Feste Positionierung.
Abbildung 26: Feste Positionierung

Schichtenbasierte Darstellung

Dies wird durch die CSS-Eigenschaft „Z-Index“ festgelegt. Er stellt die dritte Dimension des Felds dar: seine Position entlang der „Z‑Achse“.

Die Boxen sind in Stapel unterteilt (sogenannte Stapelkontexte). In jedem Stapel werden zuerst die hinteren Elemente und darauf die vorderen Elemente gezeichnet, die näher am Nutzenden sind. Bei Überschneidungen wird das vorherige Element vom vorderen Element verdeckt.

Die Stapel werden entsprechend der z-index-Eigenschaft geordnet. Felder 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 dann so aus:

Feste Positionierung.
Abbildung 27: Feste Positionierung

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

Ressourcen

  1. Browserarchitektur

    1. Grosskurth, Alan. Referenzarchitektur für Webbrowser (pdf)
    2. Gupta, Vineet. Funktionsweise von Browsern – Teil 1: Architektur
  2. Parsen

    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (auch als „Dragon Book“ bekannt), Addison-Wesley, 1986
    2. Rick Jelliffe. The Bold and the Beautiful: zwei neue Entwürfe für HTML 5.
  3. Firefox

    1. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers
    2. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers (Google tech talk video)
    3. L. David Baron, Layout Engine von Mozilla
    4. L. David Baron, Dokumentation zu Mozilla Style System
    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) – Spezifikation
  6. Erstellungsanleitung für Browser

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

Übersetzungen

Diese Seite wurde ins Japanische übersetzt - zweimal:

Sie können sich die extern gehosteten Übersetzungen von Koreanisch und Türkisch ansehen.

Vielen Dank an alle!