Funktionsweise von Browsern

Hinter den Kulissen moderner Webbrowser

Paul Irish
Tali Garsiel
Tali Garsiel

Vorwort

Diese umfassende Einführung zu den internen Abläufen von WebKit und Gecko ist das Ergebnis umfangreicher Forschungen der israelischen Entwicklerin Tali Garsiel. Über ein paar Jahre hinweg überprüfte sie alle veröffentlichten Daten zu Browser-Interna und las viel Zeit mit dem Lesen des Quellcodes von Webbrowsern. Sie schrieb:

Als Webentwickler können Sie mit den internen Aspekten von Browservorgängen vertraut sein, um bessere Entscheidungen zu treffen und die Gründe für Best Practices bei der Entwicklung zu verstehen. Obwohl dies ein ziemlich langwieriges Dokument ist, empfehlen wir Ihnen, sich etwas Zeit damit zu beschäftigen. Sie werden froh sein, dass Sie es getan haben.

Paul Irish, Chrome Developer Relations

Einleitung

Webbrowser sind die am häufigsten verwendete Software. In diesem Leitfaden erkläre ich, wie sie hinter den Kulissen funktionieren. Wir werden sehen, was passiert, wenn Sie so lange google.com in die Adressleiste eingeben, bis die Google-Seite auf dem Browserbildschirm angezeigt wird.

Browser, über die wir sprechen

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

Die Hauptfunktion des Browsers

Die Hauptfunktion eines Browsers besteht darin, die gewünschte Webressource zu präsentieren, indem er sie vom Server abruft und im Browserfenster anzeigt. Die Ressource ist normalerweise ein HTML-Dokument, es kann sich jedoch auch um eine PDF-Datei, ein Bild oder eine andere Art von Inhalt handeln. Der Standort der Ressource wird vom Nutzer mithilfe eines URI (Uniform Resource Identifier) angegeben.

Die Art und Weise, wie der Browser HTML-Dateien interpretiert und anzeigt, ist in den HTML- und CSS-Spezifikationen festgelegt. Diese Spezifikationen werden vom World Wide Web Consortium W3C eingehalten, der Normungsorganisation für das Web. Jahrelang erfüllten Browser nur einen Teil der Spezifikationen und entwickelten ihre eigenen Erweiterungen. Dies führte zu ernsthaften Kompatibilitätsproblemen für Webautoren. Heute entsprechen die meisten Browser mehr oder weniger den Spezifikationen.

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

  1. Adressleiste zum Einfügen eines URI
  2. Zurück- und Vorwärts-Schaltflächen
  3. Lesezeichenoptionen
  4. Schaltflächen zum Aktualisieren und Stoppen, um das Laden aktueller Dokumente zu aktualisieren oder zu beenden
  5. Schaltfläche für die Startseite, über die du zur Startseite gelangst

Seltsamerweise ist die Benutzeroberfläche eines Browsers in keiner formalen Spezifikation definiert. Sie ist lediglich auf bewährte Methoden zurückzuführen, die sich im Laufe der Jahre und durch gegenseitige Imitation von Browsern entwickelt haben. In der HTML5-Spezifikation werden keine UI-Elemente definiert, die ein Browser haben muss. Es werden aber einige gängige Elemente aufgeführt. Dazu gehören die Adressleiste, die Statusleiste und die Symbolleiste. Natürlich gibt es auch spezielle Funktionen für einen bestimmten Browser, wie den Download-Manager von Firefox.

Übergeordnete Infrastruktur

Zu den Hauptkomponenten des Browsers gehören:

  1. Benutzeroberfläche: Hierzu gehören die Adressleiste, die Schaltfläche „Zurück“ und „Weiter“, das Lesezeichenmenü usw. Alle Bereiche der Browseranzeige mit Ausnahme des Fensters, in dem die angeforderte Seite angezeigt wird.
  2. Browser-Engine: Marshals-Aktionen zwischen der Benutzeroberfläche und dem Rendering-Modul.
  3. Das Rendering-Modul: zuständig für die Anzeige angeforderter Inhalte Handelt es sich bei dem angeforderten Inhalt beispielsweise um HTML, parst das Rendering-Modul 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 verschiedene Plattformen hinter einer plattformunabhängigen Schnittstelle.
  5. UI-Back-End: Wird zum Zeichnen grundlegender Widgets wie Kombinationsfelder 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 Persistenzebene. Der Browser muss alle Arten von Daten, wie etwa Cookies, lokal speichern. Browser unterstützen auch Speichermechanismen wie localStorage, IndexedDB, WebSQL und FileSystem.
Browserkomponenten
Abbildung 1: Browserkomponenten

In Browsern wie Chrome werden mehrere Instanzen des Rendering-Moduls ausgeführt: eine pro Tab. Jeder Tab wird in einem separaten Prozess ausgeführt.

Rendering-Engines

Die Verantwortung des Rendering-Moduls ist nun ja... das Rendern, also die Anzeige der angeforderten Inhalte auf dem Browserbildschirm.

Das Rendering-Modul kann standardmäßig HTML- und XML-Dokumente sowie Bilder anzeigen. Über Plug-ins oder Erweiterungen können andere Datentypen angezeigt werden, zum Beispiel PDF-Dokumente mithilfe eines PDF-Viewer-Plug-ins. In diesem Kapitel konzentrieren wir uns jedoch auf den Hauptanwendungsfall: das Anzeigen von HTML und Bildern, die mit CSS formatiert sind.

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

WebKit ist ein Open-Source-Renderingmodul, das zuerst als Engine für die Linux-Plattform entwickelt und von Apple modifiziert wurde, um Mac und Windows zu unterstützen.

Der Hauptablauf

Das Rendering-Modul beginnt mit dem Abrufen der Inhalte des angeforderten Dokuments aus der Netzwerkebene. Dies geschieht normalerweise in Blöcken von 8 KB.

Danach sieht der grundlegende Ablauf des Rendering-Moduls so aus:

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

Das Rendering-Modul beginnt mit dem Parsen des HTML-Dokuments und konvertiert die Elemente in DOM-Knoten in einer Baumstruktur, die als "Inhaltsbaum" bezeichnet wird. Die Suchmaschine parst die Stildaten sowohl in externen CSS-Dateien als auch in Stilelementen. Der Stil von Informationen wird zusammen mit visuellen Anweisungen im HTML-Code verwendet, um eine weitere Struktur zu erstellen: die Rendering-Struktur.

Die Rendering-Struktur enthält Rechtecke mit visuellen Attributen wie Farbe und Abmessungen. Die Rechtecke befinden sich in der richtigen Reihenfolge, um auf dem Bildschirm angezeigt zu werden.

Nach der Konstruktion der Rendering-Baumstruktur durchläuft dieser einen Layoutprozess. Dies bedeutet, dass jedem Knoten die genauen Koordinaten zugewiesen werden, an denen er auf dem Bildschirm erscheinen soll. Die nächste Phase ist das Painieren. Die Rendering-Baumstruktur wird durchlaufen und jeder Knoten wird mithilfe der UI-Back-End-Ebene dargestellt.

Es ist wichtig zu verstehen, dass dies ein schrittweiser Prozess ist. Für eine bessere Nutzererfahrung versucht das Rendering-Modul, Inhalte so schnell wie möglich auf dem Bildschirm anzuzeigen. Es wird nicht gewartet, bis der gesamte HTML-Code geparst wurde, bevor er mit dem Erstellen und Layout der Rendering-Baumstruktur beginnt. Teile des Inhalts werden geparst und angezeigt, während der Prozess mit den übrigen Inhalten, die aus dem Netzwerk kommen, fortgesetzt wird.

Beispiele für den Hauptablauf

WebKit-Hauptablauf
Abbildung 3: Hauptablauf bei WebKit
Hauptablauf: Gecko-Rendering-Engine von Mozilla.
Abbildung 4: Hauptablauf des Rendering-Moduls Gecko in Mozilla

Anhand der Abbildungen 3 und 4 können Sie erkennen, dass WebKit und Gecko zwar etwas unterschiedliche Terminologie verwenden, der Ablauf jedoch im Grunde derselbe ist.

Bei Gecko wird die Baumstruktur der visuell formatierten Elemente „Frame Tree“ genannt. Jedes Element ist ein Frame. WebKit verwendet den Begriff "Render Tree" und besteht aus "Render Objects". WebKit verwendet den Begriff "Layout" für die Platzierung von Elementen, während Gecko dies "Reflow" nennt. "Attachment" ist der Begriff von WebKit für die Verbindung von DOM-Knoten und visuellen Informationen zum Erstellen der Rendering-Baumstruktur. Ein kleiner nicht semantischer Unterschied besteht darin, dass Gecko zwischen dem HTML-Code und dem DOM-Baum eine zusätzliche Ebene hat. Sie wird als „Content Sink“ bezeichnet und dient der Herstellung von DOM-Elementen. Wir werden über jeden Teil des Ablaufs sprechen:

Parsing – allgemein

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

Beim Parsen eines Dokuments wird es in eine Struktur übersetzt, die der Code verwenden kann. Das Ergebnis des Parsings ist normalerweise eine Knotenstruktur, die die Struktur des Dokuments darstellt. Dies wird als Analyse- oder Syntaxbaum bezeichnet.

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

Baumknoten des mathematischen Ausdrucks.
Abbildung 5: Knotenbaum des mathematischen Ausdrucks

Grammatik

Das Parsing basiert auf den Syntaxregeln, die das Dokument befolgt: die Sprache bzw. das Format, in der bzw. dem es geschrieben wurde. Jedes Format, das geparst werden kann, muss über eine deterministische Grammatik verfügen, die aus Vokabular und Syntaxregeln besteht. Sie wird als kontextfreie Grammatik bezeichnet. Menschliche Sprachen sind keine solchen Sprachen und können daher nicht mit herkömmlichen Parsing-Techniken geparst werden.

Parser-Lexer-Kombination

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

Bei der lexikalen Analyse wird die Eingabe in Tokens aufgeteilt. Tokens sind das Vokabular der Sprache: die Sammlung gültiger Bausteine. In der menschlichen Sprache besteht es aus allen Wörtern, die im Wörterbuch dieser Sprache enthalten sind.

Bei der Syntaxanalyse werden die Syntaxregeln der Sprache angewendet.

Parser teilen die Arbeit in der Regel auf zwei Komponenten auf: den Lexer (manchmal auch Tokenizer genannt), der dafür verantwortlich ist, die Eingabe in gültige Tokens aufzuteilen, und den Parser, der für die Erstellung des Parsing-Baums verantwortlich ist. Dazu wird die Dokumentstruktur gemäß den Syntaxregeln der Sprache analysiert.

Er weiß, wie er irrelevante Zeichen wie Leerzeichen und Zeilenumbrüche entfernt.

Vom Quelldokument zur Analysestruktur
Abbildung 6: Vom Quelldokument zur Parsing-Struktur der Bäume

Der Parsing-Prozess ist iterativ. Der Parser fordert vom Lexer normalerweise ein neues Token an und versucht, das Token einer der Syntaxregeln zuzuordnen. Wenn eine Regel übereinstimmt, wird dem Parser ein Knoten hinzugefügt, der dem Token entspricht. Der Parser fordert dann ein anderes Token an.

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

Übersetzung

In vielen Fällen ist der Parsing-Baum nicht das Endprodukt. Parsing wird häufig für die Übersetzung verwendet, also die Umwandlung des Eingabedokuments in ein anderes Format. Ein Beispiel ist die Kompilierung. Der Compiler, der Quellcode in Maschinencode kompiliert, parst ihn zuerst in einen Parsing-Baum und übersetzt ihn dann in ein Maschinencodedokument.

Kompilierungsablauf
Abbildung 7: Kompilierungsablauf

Parsing-Beispiel

In Abbildung 5 haben wir einen Parsing-Baum auf der Grundlage eines mathematischen Ausdrucks erstellt. Versuchen wir, eine einfache mathematische Sprache zu definieren und sehen uns den Analyseprozess an.

Syntax:

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

Analysieren wir die Eingabe-2 + 3 - 1.

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 Term gefolgt von einer Operation gefolgt von einem weiteren Term. Die nächste Übereinstimmung erfolgt erst am Ende der Eingabe. 2 + 3 - 1 ist ein Ausdruck, da wir bereits wissen, dass 2 + 3 ein Term ist. Daher haben wir einen Term gefolgt von einer Operation gefolgt von einem weiteren Term. 2 + + stimmt mit keiner Regel überein und ist daher keine gültige Eingabe.

Formale Definitionen für Vokabular und Syntax

Vokabular wird normalerweise 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.

Syntax wird normalerweise im Format BNF definiert. Unsere Sprache ist so definiert:

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

Wie bereits erwähnt, kann eine Sprache von regulären Parsern geparst werden, wenn es sich bei ihrer Grammatik um eine kontextfreie Grammatik handelt. Eine intuitive Definition einer kontextfreien Grammatik ist eine Grammatik, die vollständig in BNF ausgedrückt werden kann. Eine formale Definition findest du im Wikipedia-Artikel zur kontextfreien Grammatik.

Parsertypen

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

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

Der Top-down-Parser beginnt mit der Regel auf oberster Ebene: Er identifiziert 2 + 3 als Ausdruck. Anschließend wird 2 + 3 - 1 als Ausdruck identifiziert. Die Identifizierung des Ausdrucks entwickelt sich entsprechend den anderen Regeln, aber der Startpunkt ist die Regel auf oberster Ebene.

Der Bottom-up-Parser scannt die Eingabe, bis eine Regel übereinstimmt. Dann wird die übereinstimmende Eingabe durch die Regel ersetzt. Dieser Vorgang wird bis zum Ende der Eingabe fortgesetzt. Der teilweise übereinstimmende Ausdruck wird im Stapel des Parsers platziert.

Stack Eingabe
2 + 3 – 1
term + 3 bis 1
Begriffsoperation 3–1
expression – 1
Ausdrucksvorgang 1
expression -

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

Parser automatisch generieren

Es gibt Tools, die einen Parser generieren können. Sie geben ihnen die Grammatik Ihrer Sprache – das Vokabular und die Syntaxregeln – und sie generieren einen funktionierenden Parser. Das Erstellen eines Parsers erfordert tiefgreifende Kenntnisse des Parsings und es ist nicht einfach, manuell einen optimierten Parser zu erstellen. Daher können Parser-Generatoren sehr nützlich sein.

WebKit verwendet zwei bekannte Parsergeneratoren: Flex zum Erstellen eines Lexers und Bison zum Erstellen eines Parsers (möglicherweise stoßen Sie auf die 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 bezieht sich auf die Syntaxregeln der Sprache im BNF-Format.

HTML-Parser

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

HTML-Grammatik

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

Wie wir in der Einführung zum Parsing gesehen haben, kann die Grammatiksyntax formell mithilfe von Formaten wie BNF definiert werden.

Leider gelten nicht alle konventionellen Parserthemen für 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 formelles Format zur Definition von HTML - DTD (Document Type Definition) - es ist jedoch keine kontextfreie Grammatik.

Das sieht auf den ersten Blick seltsam aus. HTML ist ziemlich nah an XML. Es sind viele XML-Parser verfügbar. Es gibt eine XML-Variante von HTML: XHTML. Was ist also der große Unterschied?

Der Unterschied besteht darin, dass der HTML-Ansatz "nachsichtiger" ist: Hiermit können Sie bestimmte Tags weglassen, die dann implizit hinzugefügt werden, oder manchmal Start- oder End-Tags und so weiter. Insgesamt ist es eine "weiche" Syntax, im Gegensatz zur steifen und anspruchsvollen XML-Syntax.

Dieses scheinbar kleine Detail macht einen riesigen Unterschied. Einerseits ist dies der Hauptgrund für die Beliebtheit von HTML: HTML verzeiht Ihnen Fehler und macht dem Webautor das Leben leichter. Andererseits ist es schwierig, eine formale Grammatik zu schreiben. Zusammenfassend lässt sich also sagen, dass HTML von herkömmlichen Parsern nicht einfach geparst werden kann, da seine Grammatik nicht kontextfrei ist. HTML kann nicht von XML-Parsern geparst werden.

HTML-DTD

Die HTML-Definition erfolgt in einem 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 die Hierarchie. Wie wir bereits gesehen haben, bildet die HTML-DTD keine kontextfreie Grammatik.

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

DOM

Die Ausgabestruktur (der "Parsing-Baum") ist eine Baumstruktur aus DOM-Element- und Attributknoten. DOM steht für Document Object Model. Es ist die Objektdarstellung des HTML-Dokuments und die Schnittstelle von HTML-Elementen zur Außenwelt wie JavaScript.

Stamm der Baumstruktur ist das Objekt Document.

Das DOM steht in etwa 1:1-Beziehung zur Auszeichnung. Beispiel:

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

Dieses Markup würde in die folgende DOM-Baumstruktur übersetzt werden:

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

Wie HTML wird DOM von der W3C-Organisation definiert. Siehe 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.

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

Der Parsing-Algorithmus

Wie in den vorherigen Abschnitten erwähnt, kann HTML nicht mit den regulären Top-down- oder Bottom-up-Parsern geparst werden.

Hierfür gibt es folgende Gründe:

  1. Der nachsichtige Charakter der Sprache.
  2. Die Tatsache, dass Browser über eine traditionelle Fehlertoleranz verfügen, um bekannte Fälle von ungültigem HTML-Code zu unterstützen.
  3. Der Parsing-Prozess wird erneut gestartet. Bei anderen Sprachen ändert sich die Quelle beim Parsen nicht. In HTML kann jedoch durch dynamischen Code (z. B. Skriptelemente, die document.write()-Aufrufe enthalten) zusätzliche Tokens hinzugefügt werden, sodass die Eingabe durch den Parsing-Prozess geändert wird.

Da Browser keine herkömmlichen Parsing-Techniken verwenden können, erstellen sie benutzerdefinierte Parser zum Parsen von HTML.

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

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

Der Tokenizer erkennt das Token, gibt es dem Baumkonstruktor weiter, verwendet das nächste Zeichen zum Erkennen des nächsten Tokens und so weiter bis zum Ende der Eingabe.

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

Der Tokenisierungsalgorithmus

Der Algorithmus gibt ein HTML-Token aus. Der Algorithmus wird als Zustandsautomat ausgedrückt. Jeder Zustand verarbeitet ein oder mehrere Zeichen des Eingabestreams und aktualisiert den nächsten Zustand entsprechend diesen Zeichen. Die Entscheidung wird vom aktuellen Tokenisierungsstatus und vom Zustand der Baumkonstruktion beeinflusst. Das bedeutet, dass dasselbe Zeichen je nach aktuellem Zustand zu unterschiedlichen Ergebnissen für den nächsten richtigen Zustand führt. Der Algorithmus ist zu komplex, um ihn vollständig zu beschreiben. Sehen wir uns 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 < erkannt wird, ändert sich der Status in "Tag open". Das Lesen eines a-z-Zeichens führt zur Erstellung eines Start-Tag-Tokens. Der Status wird in „Tag-Name“ geändert. Dieser Zustand bleibt aktiviert, bis das >-Zeichen verbraucht ist. 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 ändert sich wieder in "Data". Das <body>-Tag wird auf die gleichen Schritte angewendet. Bisher wurden die Tags html und body ausgegeben. Wir befinden uns jetzt wieder im Zustand „Daten“. Das Lesen des H-Zeichens von Hello world führt zum Erstellen und Ausgeben eines Zeichentokens, bis die < von </body> erreicht ist. Für jedes Zeichen von Hello world wird ein Zeichentoken ausgegeben.

Wir befinden uns jetzt wieder im Status Tag offen. Die Verarbeitung der nächsten Eingabe-/ führt zur Erstellung eines end tag token und einem Wechsel in den Tag-Namensstatus. Wir bleiben wieder in diesem Zustand, bis > erreicht ist.Dann wird das neue Tag-Token ausgegeben und wir kehren zum Status"Data" zurück. Die </html>-Eingabe wird wie der vorherige Fall behandelt.

Beispieleingabe tokenisieren
Abbildung 10: Tokenisierung der Beispieleingabe

Algorithmus zur Baumkonstruktion

Beim Erstellen des Parsers wird auch das Document-Objekt erstellt. Während der Baumkonstruktion wird der DOM-Baum mit dem Dokument im Stamm geändert und ihm 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 es relevant ist und für dieses Token erstellt wird. Das Element wird dem DOM-Baum und dem Stapel offener Elemente hinzugefügt. Dieser Stapel wird verwendet, um verschachtelte Abweichungen und nicht geschlossene Tags zu korrigieren. Der Algorithmus wird auch als Zustandsautomat beschrieben. Die Status werden als „Einfügemodi“ bezeichnet.

Sehen wir uns den Baumkonstruktionsprozess für die Beispieleingabe an:

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

Die Eingabe in der Phase der Baumkonstruktion ist eine Folge von Tokens aus der Tokenisierungsphase. Der erste Modus ist der Anfangsmodus. Beim Empfang des „html“-Tokens wird in den Modus before html (vor HTML) gewechselt und das Token wird in diesem Modus noch einmal verarbeitet. Dadurch wird das HTMLHTMLElement-Element erstellt, das an das Document-Stammobjekt angehängt wird.

Der Status wird in "before head" geändert. Das "body"-Token wird dann empfangen. Ein HTMLHeadElement wird implizit erstellt, auch wenn kein „head“-Token vorhanden ist, und es wird zur Baumstruktur hinzugefügt.

Wechseln wir nun zum "in head"-Modus und dann zu "after head". Das "body"-Token wird noch einmal verarbeitet, ein HTMLBodyElement wird erstellt und eingefügt und der Modus wird in "in body" geändert.

Jetzt werden die Zeichentokens des „Hello world“-Strings empfangen. Mit dem ersten wird ein „Text“-Knoten erstellt und eingefügt. Die anderen Zeichen werden an diesen Knoten angehängt.

Durch den Empfang des "body"-Endtokens wird ein Wechsel in den Modus "after body" ausgelöst. Wir empfangen nun das HTML-End-Tag, das uns in den Modus "after after body" verschiebt. Mit dem Empfang des Dateiende-Tokens wird das Parsing beendet.

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

Aktionen nach Abschluss des Parsings

In dieser Phase markiert der Browser das Dokument als interaktiv und beginnt mit dem Parsen von Skripts, die sich im "deferred"-Modus befinden. Dies sind Skripts, die nach dem Parsen des Dokuments ausgeführt werden sollen. Der Dokumentstatus wird dann auf „abgeschlossen“ gesetzt und das Ereignis „Laden“ wird ausgelöst.

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

Fehlertoleranz der Browser

Sie erhalten auf einer HTML-Seite nie den Fehler „Ungültige Syntax“. Browser korrigieren alle ungültigen Inhalte und fahren fort.

Sehen wir uns zum Beispiel diesen HTML-Code an:

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

Ich habe wohl 1 Million Regeln verletzt, z. B. „mytag“ ist kein Standard-Tag, die „p“- und „div“-Elemente sind falsch verschachtelt und es gibt noch mehr. Der Browser zeigt das aber trotzdem korrekt an und beschwert sich nicht. Ein Großteil des Parsercodes ist also die Behebung der Fehler des HTML-Autors.

Die Fehlerbehandlung ist in Browsern ziemlich konsistent, ist jedoch erstaunlicherweise nicht Teil der HTML-Spezifikationen. Wie Lesezeichen und Zurück-/Vorwärts-Schaltflächen ist dies eine Sache, die sich im Laufe der Jahre in Browsern entwickelt hat. Es gibt bekannte, ungültige HTML-Konstrukte, die auf vielen Websites wiederholt werden, und die Browser versuchen, diese in einer Weise zu beheben, die den anderen Browsern entspricht.

In der HTML5-Spezifikation werden jedoch einige dieser Anforderungen definiert. WebKit fasst dies im Kommentar zu Beginn der HTML-Parser-Klasse gut zusammen.

Der Parser parst tokenisierte Eingaben in das Dokument und baut dabei die Dokumentstruktur auf. Wenn das Dokument wohlgeformt ist, ist das Parsen unkompliziert.

Leider müssen wir mit vielen HTML-Dokumenten umgehen, die nicht wohlgeformt sind, sodass der Parser fehlertolerant sein muss.

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

  1. Das hinzugefügte Element ist innerhalb eines äußeren Tags explizit unzulässig. In diesem Fall sollten wir alle Tags bis zu dem Tag schließen, der das Element verbietet, und das Tag anschließend hinzufügen.
  2. Elemente dürfen nicht direkt hinzugefügt werden. Es kann sein, dass die Person, die das Dokument verfasst hat, ein Tag dazwischen vergessen hat. Das Tag dazwischen ist optional. Dies könnte 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. Alle Inline-Elemente werden bis zum nächsthöheren Blockelement geschlossen.
  4. Falls das Problem dadurch nicht behoben wird, schließen Sie Elemente, bis wir das Element hinzufügen dürfen, oder ignorieren Sie das Tag.

Sehen wir uns einige Beispiele für WebKit-Fehlertoleranz an:

</br> statt <br>

Einige Websites verwenden </br> anstelle von <br>. Für die 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;
}

Hinweis: Die Fehlerbehandlung erfolgt intern und wird dem Nutzer nicht angezeigt.

Eine verirrte Tabelle

Eine verirrte Tabelle ist eine Tabelle innerhalb einer anderen Tabelle, 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: Die innere Tabelle wird aus dem Stapel der äußeren Tabelle herausgenommen. Die Tabellen sind jetzt gleichgeordnet.

Verschachtelte Formularelemente

Falls der Nutzer ein Formular in ein anderes Formular eingibt, wird das zweite Formular ignoriert.

Der Code:

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

Zu tiefe Tag-Hierarchie

Der Kommentar spricht für sich selbst.

bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{

unsigned i = 0;
for (HTMLStackElem* curr = m_blockStack;
         i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
     curr = curr->next, i++) { }
return i != cMaxRedundantTagDepth;
}

Falsch platzierte HTML- oder Body-End-Tags

Auch hier spricht der Kommentar für sich selbst.

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

Webautoren sollten also aufgepasst haben: Wenn Sie nicht als Beispiel in einem Code-Snippet zur Fehlertoleranz von WebKit erscheinen möchten, sollten Sie wohlgeformten HTML-Code schreiben.

CSS-Parsing

Erinnern Sie sich noch an die Parsing-Konzepte aus der Einführung? Im Gegensatz zu HTML ist CSS eine kontextfreie Grammatik und kann mit den in der Einführung beschriebenen Parsertypen analysiert werden. Die CSS-Spezifikation definiert die lexikalische Grammatik und die Syntax-Grammatik von CSS.

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“ ist eine Kennung für eine Kennung, z. B. ein Klassenname. "name" ist eine Element-ID, auf die durch "#" 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*] ')' ]
  ;

Was bedeuten diese Regeln?

Ein Regelsatz ist diese Struktur:

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

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

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

Ein Regelsatz besteht also aus einem Selektor oder optional mehreren durch ein Komma und Leerzeichen getrennten Selektoren (S steht für White Space). Ein Regelsatz enthält geschweifte Klammern, in die sich eine Deklaration oder optional mehrere durch ein Semikolon getrennte Deklarationen befinden. „declaration“ und „selector“ werden in den folgenden BNF-Definitionen definiert.

WebKit-CSS-Parser

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

CSS wird geparst.
Abbildung 12: CSS-Code parsen

Verarbeitungsreihenfolge für Skripts 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 Parsen des Dokuments wird angehalten, bis das Skript ausgeführt wurde. Wenn das Skript extern ist, muss die Ressource zuerst aus dem Netzwerk abgerufen werden. Dies erfolgt ebenfalls synchron und das Parsen wird angehalten, bis die Ressource abgerufen wurde. Dies war viele Jahre das Modell und ist auch in den HTML4- und 5-Spezifikationen definiert. Autoren können das Attribut "defer" zu einem Skript hinzufügen. In diesem Fall wird das Parsen des Dokuments nicht unterbrochen und es wird ausgeführt, nachdem das Dokument geparst wurde. In HTML5 wird eine Option hinzugefügt, mit der das Skript als asynchron markiert werden kann, sodass es geparst und von einem anderen Thread ausgeführt wird.

Spekulatives Parsing

Sowohl WebKit als auch Firefox nehmen diese Optimierung vor. 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 sie lädt. Auf diese Weise können Ressourcen über parallele Verbindungen geladen werden und die Gesamtgeschwindigkeit wird verbessert. Hinweis: Der spekulative Parser parst nur Verweise auf externe Ressourcen wie externe Skripts, Stylesheets und Bilder: Er ändert nicht den DOM-Baum – dieser bleibt dem Hauptparser überlassen.

Style sheets

Stylesheets hingegen haben ein anderes Modell. Da Stylesheets den DOM-Baum nicht ändern, scheint es keinen Grund zu geben, auf sie zu warten und das Parsen des Dokuments anzuhalten. Es gibt jedoch ein Problem, wenn Skripts beim Parsen des Dokuments Stilinformationen anfordern. Wenn der Stil noch nicht geladen und geparst wurde, erhält das Skript falsche Antworten, was offensichtlich eine Menge Probleme verursacht. Dies scheint ein Grenzfall zu sein, kommt aber recht häufig vor. Firefox blockiert alle Skripts, wenn ein Stylesheet noch geladen und geparst wird. WebKit blockiert Skripts nur, wenn diese versuchen, auf bestimmte Stileigenschaften zuzugreifen, die von nicht geladenen Stylesheets beeinflusst werden können.

Konstruktion der Rendering-Baumstruktur

Während der DOM-Baum erstellt wird, konstruiert der Browser einen weiteren Baum, den Rendering-Baum. Diese Baumstruktur enthält visuelle Elemente in der Reihenfolge, in der sie angezeigt werden. Es ist die visuelle Darstellung des Dokuments. Der Zweck dieser Struktur besteht darin, 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 Begriff Renderer oder Rendering-Objekt.

Ein Renderer weiß, wie er sich und seine untergeordneten Elemente anordnen und malen muss.

Die RenderObject-Klasse von WebKit, die Basisklasse der Renderer, 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 stellt einen rechteckigen Bereich dar, der normalerweise der CSS-Box eines Knotens entspricht, wie in der CSS2-Spezifikation beschrieben. Er enthält geometrische Informationen wie Breite, Höhe und Position.

Der Boxtyp wird vom Wert "display" des für den Knoten relevanten Stilattributs bestimmt (siehe Abschnitt Stilberechnung). Dies ist der WebKit-Code, mit dem entschieden wird, welcher Renderertyp für einen DOM-Knoten gemäß dem display-Attribut 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 nicht geometrische Informationen enthalten.

Die Rendering-Struktur im Verhältnis zur DOM-Struktur

Die Renderer entsprechen DOM-Elementen, aber es gibt keine Eins-zu-Eins-Beziehung. Nicht visuelle DOM-Elemente werden nicht in die Rendering-Struktur eingefügt. Ein Beispiel hierfür ist das „head“-Element. Auch Elemente, deren Anzeigewert „none“ zugewiesen ist, werden nicht in der Struktur angezeigt. Elemente mit der Sichtbarkeit „hidden“ erscheinen dagegen dort.

Es gibt DOM-Elemente, die mehreren visuellen Objekten entsprechen. Dies sind normalerweise Elemente mit einer komplexen Struktur, die nicht durch ein einzelnes Rechteck beschrieben werden können. Das Auswahlelement hat beispielsweise drei Renderer: einen für den Anzeigebereich, einen für das Dropdown-Listenfeld und einen für die Schaltfläche. Wenn Text in mehrere Zeilen unterteilt ist, 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. Gemäß der CSS-Spezifikation darf ein Inline-Element entweder nur Blockelemente oder nur Inline-Elemente enthalten. Bei gemischten Inhalten werden anonyme Block-Renderer erstellt, um die Inline-Elemente zu umschließen.

Einige Rendering-Objekte entsprechen einem DOM-Knoten, jedoch nicht an derselben Stelle in der Baumstruktur. Floats und absolut positionierte Elemente befinden sich außerhalb des Ablaufs, werden in einem anderen Teil der Struktur platziert und dem echten Frame zugeordnet. Ein Platzhalter-Frame ist die Stelle, an der sie hätten sein sollen.

Die Rendering-Struktur und die entsprechende DOM-Struktur.
Abbildung 13: Die Rendering-Struktur und die entsprechende DOM-Struktur. „Viewport“ ist der ursprüngliche übergeordnete Block. In WebKit ist es das „RenderView“-Objekt

Ablauf der Baumkonstruktion

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

In WebKit wird das Auflösen des Stils und das Erstellen eines Renderers als "Anhängen" bezeichnet. Jeder DOM-Knoten verfügt über eine "attach"-Methode. Das Anhängen erfolgt synchron. Das Einfügen von Knoten in die DOM-Baumstruktur ruft die neue "attach"-Methode des Knotens auf.

Mit der Verarbeitung der „html“- und „body“-Tags wird der Stamm der Rendering-Struktur konstruiert. Das Stamm-Rendering-Objekt entspricht dem, was in der CSS-Spezifikation den beinhaltenden Block bezeichnet: den obersten Block, der alle anderen Blöcke enthält. Seine Abmessungen stellen den Darstellungsbereich dar: die Abmessungen des Darstellungsbereichs des Browserfensters. In Firefox wird dies ViewPortFrame und bei WebKit als RenderView bezeichnet. Dies ist das Rendering-Objekt, auf das das Dokument verweist. Der Rest der Baumstruktur wird als DOM-Knoten eingefügt.

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. Dazu werden die Stileigenschaften jedes Elements berechnet.

Der Stil umfasst Stylesheets verschiedener Ursprünge, Inline-Stilelemente und visuelle Eigenschaften im HTML-Code (z. B. die Eigenschaft "bgcolor").

Der Ursprung von Stylesheets sind die Standard-Stylesheets des Browsers, die vom Seitenautor bereitgestellten Stylesheets und Nutzer-Stylesheets – diese sind vom Browsernutzer bereitgestellte Stylesheets (mit Browsern können Sie Ihre bevorzugten Stile definieren). In Firefox wird hierfür beispielsweise ein Stylesheet im Firefox-Profilordner abgelegt.)

Die Stilberechnung bringt einige Schwierigkeiten mit sich:

  1. Stildaten sind ein sehr umfangreiches Konstrukt, da sie die zahlreichen Stileigenschaften enthalten. Dies kann zu Speicherproblemen führen.
  2. Das Finden der entsprechenden Regeln für jedes Element kann zu Leistungsproblemen führen, wenn es nicht optimiert ist. Das Durchlaufen der gesamten Regelliste für jedes Element nach Übereinstimmungen ist eine aufwendige Aufgabe. Selektoren können eine komplexe Struktur haben, die dazu führen kann, dass der Zuordnungsprozess auf einem scheinbar vielversprechenden Pfad beginnt, der sich als nutzlos erwiesen hat, und dass ein anderer Pfad ausprobiert werden muss.

    Hier ein Beispiel mit diesem zusammengesetzten Selektor:

    div div div div{
    ...
    }
    

    Dies bedeutet, dass die Regeln für einen <div> gelten, der auf 3 „div“-Elemente folgt. Angenommen, Sie möchten prüfen, ob die Regel für ein bestimmtes <div>-Element gilt. Sie wählen zur Überprüfung einen Pfad aus, der den Baum hinaufführt. 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. Anschließend müssen Sie andere Pfade in der Baumstruktur ausprobieren.

  3. Das Anwenden der Regeln beinhaltet 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 gleichgeordnet oder verwandt und:

  1. Die Elemente müssen sich im selben Mausstatus befinden.Das heißt, eines der Elemente darf nicht im Modus „:hover“ sein, das andere nicht.
  2. Keines der Elemente sollte eine ID haben
  3. Die Tag-Namen müssen übereinstimmen.
  4. Die Klassenattribute sollten übereinstimmen.
  5. Der Satz zugeordneter Attribute muss identisch sein
  6. Die Linkstatus müssen übereinstimmen
  7. Die Fokuszustände müssen übereinstimmen
  8. Keines der Elemente sollte von Attributselektoren betroffen sein, d. h. sie sind so definiert, dass sie eine Selektorübereinstimmung haben, bei der ein Attributselektor an einer beliebigen Position innerhalb des Selektors verwendet wird
  9. Die Elemente dürfen kein Inline-Stilattribut enthalten
  10. Es dürfen keine gleichgeordneten Selektoren 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 vorhanden. Dazu gehören 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 auch über Stilobjekte, diese werden jedoch nicht in einer Baumstruktur wie dem Stilkontextbaum gespeichert, nur der DOM-Knoten verweist auf seinen relevanten Stil.

Kontextstruktur im Firefox-Stil.
Abbildung 14: Kontextbaum im Stil von Firefox

Die Stilkontexte enthalten Endwerte. Die Werte werden berechnet, indem alle übereinstimmenden Regeln in der richtigen Reihenfolge angewendet und Änderungen vorgenommen 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 eines Regelbaums ist wirklich clever. Es ermöglicht die gemeinsame Nutzung dieser Werte zwischen Knoten, um eine erneute Verarbeitung zu vermeiden. Das spart auch Platz.

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

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

Berechneter Regelbaum
Abbildung 15: Berechneter Regelbaum

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

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

Unterteilung 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 werden entweder übernommen oder nicht übernommen. Geerbte Eigenschaften sind Eigenschaften, die vom übergeordneten Element übernommen werden, sofern sie nicht vom Element definiert werden. Nicht geerbte Eigenschaften, die als „reset“-Eigenschaften bezeichnet werden, verwenden Standardwerte, wenn sie nicht definiert sind.

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

Stilkontexte mithilfe des Regelbaums berechnen

Bei der Berechnung des Stilkontexts für ein bestimmtes Element berechnen wir zuerst einen Pfad im Regelbaum oder verwenden einen vorhandenen Pfad. Anschließend beginnen wir damit, die Regeln im Pfad anzuwenden, um die Strukturen in unserem neuen Stilkontext zu füllen. Wir beginnen beim untersten Knoten des Pfads – dem Knoten mit der höchsten Priorität (normalerweise der spezifischste Selektor) und durchlaufen den Baum nach oben, bis unsere Struktur voll ist. Wenn es keine Spezifikation für die Struktur in diesem Regelknoten gibt, können wir sie erheblich optimieren. Wir steigen den Baum hinauf, bis wir einen Knoten finden, der sie vollständig angibt. Das ist die beste Optimierung. Die gesamte Struktur ist freigegeben. Dies spart die Berechnung von Endwerten und Speicherplatz.

Wenn wir Teildefinitionen finden, klettern wir den Baum hinauf, bis die Struktur gefüllt ist.

Wenn wir keine Definitionen für unsere Struktur finden, verweisen wir auf die Struktur unserer übergeordneten Struktur im Kontextbaum, falls es sich um eine vererbte Struktur handelt. In diesem Fall war es auch möglich, Strukturen freizugeben. Wenn es sich um eine Zurücksetzen-Struktur handelt, werden Standardwerte verwendet.

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

Falls ein Element ein gleichgeordnetes oder übergeordnetes Element hat, das auf denselben Baumknoten verweist, kann der gesamte Stilkontext von beiden gemeinsam genutzt werden.

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

<html>
  <body>
    <div class="err" id="div1">
      <p>
        this is a <span class="big"> big error </span>
        this is also a
        <span class="big"> very  big  error</span> error
      </p>
    </div>
    <div class="err" id="div2">another error</div>
  </body>
</html>

Und die folgenden Regeln:

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

Nehmen wir der Einfachheit halber an, wir müssen nur zwei Strukturen ausfüllen: die Farbstruktur und die Randstruktur. Die Farbstruktur enthält nur ein Element: die Farbe. Die Randstruktur enthält die vier Seiten.

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

Regelbaum
Abbildung 16: Der Regelbaum

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

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 gleichen die Regeln ab und stellen fest, dass die Regeln für <div> 1, 2 und 6 sind. Das bedeutet, dass im Baum bereits ein Pfad vorhanden ist, den unser Element verwenden 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. Als Erstes füllen wir die Randstruktur aus. Da der letzte Regelknoten (F) nicht zur Randstruktur beiträgt, können wir den Baum hinaufgehen, bis wir eine zwischengespeicherte Struktur finden, die in einer vorherigen Knoteneinfügung berechnet wurde, und diese verwenden. Wir finden sie auf Knoten B, dem obersten Knoten, der Randregeln definiert.

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

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

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

Wenn wir beispielsweise Regeln für Schriftarten in einem Absatz hinzugefügt haben:

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

Dann hätte das Absatzelement, das ein untergeordnetes Element des div-Elements im Kontextbaum ist, dieselbe Schriftartstruktur wie sein übergeordnetes Element haben. Dies ist der Fall, wenn für den Absatz keine Schriftartregeln festgelegt wurden.

In WebKit, das über keinen Regelbaum verfügt, werden die übereinstimmenden Deklarationen viermal durchlaufen. Zuerst werden unwichtige Eigenschaften mit hoher Priorität angewendet (Eigenschaften, die zuerst angewendet werden sollten, weil andere davon abhängen, wie z. B. Display), dann werden wichtige Regeln mit hoher Priorität, dann normale Priorität als unwichtig und dann wichtige Regeln mit normaler Priorität angewendet. Dies bedeutet, dass Eigenschaften, die mehrfach vorkommen, entsprechend der richtigen Kaskadenreihenfolge aufgelöst werden. Der Letzte gewinnt.

Zusammenfassend lässt sich also sagen, dass das Teilen der Stilobjekte (gesamte oder einige der darin enthaltenen Strukturen) die Probleme 1 und 3 löst. Der Firefox-Regelbaum hilft auch dabei, die Eigenschaften in der richtigen Reihenfolge anzuwenden.

Manipulieren der Regeln für eine einfache Zuordnung

Für Stilregeln gibt es mehrere Quellen:

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

Wie bereits in Problem 2 erwähnt, kann sich der Abgleich von CSS-Regeln etwas komplizierter machen. Um die Schwierigkeit zu lösen, werden die Regeln verändert, um den Zugriff zu erleichtern.

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

Diese Bearbeitung erleichtert das Abgleichen von Regeln erheblich. Es muss nicht jede Deklaration überprüft werden: Wir können die relevanten Regeln für ein Element aus den Zuordnungen extrahieren. Durch diese Optimierung werden mehr als 95% der Regeln eliminiert, sodass sie während des Zuordnungsprozesses nicht einmal berücksichtigt werden müssen(4.1).

Sehen wir uns zum Beispiel die folgenden Stilregeln an:

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

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

Für das folgende HTML-Fragment:

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

Wir suchen zuerst nach Regeln für das p-Element. Die Klassenzuordnung enthält einen Fehlerschlüssel, unter dem die Regel für „p.error“ zu finden ist. Das div-Element hat relevante Regeln in der ID-Map (Schlüssel ist die ID) und der Tag-Map. Jetzt müssen Sie nur noch herausfinden, welche der durch die Schlüssel extrahierten Regeln wirklich übereinstimmen.

Angenommen, die Regel für das div-Element lautet wie folgt:

table div {margin: 5px}

Er wird trotzdem aus der Tag-Zuordnung extrahiert, da der Schlüssel der Selektor ganz rechts ist. Er entspricht jedoch nicht dem div-Element, das keinen Tabellenübergeordnet hat.

Sowohl WebKit als auch Firefox nehmen diese Bearbeitung vor.

Kaskadenreihenfolge von Stylesheets

Das Stilobjekt verfügt über Eigenschaften, die jedem visuellen Attribut (allen CSS-Attributen, aber allgemeiner) entsprechen. Wenn die Eigenschaft durch keine 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 Kaskadenreihenfolge, um das Problem zu lösen.

Eine Deklaration für eine Stileigenschaft kann in verschiedenen Stylesheets und mehrmals innerhalb eines Stylesheets vorkommen. Daher ist die Reihenfolge der Anwendung der Regeln sehr wichtig. Dies wird als Kaskadenreihenfolge bezeichnet. Laut der CSS2-Spezifikation ist die Kaskadenreihenfolge (von niedrig nach hoch):

  1. Browserdeklarationen
  2. Normale Nutzerdeklarationen
  3. Normale Autorendeklarationen
  4. Wichtige Autorendeklarationen
  5. Wichtige Nutzerdeklarationen

Die Browserdeklarationen sind am wenigsten wichtig und Nutzer überschreiben den Autor nur dann, wenn die Deklaration als wichtig markiert wurde. Deklarationen mit derselben Reihenfolge werden nach der Spezifität und dann nach 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 Selektor-Spezifität wird durch die CSS2-Spezifikation so definiert:

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

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

Die Zahlenbasis, die Sie verwenden müssen, wird durch die höchste Anzahl 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 17-stellige Zahlenbasis. Die spätere Situation kann bei einem Selektor wie diesem auftreten: html body div div p... (17 Tags in Ihrem Selektor... nicht sehr wahrscheinlich).

Beispiele:

 *             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
 li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
 li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
 h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
 ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
 li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
 #x34y         {}  /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
 style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

Regeln sortieren

Nachdem die Regeln abgeglichen wurden, werden sie entsprechend den Kaskadenregeln sortiert. WebKit verwendet Blasensortierung für kleine Listen und Zusammenführen für große 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 ein Flag, das angibt, ob alle Top-Level-Stylesheets (einschließlich @imports) geladen wurden. Wenn der Stil beim Anhängen nicht vollständig geladen wird, werden Platzhalter verwendet und er im Dokument markiert. Sobald die Stylesheets geladen wurden, werden sie neu berechnet.

Layout

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

HTML verwendet ein flussbasiertes Layoutmodell. Dies bedeutet, dass es meistens möglich ist, die Geometrie in einem einzigen Durchlauf zu berechnen. Elemente, die später „im Fluss“ erscheinen, wirken sich normalerweise nicht auf die Geometrie von Elementen aus, die früher „im Ablauf“ sind. Das Layout kann sich also von links nach rechts und von oben nach unten durch das Dokument bewegen. Es gibt Ausnahmen: Beispielsweise können HTML-Tabellen mehr als eine Karte / ein Ticket erfordern.

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

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

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

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

Dirty Bit-System

Damit Browser nicht für jede kleine Änderung ein vollständiges Layout vornehmen 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“: erforderliches Layout.

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 vorhanden ist, das ein Layout benötigt.

Globales und inkrementelles Layout

Das Layout kann für die gesamte Rendering-Struktur ausgelöst werden. Dies ist ein „globales“ Layout. Das kann folgende Gründe haben:

  1. Eine globale Stiländerung, die alle Renderer betrifft, z. B. eine Änderung der Schriftgröße.
  2. Durch die Größenanpassung eines Bildschirms

Das Layout kann inkrementell sein. Es werden nur die schmutzigen Renderer angelegt. Dies kann zu Schäden führen und zusätzliche Layouts erfordern.

Inkrementelles Layout wird (asynchron) ausgelöst, wenn Renderer als „dirty“ markiert sind. Das ist beispielsweise der Fall, wenn neue Renderer an die Rendering-Baumstruktur angehängt werden, nachdem zusätzliche Inhalte aus dem Netzwerk stammen und dem DOM-Baum hinzugefügt wurden.

Inkrementelles Layout.
Abbildung 18: Inkrementelles Layout – nur schmutzige Renderer und ihre untergeordneten Elemente werden dargestellt

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 Batch-Ausführung dieser Befehle aus. WebKit verfügt auch über einen Timer, der ein inkrementelles Layout ausführt. Die Baumstruktur wird durchlaufen und die „schmutzigen“ Renderer erhalten das Layout.

Skripts, die Stilinformationen wie "offsetHeight" anfordern, können das inkrementelle Layout synchron auslösen.

Das globale Layout wird normalerweise synchron ausgelöst.

Manchmal wird das Layout als Callback nach einem anfänglichen Layout ausgelöst, weil sich einige Attribute wie z. B. die Scroll-Position 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 Renderinggrößen aus einem Cache entnommen und nicht neu berechnet...

In einigen Fällen wird nur eine Unterstruktur geändert und das Layout beginnt nicht vom Stamm aus. Dies kann in Fällen passieren, in denen 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 im Stammverzeichnis beginnt).

Der Layoutprozess

Das Layout hat normalerweise folgendes Muster:

  1. Der übergeordnete Renderer legt seine eigene Breite fest.
  2. Das übergeordnete Element hat Vorrang vor den untergeordneten Elementen und:
    1. Platziert den untergeordneten Renderer (legt x und y fest).
    2. Ruft bei Bedarf ein untergeordnetes Layout auf - sie sind unsauber oder befinden sich in einem globalen Layout 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 sowie die Höhe der Ränder und des Abstands, um seine eigene Höhe festzulegen. Diese wird vom übergeordneten Renderer verwendet.
  4. Setzt „Dirty Bit“ auf „false“.

Firefox verwendet ein Statusobjekt(nsHTMLReflowState) als Parameter für das Layout (auch "Reflow" genannt). Der Zustand enthält unter anderem die Breite der übergeordneten Elemente.

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

Berechnung der Breite

Die Breite des Renderers wird anhand der Breite des Containerblocks, der Stileigenschaft "width" des Renderers sowie der Ränder und Rahmen berechnet.

Beispielsweise die Breite des folgenden div-Elements:

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

Wird von WebKit wie folgt 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 wie folgt berechnet wird:
clientWidth() - paddingLeft() - paddingRight()

„clientWidth“ und „clientHeight“ stellen das Innere eines Objekts ohne Rahmen und Bildlaufleiste dar.

  • Die Breite der Elemente wird im Stilattribut „width“ angegeben. Sie wird als absoluter Wert berechnet, indem der Prozentsatz der Containerbreite berechnet wird.

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

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

Ist die bevorzugte Breite größer als die maximale Breite, wird die maximale Breite verwendet. Wenn sie unter der Mindestbreite (kleinste unverzerrbare Einheit) liegt, wird die Mindestbreite verwendet.

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

Zeilenumbruch

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

Malerei

In der Painting-Phase wird die Rendering-Struktur durchlaufen und die "paint()"-Methode 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, also die gesamte Struktur wird dargestellt, oder inkrementell sein. Beim inkrementellen Painting ändern sich einige Renderer so, dass sich dies nicht auf die gesamte Struktur auswirkt. Durch den geänderten Renderer wird sein Rechteck auf dem Bildschirm ungültig. Das Betriebssystem sieht sie 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 das komplizierter, weil sich der Renderer in einem anderen Prozess befindet als der Hauptprozess. Chrome simuliert das Betriebssystemverhalten in gewissem Umfang. Die Präsentation überwacht diese Ereignisse und delegiert die Nachricht an den Renderingstamm. Die Baumstruktur wird durchlaufen, bis der relevante Renderer erreicht ist. Die Aktualisierung erfolgt automatisch (und normalerweise auch für die untergeordneten Elemente).

Painting-Reihenfolge

CSS2 definiert die Reihenfolge des Painting-Prozesses. Dies ist tatsächlich 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 gezeichnet werden. Die Stapelreihenfolge eines Block-Renderers sieht so aus:

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

Firefox-Displayliste

Firefox durchläuft die Rendering-Struktur und erstellt eine Anzeigeliste für das dargestellte Rechteck. Sie enthält die für das Rechteck relevanten Renderer in der richtigen Painting-Reihenfolge (Hintergründe der Renderer, dann Rahmen usw.).

Auf diese Weise muss der Baum für die Darstellung nur einmal und nicht mehrmals durchlaufen werden. Es werden alle Hintergründe, dann alle Bilder, dann alle Rahmen usw. gezeichnet.

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

WebKit-Rechteckspeicher

Vor der Aktualisierung speichert WebKit das alte Rechteck als Bitmap. Dann wird nur das Delta zwischen den neuen und den alten Rechtecken dargestellt.

Dynamische Änderungen

Die Browser führen bei einer Änderung nur die geringstmöglichen Aktionen aus. Bei Änderungen an der Farbe eines Elements wird daher nur die Darstellung des Elements aktualisiert. Änderungen an der Position des Elements führen dazu, dass das Layout und die Darstellung des Elements, seiner untergeordneten Elemente und möglicherweise gleichgeordneter Elemente aktualisiert werden. Durch das Hinzufügen eines DOM-Knotens werden das Layout und die Darstellung des Knotens aktualisiert. Größere Änderungen, wie die Vergrößerung der Schriftgröße des "html"-Elements, führen zur Entwertung der Caches sowie zur Neulayout- und Darstellungsaktualisierung der gesamten Struktur.

Die Rendering-Engine-Threads

Das Rendering-Modul ist ein Single-Thread-Modul. Mit Ausnahme der Netzwerkvorgänge findet fast alles in einem einzigen Thread statt. In Firefox und Safari ist dies der Hauptthread des Browsers. In Chrome ist dies der Hauptthread des Tab-Prozesses.

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

Ereignisschleife

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

while (!mExiting)
    NS_ProcessNextEvent(thread);

Visuelles CSS2-Modell

Der Canvas

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

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

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

CSS-Box-Modell

Im CSS-Boxmodell werden die rechteckigen Felder beschrieben, die für Elemente in der Dokumentstruktur generiert und gemäß dem visuellen Formatierungsmodell angeordnet werden.

Jedes Feld hat einen Inhaltsbereich (z. B. Text oder ein Bild) und optional einen Rand, einen Rahmen und einen Rand.

CSS2-Boxmodell
Abbildung 19: CSS2-Boxmodell

Jeder Knoten generiert 0...n solche Boxen.

Alle Elemente haben eine „display“-Eigenschaft, die den Boxtyp bestimmt, der generiert wird.

Beispiele:

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

Die Standardeinstellung ist „Inline“, aber das Stylesheet des Browsers 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 entsprechend seiner Position im Dokument positioniert. Das heißt, ihr Platz in der Rendering-Baumstruktur entspricht ihrem Platz im DOM-Baum und wird entsprechend dem Boxtyp und den Abmessungen angelegt
  2. Unverankert: Das Objekt wird zuerst wie im normalen Ablauf dargestellt und dann so weit nach links oder rechts wie möglich verschoben.
  3. Absolut: Das Objekt wird in der Rendering-Struktur an einer anderen Stelle als im DOM-Baum platziert.

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

  • statische und relative Werte verursachen einen normalen Fluss
  • absolute und feste Ursachen für absolute Positionierung

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

Die Anordnung der Boxen wird durch Folgendes bestimmt:

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

Boxtypen

Block-Box: bildet einen Block. Er hat ein eigenes Rechteck im Browserfenster.

Block-Feld.
Abbildung 20: Block-Feld

Inline-Feld: hat keinen eigenen Block, sondern befindet sich innerhalb eines übergeordneten Blocks.

Inline-Felder.
Abbildung 21: Inline-Felder

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

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

Inline-Boxen werden innerhalb von Linien oder Zeilenfeldern platziert. Die Linien sind mindestens so hoch wie die höchste Box, können aber auch höher sein, wenn die Boxen an der Basislinie ausgerichtet sind, d. h., der untere Teil eines Elements ist an einem Punkt einer anderen Box ausgerichtet, bei dem es sich nicht um die untere Box handelt. Wenn die Containerbreite nicht ausreicht, werden die Inlines auf mehrere Zeilen verteilt. Das passiert normalerweise in einem Absatz.

Linien.
Abbildung 23: Linien

Positioning

Relativ

Relative Positionierung – positioniert wie üblich und dann um das erforderliche Delta verschoben

Relative Positionierung
Abbildung 24: Relative Positionierung

Float

Eine Float-Box wird an die linke oder rechte Seite einer Zeile verschoben. Interessant ist, dass die anderen Felder um ihn herum verlaufen. 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:

Gleitkommawert.
Abbildung 25: Gleitkommazahl

Absolut und fest

Das Layout wird genau definiert, unabhängig vom normalen Ablauf. Das Element nimmt nicht am normalen Fluss teil. Die Abmessungen beziehen sich auf den Container. Bei der festen Positionierung ist der Container der Darstellungsbereich.

Feste Positionierung
Abbildung 26: Feste Positionierung

Ebenendarstellung

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

Die Boxen werden in Stapel unterteilt (Stapelkontexte genannt). In jedem Stapel werden zuerst die hinteren Elemente und die vorderen Elemente darüber dargestellt, da sie näher am Nutzer sind. Im Falle einer Überschneidung wird das erste Element durch das vorderste Element ausgeblendet.

Die Stapel werden entsprechend der Z-Index-Eigenschaft angeordnet. Boxen mit der Eigenschaft "z-index" bilden einen lokalen Stapel. Der Darstellungsbereich hat 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 ist:

Feste Positionierung
Abbildung 27: Feste Positionierung

Obwohl das rote div-Element in der Auszeichnung vor dem grünen div-Element steht und im regulären Ablauf vorher dargestellt worden wäre, ist die Z-Index-Eigenschaft höher, sodass es im Stapel weiter vorne liegt, der von der Stammbox gehalten wird.

Ressourcen

  1. Browserarchitektur

    1. Grosskurth, Alan. Eine 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“ bezeichnet), Addison-Wesley, 1986
    2. Rick Jelliffe. 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 zum Mozilla Style-System
    5. Chris Waterson, Hinweise zum HTML-Reflow
    6. Chris Waterson, Gecko Überblick
    7. Alexander Larsson, The life of an HTML HTTP request
  4. WebKit

    1. David Hyatt, CSS implementieren(Teil 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. Spezifikation für Cascading Style Sheets Level 2 Revision 1 (CSS 2.1)
  6. Build-Anleitung für Browser

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

Übersetzungen

Diese Seite wurde zweimal ins Japanische übersetzt:

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

Vielen Dank an alle!