ホームページ >ウェブフロントエンド >htmlチュートリアル >ブラウザの仕組み: 新しい Web ブラウザの舞台裏_html/css_WEB-ITnose

ブラウザの仕組み: 新しい Web ブラウザの舞台裏_html/css_WEB-ITnose

WBOY
WBOYオリジナル
2016-06-21 08:48:161336ブラウズ

序文

これは、WebKit と Gecko の内部操作についての包括的な入門書であり、イスラエルの開発者 Tali Gahir による広範な研究の結果です。過去数年にわたり、彼女はブラウザの内部に関する公開されているすべてのデータを調査し、Web ブラウザのソース コードを何時間もかけて精査してきました。彼女はこう書きました:

IE が市場シェアの 90% を占めていた時代、私たちはブラウザを「ブラック ボックス」として扱う以外に何もできませんでした。しかし現在、オープンソース ブラウザーが市場シェアの半分以上を占めているため、ベールを剥がして Web ブラウザーの内部で何が起こっているのかを見てみる時期が来ています。そうですね、そこには数百万行の C++ コードが含まれています...

タリーは研究結果を自身の Web サイト

で公開しましたが、より多くの人に知ってもらう価値があると感じたため、再構成して公開しましたここ。

Web 開発者として、ブラウザーの内部動作を学ぶことは、より賢明な決定を下し、ベストな開発プラクティスが機能する理由を理解するのに役立ちます。これはかなり長い文書ですが、時間をかけてじっくり読むことをお勧めします。時間を費やす価値は十分にあったと感じていただけると思います。 Paul Irish、Chrome Developer Affairs

はじめに

Web ブラウザはおそらく最も広く使用されているソフトウェアです。この紹介記事では、それらが舞台裏でどのように機能するかを見ていきます。アドレス バーに google.com と入力してから、ブラウザ画面に Google ホームページが表示されるまでに何が起こるかを学びます。

目次

これから説明するブラウザ

現在使用されている主流ブラウザは、Internet Explorer、Firefox、Safari、Chrome、Opera の 5 つです。 。この記事では、例としてオープン ソース ブラウザ、つまり Firefox、Chrome、Safari (一部オープン ソース) を使用します。 StatCounter Browser Statistics によると、現在 (2011 年 8 月) Firefox、Safari、Chrome の合計市場シェアは 60% 近くです。オープンソース ブラウザが現在、ブラウザ市場の非常に強固な部分を占めていることがわかります。

ブラウザの主な機能

ブラウザの主な機能は、サーバーにリクエストを送信し、選択したネットワーク リソースをブラウザ ウィンドウに表示することです。ここで説明するリソースは通常 HTML ドキュメントを指しますが、PDF、画像、またはその他のタイプのリソースである場合もあります。リソースの場所は、ユーザーが URI (Uniform Resource Identifier) を使用して指定します。

ブラウザーが HTML ファイルを解釈して表示する方法は、HTML および CSS の仕様で指定されています。これらの仕様は、Web 標準化団体である W3C (World Wide Web Consortium) によって維持されています。

長年にわたり、ブラウザーは独自の拡張機能を開発する際にこれらの仕様に完全には準拠していないため、Web 開発者に深刻な互換性の問題が発生してきました。現在、ほとんどのブラウザは多かれ少なかれ準拠しています。

ブラウザのユーザー インターフェイスは、次のような多くの要素を相互に共有します。

  • URI を入力するためのアドレス バー
  • 進むボタンと戻るボタン
  • ブックマーク設定オプション
  • 現在のドキュメントを更新および読み込みを停止するための更新ボタンと停止ボタン
  • ホーム ページに戻るためのホーム ボタン

奇妙なことに、次のようなものがあります。これはブラウザのユーザー インターフェイスの正式な仕様ではなく、長年にわたるベスト プラクティスの自然な進化と相互の模倣です。また、HTML5 ではブラウザーに必須のユーザー インターフェイス要素は定義されていませんが、アドレス バー、ステータス バー、ツールバーなどのいくつかの一般的な要素がリストされています。もちろん、Firefox のダウンロード マネージャーなど、各ブラウザーが独自の機能を備えている場合もあります。

ブラウザの高レベル構造

ブラウザの主なコンポーネントは次のとおりです ():

  1. ユーザー インターフェイス - アドレス バー、進む/戻るボタン、ブックマーク メニューなどが含まれます。メイン ブラウザ ウィンドウで要求したページの表示を除けば、表示の他のすべての部分はユーザー インターフェイスに属します。
  2. ブラウザ エンジン - ユーザー インターフェイスとレンダリング エンジンの間で命令を渡します。
  3. レンダリング エンジン - 要求されたコンテンツの表示を担当します。要求されたコンテンツが HTML の場合、HTML および CSS コンテンツを解析し、解析されたコンテンツを画面に表示します。
  4. ネットワーク - HTTP リクエストなどのネットワーク呼び出し用。そのインターフェイスはプラットフォームに依存せず、すべてのプラットフォームに基盤となる実装を提供します。
  5. ユーザー インターフェイス バックエンド - コンボ ボックスやウィンドウなどの基本的なウィジェットを描画するために使用されます。内部ではオペレーティング システムのユーザー インターフェイス メソッドを使用しながら、プラットフォームに依存しない共通インターフェイスを公開します。
  6. JavaScript インタプリタ 。 JavaScript コードを解析して実行するために使用されます。
  7. データストレージ。これは永続化レイヤーです。ブラウザは Cookie などのさまざまなデータをハード ドライブに保存する必要があります。新しい HTML 仕様 (HTML5) は、完全な (ただし軽量の) ブラウザ内データベースである「Web データベース」を定義しています。
図: ブラウザの主要コンポーネント。

ほとんどのブラウザとは異なり、Chrome ブラウザの各タブがレンダリング エンジン インスタンスに対応していることは注目に値します。各タブは独立したプロセスです。

プレゼンテーション エンジン

プレゼンテーション エンジンの役割は…もちろん「レンダリング」、つまり要求されたコンテンツをブラウザ画面に表示することです。

デフォルトでは、レンダリング エンジンは HTML および XML ドキュメントと画像を表示できます。プラグイン (またはブラウザ拡張機能) を通じて、他のタイプのコンテンツを表示することもできます。たとえば、PDF ビューア プラグインを使用すると、PDF ドキュメントを表示できます。ただし、この章では、CSS を使用してフォーマットされた HTML コンテンツと画像を表示するという主な用途に焦点を当てます。

レンダリング エンジン

この記事で説明するブラウザ (Firefox、Chrome、Safari) は 2 つのレンダリング エンジンで構築されています。 Firefox は、Mozilla の「自家製」レンダリング エンジンである Gecko を使用しています。 Safari ブラウザと Chrome ブラウザはどちらも WebKit を使用します。

WebKit は、もともと Linux プラットフォームで使用されていたオープンソースのレンダリング エンジンで、後に Mac と Windows をサポートするために Apple によって変更されました。詳細については、webkit.org を参照してください。

メインプロセス

レンダリング エンジンは、最初にネットワーク層から要求されたドキュメントのコンテンツを取得します。コンテンツのサイズは通常 8000 ブロックに制限されます。

次に、以下に示す基本プロセスを進めます。

図: レンダリング エンジンの基本プロセス。

レンダリング エンジンは HTML ドキュメントの解析を開始し、各タグを「コンテンツ ツリー」上のノードに 1 つずつ変換します。外部 CSS ファイルおよびスタイル要素内のスタイル データも解析されます。 HTML の視覚的な指示を含むこれらのスタイル情報は、別のツリー構造を作成するために使用されます。

レンダリング ツリーには、色やサイズなどの視覚的プロパティを持つ複数の四角形が含まれています。これらの四角形の配置順序が、画面上に表示される順序になります。

プレゼンテーション ツリーが構築された後、「」処理段階に入ります。この段階では、各ノードに画面上で表示される正確な座標を割り当てます。次の段階は、レンダリング エンジンがレンダリング ツリーを横断し、各ノードがユーザー インターフェイス バックエンド層によって描画されることです。

これは段階的なプロセスであることに注意することが重要です。より良いユーザー エクスペリエンスを実現するために、レンダリング エンジンはコンテンツをできるだけ早く画面に表示するよう努めます。レンダリング ツリーの構築とレイアウトの設定を開始する前に、HTML ドキュメント全体が解析されるまで待つ必要はありません。レンダリング エンジンは、コンテンツの一部を解析して表示すると同時に、残りのコンテンツをネットワークから受信して処理し続けます。

メインプロセスの例

図: WebKit のメインフローチャート: Mozilla の Gecko レンダリング エンジンのメインプロセス ()

図 3 と図 4 からわかるように、WebKit と Gecko で使用される用語は次のとおりです。わずかに異なりますが、全体的なプロセスは基本的に同じです。

Gecko では、視覚的な書式設定要素のツリーを「フレーム ツリー」と呼びます。すべての要素がフレームです。 WebKit で使用される用語は「レンダリング ツリー」であり、これは「レンダリング オブジェクト」で構成されます。要素の配置について、WebKit では「レイアウト」という用語が使用されますが、Gecko ではそれを「リフロー」と呼びます。 WebKit では、DOM ノードと視覚情報を接続してレンダリング ツリーを作成するプロセスに対して「追加」という用語が使用されます。セマンティックではない微妙な違いの 1 つは、Gecko には DOM 要素を生成するための HTML と DOM ツリーの間に「コンテンツ スロット」と呼ばれるレイヤーがあることです。プロセスの各部分を 1 つずつ説明します。

解析 - 概要

解析はレンダリング エンジンの非常に重要な部分であるため、さらに詳しく説明します。まず、解析を紹介します。

ドキュメントの解析とは、ドキュメントをコードで理解して使用できる意味のある構造に変換することを意味します。解析の結果は通常、文書構造を表すノード ツリーであり、これは解析ツリーまたは構文ツリーと呼ばれます。

例 - 式 2 + 3 - 1 を解析すると、次のツリーが返されます。

図: 数学式ツリー ノード

構文

解析は、文書に続く文法規則に基づいて行われます (文書が書かれている言語または形式)。解析できるすべての形式は、特定の文法 (語彙と文法規則で構成される) に対応している必要があります。これをこう呼びます。人間の言語はそのような言語ではないため、従来の解析技術を使用して解析することはできません。

パーサーと字句アナライザーの組み合わせ

解析プロセスは、字句分析と構文分析の 2 つのサブプロセスに分割できます。

字句解析は、入力を多数のトークンに分割するプロセスです。トークンは言語の単語であり、コンテンツを構成する単位です。人間の言語では、言語辞書の単語に相当します。

文法分析は、言語の文法規則を適用するプロセスです。

パーサーは通常、解析作業を次の 2 つのコンポーネントに分割します: 字句解析器 (トークン ジェネレーターとも呼ばれる)。入力コンテンツを有効なトークンに分解します。 パーサー は、言語の文法規則に従って文書の構造を分析し、解析ツリーを構築する役割を果たします。字句アナライザーは、スペースや改行などの無関係な文字を分離する方法を知っています。

図: ソース文書から解析ツリーまで

解析は反復プロセスです。通常、パーサーはレクサーに新しいトークンを要求し、それを何らかの文法規則と照合しようとします。一致するルールが見つかった場合、パーサーはそのトークンに対応するノードを解析ツリーに追加し、次のトークンの要求に進みます。

一致するルールがない場合、パーサーはトークンを内部に保存し、内部に保存されているすべてのトークンに一致するルールが見つかるまでトークンの要求を続けます。一致するルールが見つからない場合、パーサーは例外を発生させます。これは、ドキュメントが無効であり、構文エラーが含まれていることを意味します。

翻訳

多くの場合、解析ツリーはまだ最終製品ではありません。解析は通常、入力ドキュメントを別の形式に変換する翻訳中に使用されます。コンパイルはその一例です。コンパイラは、まずソース コードを解析ツリーに解析し、次に解析ツリーをマシン コード ドキュメントに変換することによって、ソース コードをマシン コードにコンパイルできます。

図: コンパイル プロセス

解析の例

図 5 では、数式を使用して解析ツリーを構築します。ここで、解析プロセスを示すために単純な数学言語を定義してみましょう。

語彙: 整数、プラス記号、マイナス記号を含む言語を使用します。

構文:

  1. 言語を構成する文法単位は、式、用語、演算子です。
  2. 私たちが使用する言語には、任意の数の表現を含めることができます。
  3. 式の定義は、「項」の後に「演算子」が続き、さらに別の「項」が続きます。
  4. 演算子はプラスまたはマイナス記号です。
  5. 項は整数または式です。

2 + 3 - 1 を分析しましょう。

文法規則に一致する最初の部分文字列は 2 で、これは文法規則 5 に従った項目です。構文ルールに一致する 2 番目の部分文字列は 2 + 3 で、これはルール 3 (用語の後に演算子、その後に用語) に従った式です。次の一致は入力の終わりに達しました。 2 + 3 - 1 は式です。2 + 3 が項であることはすでにわかっており、「項の後に演算子、次に項」のルールに従います。 2++ はどのルールにも一致しないため、無効な入力です。

語彙と文法の正式な定義

語彙は通常、正規表現で表されます。

たとえば、サンプル言語は次のように定義できます。

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

ご覧のとおり、ここでは正規表現を使用して整数の定義が与えられています。

文法は通常、BNF と呼ばれる形式を使用して定義されます。この例の言語は次のように定義できます。

expression :=  term  operation  termoperation :=  PLUS | MINUSterm := INTEGER | expression

前に、言語の文法が文脈自由文法であれば、通常のパーサーで解析できると言いました。文脈自由文法の直感的な定義は、BNF 形式で完全に表現できる文法です。正式な定義については、文脈自由文法に関するウィキペディアの記事を参照してください。

パーサーの種類

パーサーには、トップダウン パーサーとボトムアップ パーサーの 2 つの基本的な種類があります。直感的には、トップダウン パーサーは文法の高レベル構造から開始し、そこで一致する構造を見つけようとします。ボトムアップ パーサーは、低レベルのルールから開始し、高レベルのルールが満たされるまで、入力コンテンツを徐々に文法ルールに変換します。

両方のパーサーがこの例をどのように解析するかを見てみましょう:

トップダウン パーサーは高レベルのルールから開始します。最初に 2 + 3 を式として識別し、次に 2 + 3 を識別します。 - 式として 1 (式を識別するプロセスには他のルールの照合が含まれますが、開始点は最上位のルールです)。

ボトムアップ パーサーは入力コンテンツをスキャンし、一致するルールを見つけて、一致する入力コンテンツをルールで置き換えます。このように入力内容が終わるまで置換を続けます。部分的に一致した式はパーサーのスタックに保存されます。

堆栈 输入
2 + 3 - 1
+ 3 - 1
项运算 3 - 1
表达式 - 1
表达式运算符 1
表达式  

这种自下而上的解析器称为移位归约解析器,因为输入在向右移位(设想有一个指针从输入内容的开头移动到结尾),并且逐渐归约到语法规则上。

自动生成解析器

有一些工具可以帮助您生成解析器,它们称为解析器生成器。您只要向其提供您所用语言的语法(词汇和语法规则),它就会生成相应的解析器。创建解析器需要对解析有深刻理解,而人工创建并优化解析器并不是一件容易的事情,所以解析器生成器是非常实用的。

WebKit 使用了两种非常有名的解析器生成器:用于创建词法分析器的 Flex 以及用于创建解析器的 Bison (您也可能遇到 Lex 和 Yacc 这样的别名)。Flex 的输入是包含标记的正则表达式定义的文件。Bison 的输入是采用 BNF 格式的语言语法规则。

HTML 解析器

HTML 解析器的任务是将 HTML 标记解析成解析树。

HTML 语法定义

HTML 的词汇和语法在 W3C 组织创建的中进行了定义。当前的版本是 HTML4,HTML5 正在处理过程中。

非与上下文无关的语法

正如我们在解析过程的简介中已经了解到的,语法可以用 BNF 等格式进行正式定义。

很遗憾,所有的常规解析器都不适用于 HTML(我并不是开玩笑,它们可以用于解析 CSS 和 JavaScript)。HTML 并不能很容易地用解析器所需的与上下文无关的语法来定义。

有一种可以定义 HTML 的正规格式:DTD(Document Type Definition,文档类型定义),但它不是与上下文无关的语法。

这初看起来很奇怪:HTML 和 XML 非常相似。有很多 XML 解析器可以使用。HTML 存在一个 XML 变体 (XHTML),那么有什么大的区别呢?

区别在于 HTML 的处理更为“宽容”,它允许您省略某些隐式添加的标记,有时还能省略一些起始或者结束标记等等。和 XML 严格的语法不同,HTML 整体来看是一种“软性”的语法。

显然,这种看上去细微的差别实际上却带来了巨大的影响。一方面,这是 HTML 如此流行的原因:它能包容您的错误,简化网络开发。另一方面,这使得它很难编写正式的语法。概括地说,HTML 无法很容易地通过常规解析器解析(因为它的语法不是与上下文无关的语法),也无法通过 XML 解析器来解析。

HTML DTD

HTML 的定义采用了 DTD 格式。此格式可用于定义 SGML 族的语言。它包括所有允许使用的元素及其属性和层次结构的定义。如上文所述,HTML DTD 无法构成与上下文无关的语法。

DTD 存在一些变体。严格模式完全遵守 HTML 规范,而其他模式可支持以前的浏览器所使用的标记。这样做的目的是确保向下兼容一些早期版本的内容。最新的严格模式 DTD 可以在这里找到: www.w3.org/TR/html4/strict.dtd

DOM

解析器的输出“解析树”是由 DOM 元素和属性节点构成的树结构。DOM 是文档对象模型 (Document Object Model) 的缩写。它是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的接口。

解析树的根节点是“ Document ”对象。

DOM 与标记之间几乎是一一对应的关系。比如下面这段标记:

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

可翻译成如下的 DOM 树:

图 :示例标记的 DOM 树

和 HTML 一样,DOM 也是由 W3C 组织指定的。请参见 www.w3.org/DOM/DOMTR 。这是关于文档操作的通用规范。其中一个特定模块描述针对 HTML 的元素。HTML 的定义可以在这里找到: www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html 。

我所说的树包含 DOM 节点,指的是树是由实现了某个 DOM 接口的元素构成的。浏览器在具体的实现中会有一些供内部使用的其他属性。

解析算法

我们在之前章节已经说过,HTML 无法用常规的自上而下或自下而上的解析器进行解析。

原因在于:

  1. 语言的宽容本质。
  2. 浏览器历来对一些常见的无效 HTML 用法采取包容态度。
  3. 解析过程需要不断地反复。源内容在解析过程中通常不会改变,但是在 HTML 中,脚本标记如果包含 document.write ,就会添加额外的标记,这样解析过程实际上就更改了输入内容。

由于不能使用常规的解析技术,浏览器就创建了自定义的解析器来解析 HTML。

HTML5 规范详细地描述了解析算法 。此算法由两个阶段组成:标记化和树构建。

标记化是词法分析过程,将输入内容解析成多个标记。HTML 标记包括起始标记、结束标记、属性名称和属性值。

标记生成器识别标记,传递给树构造器,然后接受下一个字符以识别下一个标记;如此反复直到输入的结束。

图 :HTML 解析流程(摘自 HTML5 规范)

标记化算法

该算法的输出结果是 HTML 标记。该算法使用状态机来表示。每一个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。当前的标记化状态和树结构状态会影响进入下一状态的决定。这意味着,即使接收的字符相同,对于下一个正确的状态也会产生不同的结果,具体取决于当前的状态。该算法相当复杂,无法在此详述,所以我们通过一个简单的示例来帮助大家理解其原理。

基本示例 - 将下面的 HTML 代码标记化:

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

初始状态是数据状态。遇到字符 77d1f96db288e49541a70116b8bcc74c 字符。在此期间接收的每个字符都会附加到新的标记名称上。在本例中,我们创建的标记是 html 标记。

遇到 > 标记时,会发送当前的标记,状态改回 “数据状态” 。 6c04bd5ca3fcae76e30b72ad730ca86d 标记也会进行同样的处理。目前 html 和 body 标记均已发出。现在我们回到 “数据状态” 。接收到 Hello world 中的 H 字符时,将创建并发送字符标记,直到接收 36cc49f0c466276486e50c850b7e4956 中的 6d85211e57ac57e9bd4917f9d6886733 。然后将发送新的标记,并回到 “数据状态” 。 73a6ac4ed44ffec12cee46588e518a5e 输入也会进行同样的处理。

图 :对示例输入进行标记化

树构建算法

在创建解析器的同时,也会创建 Document 对象。在树构建阶段,以 Document 为根节点的 DOM 树也会不断进行修改,向其中添加各种元素。标记生成器发送的每个节点都会由树构建器进行处理。规范中定义了每个标记所对应的 DOM 元素,这些元素会在接收到相应的标记时创建。这些元素不仅会添加到 DOM 树中,还会添加到开放元素的堆栈中。此堆栈用于纠正嵌套错误和处理未关闭的标记。其算法也可以用状态机来描述。这些状态称为“插入模式”。

让我们来看看示例输入的树构建过程:

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

树构建阶段的输入是一个来自标记化阶段的标记序列。第一个模式是 “initial mode” 。接收 HTML 标记后转为 “before html” 模式,并在这个模式下重新处理此标记。这样会创建一个 HTMLHtmlElement 元素,并将其附加到 Document 根对象上。

然后状态将改为 “before head” 。此时我们接收“body”标记。即使我们的示例中没有“head”标记,系统也会隐式创建一个 HTMLHeadElement,并将其添加到树中。

现在我们进入了 “in head” 模式,然后转入 “after head” 模式。系统对 body 标记进行重新处理,创建并插入 HTMLBodyElement,同时模式转变为 “in body”

现在,接收由“Hello world”字符串生成的一系列字符标记。接收第一个字符时会创建并插入“Text”节点,而其他字符也将附加到该节点。

接收 body 结束标记会触发 “after body” 模式。现在我们将接收 HTML 结束标记,然后进入 “after after body” 模式。接收到文件结束标记后,解析过程就此结束。

图 :示例 HTML 的树构建

解析结束后的操作

在此阶段,浏览器会将文档标注为交互状态,并开始解析那些处于“deferred”模式的脚本,也就是那些应在文档解析完成后才执行的脚本。然后,文档状态将设置为“完成”,一个“加载”事件将随之触发。

您可以 在 HTML5 规范中查看标记化和树构建的完整算法

浏览器的容错机制

您在浏览 HTML 网页时从来不会看到“语法无效”的错误。这是因为浏览器会纠正任何无效内容,然后继续工作。

以下面的 HTML 代码为例:

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

在这里,我已经违反了很多语法规则(“mytag”不是标准的标记,“p”和“div”元素之间的嵌套有误等等),但是浏览器仍然会正确地显示这些内容,并且毫无怨言。因为有大量的解析器代码会纠正 HTML 网页作者的错误。

不同浏览器的错误处理机制相当一致,但令人称奇的是,这种机制并不是 HTML 当前规范的一部分。和书签管理以及前进/后退按钮一样,它也是浏览器在多年发展中的产物。很多网站都普遍存在着一些已知的无效 HTML 结构,每一种浏览器都会尝试通过和其他浏览器一样的方式来修复这些无效结构。

HTML5 规范定义了一部分这样的要求。WebKit 在 HTML 解析器类的开头注释中对此做了很好的概括。

解析器对标记化输入内容进行解析,以构建文档树。如果文档的格式正确,就直接进行解析。

遗憾的是,我们不得不处理很多格式错误的 HTML 文档,所以解析器必须具备一定的容错性。

我们至少要能够处理以下错误情况:

  1. 明显不能在某些外部标记中添加的元素。在此情况下,我们应该关闭所有标记,直到出现禁止添加的元素,然后再加入该元素。
  2. 我们不能直接添加的元素。这很可能是网页作者忘记添加了其中的一些标记(或者其中的标记是可选的)。这些标签可能包括:HTML HEAD BODY TBODY TR TD LI(还有遗漏的吗?)。
  3. 向 inline 元素内添加 block 元素。关闭所有 inline 元素,直到出现下一个较高级的 block 元素。
  4. 如果这样仍然无效,可关闭所有元素,直到可以添加元素为止,或者忽略该标记。

让我们看一些 WebKit 容错的示例:

使用了 0b9f73f8e206867bd1f5dc5957dbcb38 而不是 0c6dc11e160d3b678d68754cc175188a

有些网站使用了 0b9f73f8e206867bd1f5dc5957dbcb38 而不是 0c6dc11e160d3b678d68754cc175188a。为了与 IE 和 Firefox 兼容,WebKit 将其与 0c6dc11e160d3b678d68754cc175188a 做同样的处理。代码如下:

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

请注意,错误处理是在内部进行的,用户并不会看到这个过程。

离散表格

离散表格是指位于其他表格内容中,但又不在任何一个单元格内的表格。比如以下的示例:

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

WebKit 会将其层次结构更改为两个同级表格:

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

代码如下:

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

WebKit 使用一个堆栈来保存当前的元素内容,它会从外部表格的堆栈中弹出内部表格。现在,这两个表格就变成了同级关系。

嵌套的表单元素

如果用户在一个表单元素中又放入了另一个表单,那么第二个表单将被忽略。代码如下:

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

过于复杂的标记层次结构

代码的注释已经说得很清楚了。

示例网站 www.liceo.edu.mx 嵌套了约 1500 个标记,全都来自一堆 a4b561c25d9afb9ac8dc4d70affff419 标记。我们只允许最多 20 层同类型标记的嵌套,如果再嵌套更多,就会全部忽略。

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

放错位置的 html 或者 body 结束标记

同样,代码的注释已经说得很清楚了。

支持格式非常糟糕的 HTML 代码。我们从不关闭 body 标记,因为一些愚蠢的网页会在实际文档结束之前就关闭。我们通过调用 end() 来执行关闭操作。

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

所以网页作者需要注意,除非您想作为反面教材出现在 WebKit 容错代码段的示例中,否则还请编写格式正确的 HTML 代码。

CSS 解析

还记得简介中解析的概念吗?和 HTML 不同,CSS 是上下文无关的语法,可以使用简介中描述的各种解析器进行解析。事实上, CSS 规范定义了 CSS 的词法和语法 。

让我们来看一些示例:词法语法(词汇)是针对各个标记用正则表达式定义的:

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”是标识符 (identifier) 的缩写,比如类名。“name”是元素的 ID(通过“#”来引用)。

语法是采用 BNF 格式描述的。

ruleset  : selector [ ',' S* selector ]*    '{' S* declaration [ ';' S* declaration ]* '}' S*  ;selector  : simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?  ;simple_selector  : element_name [ HASH | class | attrib | pseudo ]*  | [ HASH | class | attrib | pseudo ]+  ;class  : '.' IDENT  ;element_name  : IDENT | '*'  ;attrib  : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*    [ IDENT | STRING ] S* ] ']'  ;pseudo  : ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]  ;

解释:这是一个规则集的结构:

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

div.error 和 a.error 是选择器。大括号内的部分包含了由此规则集应用的规则。此结构的正式定义是这样的:

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

这表示一个规则集就是一个选择器,或者由逗号和空格(S 表示空格)分隔的多个(数量可选)选择器。规则集包含了大括号,以及其中的一个或多个(数量可选)由分号分隔的声明。“声明”和“选择器”将由下面的 BNF 格式定义。

WebKit CSS 解析器

WebKit 使用解析器生成器,通过 CSS 语法文件自动创建解析器。正如我们之前在解析器简介中所说,Bison 会创建自下而上的移位归约解析器。Firefox 使用的是人工编写的自上而下的解析器。这两种解析器都会将 CSS 文件解析成 StyleSheet 对象,且每个对象都包含 CSS 规则。CSS 规则对象则包含选择器和声明对象,以及其他与 CSS 语法对应的对象。

图 :解析 CSS

处理脚本和样式表的顺序

脚本

网络的模型是同步的。网页作者希望解析器遇到 3f1c4e4b6b16bbbd69b2ee476dc4f83a 标记时立即解析并执行脚本。文档的解析将停止,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。此模型已经使用了多年,也在 HTML4 和 HTML5 规范中进行了指定。作者也可以将脚本标注为“defer”,这样它就不会停止文档解析,而是等到解析结束才执行。HTML5 增加了一个选项,可将脚本标记为异步,以便由其他线程解析和执行。

预解析

WebKit 和 Firefox 都进行了这项优化。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。

样式表

另一方面,样式表有着不同的模型。理论上来说,应用样式表不会更改 DOM 树,因此似乎没有必要等待样式表并停止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复,这样显然会产生很多问题。这看上去是一个非典型案例,但事实上非常普遍。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。而对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。

呈现树构建

在 DOM 树构建的同时,浏览器还会构建另一个树结构:呈现树。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是让您按照正确的顺序绘制内容。

Firefox 将呈现树中的元素称为“框架”。WebKit 使用的术语是呈现器或呈现对象。

呈现器知道如何布局并将自身及其子元素绘制出来。

WebKits RenderObject 类是所有呈现器的基类,其定义如下:

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}

每一个呈现器都代表了一个矩形的区域,通常对应于相关节点的 CSS 框,这一点在 CSS2 规范中有所描述。它包含诸如宽度、高度和位置等几何信息。

框的类型会受到与节点相关的“display”样式属性的影响(请参阅章节)。下面这段 WebKit 代码描述了根据 display 属性的不同,针对同一个 DOM 节点应创建什么类型的呈现器。

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

元素类型也是考虑因素之一,例如表单控件和表格都对应特殊的框架。

在 WebKit 中,如果一个元素需要创建特殊的呈现器,就会替换 createRenderer

方法。呈现器所指向的样式对象中包含了一些和几何无关的信息。

呈现树和 DOM 树的关系

呈现器是和 DOM 元素相对应的,但并非一一对应。非可视化的 DOM 元素不会插入呈现树中,例如“head”元素。如果元素的 display 属性值为“none”,那么也不会显示在呈现树中(但是 visibility 属性值为“hidden”的元素仍会显示)。

有一些 DOM 元素对应多个可视化对象。它们往往是具有复杂结构的元素,无法用单一的矩形来描述。例如,“select”元素有 3 个呈现器:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。如果由于宽度不够,文本无法在一行中显示而分为多行,那么新的行也会作为新的呈现器而添加。另一个关于多呈现器的例子是格式无效的 HTML。根据 CSS 规范,inline 元素只能包含 block 元素或 inline 元素中的一种。如果出现了混合内容,则应创建匿名的 block 呈现器,以包裹 inline 元素。

有一些呈现对象对应于 DOM 节点,但在树中所在的位置与 DOM 节点不同。浮动定位和绝对定位的元素就是这样,它们处于正常的流程之外,放置在树中的其他地方,并映射到真正的框架,而放在原位的是占位框架。

图 :呈现树及其对应的 DOM 树 ()。初始容器 block 为“viewport”,而在 WebKit 中则为“RenderView”对象。

构建呈现树的流程

在 Firefox 中,系统会针对 DOM 更新注册展示层,作为侦听器。展示层将框架创建工作委托给 FrameConstructor ,由该构造器解析样式(请参阅)并创建框架。

在 WebKit 中,解析样式和创建呈现器的过程称为“附加”。每个 DOM 节点都有一个“attach”方法。附加是同步进行的,将节点插入 DOM 树需要调用新的节点“attach”方法。

处理 html 和 body 标记就会构建呈现树根节点。这个根节点呈现对象对应于 CSS 规范中所说的容器 block,这是最上层的 block,包含了其他所有 block。它的尺寸就是视口,即浏览器窗口显示区域的尺寸。Firefox 称之为 ViewPortFrame ,而 WebKit 称之为 RenderView 。这就是文档所指向的呈现对象。呈现树的其余部分以 DOM 树节点插入的形式来构建。

请参阅 关于处理模型的 CSS2 规范 。

样式计算

构建呈现树时,需要计算每一个呈现对象的可视化属性。这是通过计算每个元素的样式属性来完成的。

样式包括来自各种来源的样式表、inline 样式元素和 HTML 中的可视化属性(例如“bgcolor”属性)。其中后者将经过转化以匹配 CSS 样式属性。

样式表的来源包括浏览器的默认样式表、由网页作者提供的样式表以及由浏览器用户提供的用户样式表(浏览器允许您定义自己喜欢的样式。以 Firefox 为例,用户可以将自己喜欢的样式表放在“Firefox Profile”文件夹下)。

样式计算存在以下难点:

  1. 样式 数据是一个超大的结构,存储了无数的样式属性,这可能造成内存问题。
  2. 如果 不进行优化,为每一个元素查找匹配的规则会造成性能问题。要为每一个元素遍历整个规则列表来寻找匹配规则,这是一项浩大的工程。选择器会具有很复杂的结构,这就会导致某个匹配过程一开始看起来很可能是正确的,但最终发现其实是徒劳的,必须尝试其他匹配路径。

    例如下面这个组合选择器:

    div div div div{  ...}
    这意味着规则适用于作为 3 个 div 元素的子代的 dc6dce4a544fdca2df29d5ac0ea9906b 。如果您要检查规则是否适用于某个指定的 dc6dce4a544fdca2df29d5ac0ea9906b

    元素,应选择树上的一条向上路径进行检查。您可能需要向上遍历节点树,结果发现只有两个 div,而且规则并不适用。然后,您必须尝试树中的其他路径。

  3. 应用 规则涉及到相当复杂的层叠规则(用于定义这些规则的层次)。

让我们来看看浏览器是如何处理这些问题的:

共享样式数据

WebKit 节点会引用样式对象 (RenderStyle)。这些对象在某些情况下可以由不同节点共享。这些节点是同级关系,并且:

  1. 这些元素必须处于相同的鼠标状态(例如,不允许其中一个是“:hover”状态,而另一个不是)
  2. 任何元素都没有 ID
  3. 标记名称应匹配
  4. 类属性应匹配
  5. 映射属性的集合必须是完全相同的
  6. 链接状态必须匹配
  7. 焦点状态必须匹配
  8. 任何元素都不应受属性选择器的影响,这里所说的“影响”是指在选择器中的任何位置有任何使用了属性选择器的选择器匹配
  9. 元素中不能有任何 inline 样式属性
  10. 不能使用任何同级选择器。WebCore 在遇到任何同级选择器时,只会引发一个全局开关,并停用整个文档的样式共享(如果存在)。这包括 + 选择器以及 :first-child 和 :last-child 等选择器。

Firefox 规则树

为了简化样式计算,Firefox 还采用了另外两种树:规则树和样式上下文树。WebKit 也有样式对象,但它们不是保存在类似样式上下文树这样的树结构中,只是由 DOM 节点指向此类对象的相关样式。

图 :Firefox 样式上下文树 ()

样式上下文包含端值。要计算出这些值,应按照正确顺序应用所有的匹配规则,并将其从逻辑值转化为具体的值。例如,如果逻辑值是屏幕大小的百分比,则需要换算成绝对的单位。规则树的点子真的很巧妙,它使得节点之间可以共享这些值,以避免重复计算,还可以节约空间。

所有匹配的规则都存储在树中。路径中的底层节点拥有较高的优先级。规则树包含了所有已知规则匹配的路径。规则的存储是延迟进行的。规则树不会在开始的时候就为所有的节点进行计算,而是只有当某个节点样式需要进行计算时,才会向规则树添加计算的路径。

这个想法相当于将规则树路径视为词典中的单词。如果我们已经计算出如下的规则树:

假设我们需要为内容树中的另一个元素匹配规则,并且找到匹配路径是 B - E - I(按照此顺序)。由于我们在树中已经计算出了路径 A - B - E - I - L,因此就已经有了此路径,这就减少了现在所需的工作量。

让我们看看规则树如何帮助我们减少工作。

结构划分

样式上下文可分割成多个结构。这些结构体包含了特定类别(如 border 或 color)的样式信息。结构中的属性都是继承的或非继承的。继承属性如果未由元素定义,则继承自其父代。非继承属性(也称为“重置”属性)如果未进行定义,则使用默认值。

规则树通过缓存整个结构(包含计算出的端值)为我们提供帮助。这一想法假定底层节点没有提供结构的定义,则可使用上层节点中的缓存结构。

使用规则树计算样式上下文

在计算某个特定元素的样式上下文时,我们首先计算规则树中的对应路径,或者使用现有的路径。然后我们沿此路径应用规则,在新的样式上下文中填充结构。我们从路径中拥有最高优先级的底层节点(通常也是最特殊的选择器)开始,并向上遍历规则树,直到结构填充完毕。如果该规则节点对于此结构没有任何规范,那么我们可以实现更好的优化:寻找路径更上层的节点,找到后指定完整的规范并指向相关节点即可。这是最好的优化方法,因为整个结构都能共享。这可以减少端值的计算量并节约内存。如果我们找到了部分定义,就会向上遍历规则树,直到结构填充完毕。

如果我们找不到结构的任何定义,那么假如该结构是“继承”类型,我们会在 上下文树 中指向父代的结构,这样也可以共享结构。如果是 reset 类型的结构,则会使用默认值。

如果最特殊的节点确实添加了值,那么我们需要另外进行一些计算,以便将这些值转化成实际值。然后我们将结果缓存在树节点中,供子代使用。

如果某个元素与其同级元素都指向同一个树节点,那么它们就可以共享 整个样式上下文

让我们来看一个例子,假设我们有如下 HTML 代码:

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

还有如下规则:

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

为了简便起见,我们只需要填充两个结构:color 结构和 margin 结构。color 结构只包含一个成员(即“color”),而 margin 结构包含四条边。形成的规则树如下图所示(节点的标记方式为“节点名 : 指向的规则序号”):

图 :规则树

上下文树如下图所示(节点名 : 指向的规则节点):

图 :上下文树

假设我们解析 HTML 时遇到了第二个 dc6dce4a544fdca2df29d5ac0ea9906b 标记,我们需要为此节点创建样式上下文,并填充其样式结构。

经过规则匹配,我们发现该 dc6dce4a544fdca2df29d5ac0ea9906b 的匹配规则是第 1、2 和 6 条。这意味着规则树中已有一条路径可供我们的元素使用,我们只需要再为其添加一个节点以匹配第 6 条规则(规则树中的 F 节点)。

我们将创建样式上下文并将其放入上下文树中。新的样式上下文将指向规则树中的 F 节点。

现在我们需要填充样式结构。首先要填充的是 margin 结构。由于最后的规则节点 (F) 并没有添加到 margin 结构,我们需要上溯规则树,直至找到在先前节点插入中计算过的缓存结构,然后使用该结构。我们会在指定 margin 规则的最上层节点(即 B 节点)上找到该结构。

我们已经有了 color 结构的定义,因此不能使用缓存的结构。由于 color 有一个属性,我们无需上溯规则树以填充其他属性。我们将计算端值(将字符串转化为 RGB 等)并在此节点上缓存经过计算的结构。

第二个 45a2772a6b6107b401db3c9b82c049c2 元素处理起来更加简单。我们将匹配规则,最终发现它和之前的 span 一样指向规则 G。由于我们找到了指向同一节点的同级,就可以共享整个样式上下文了,只需指向之前 span 的上下文即可。

对于包含了继承自父代的规则的结构,缓存是在上下文树中进行的(事实上 color 属性是继承的,但是 Firefox 将其视为 reset 属性,并缓存到规则树上)。例如,如果我们在某个段落中添加 font 规则:

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

那么,该段落元素作为上下文树中的 div 的子代,就会共享与其父代相同的 font 结构(前提是该段落没有指定 font 规则)。

在 WebKit 中没有规则树,因此会对匹配的声明遍历 4 次。首先应用非重要高优先级的属性(由于作为其他属性的依据而应首先应用的属性,例如 display),接着是高优先级重要规则,然后是普通优先级非重要规则,最后是普通优先级重要规则。这意味着多次出现的属性会根据正确的层叠顺序进行解析。最后出现的最终生效。

因此概括来说,共享样式对象(整个对象或者对象中的部分结构)可以解决问题和问题。Firefox 规则树还有助于按照正确的顺序应用属性。

对规则进行处理以简化匹配

样式规则有一些来源:

  • 外部样式表或样式元素中的 CSS 规则
    p {color:blue}
  • inline 样式属性及类似内容
    <p style="color:blue" />
  • HTML 可视化属性(映射到相关的样式规则)
    <p bgcolor="blue" />

后两种很容易和元素进行匹配,因为元素拥有样式属性,而且 HTML 属性可以使用元素作为键值进行映射。

我们之前在中提到过,CSS 规则匹配可能比较棘手。为了解决这一难题,可以对 CSS 规则进行一些处理,以便访问。

样式表解析完毕后,系统会根据选择器将 CSS 规则添加到某个哈希表中。这些哈希表的选择器各不相同,包括 ID、类名称、标记名称等,还有一种通用哈希表,适合不属于上述类别的规则。如果选择器是 ID,规则就会添加到 ID 表中;如果选择器是类,规则就会添加到类表中,依此类推。

这种处理可以大大简化规则匹配。我们无需查看每一条声明,只要从哈希表中提取元素的相关规则即可。这种优化方法可排除掉 95% 以上规则,因此在匹配过程中根本就不用考虑这些规则了 ()。

我们以如下的样式规则为例:

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

第一条规则将插入类表,第二条将插入 ID 表,而第三条将插入标记表。

对于下面的 HTML 代码段:

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

我们首先会为 p 元素寻找匹配的规则。类表中有一个“error”键,在下面可以找到“p.error”的规则。div 元素在 ID 表(键为 ID)和标记表中有相关的规则。剩下的工作就是找出哪些根据键提取的规则是真正匹配的了。例如,如果 div 的对应规则如下:

table div {margin:5px}

这条规则仍然会从标记表中提取出来,因为键是最右边的选择器,但这条规则并不匹配我们的 div 元素,因为 div 没有 table 祖先。

WebKit 和 Firefox 都进行了这一处理。

以正确的层叠顺序应用规则

样式对象具有与每个可视化属性一一对应的属性(均为 CSS 属性但更为通用)。如果某个属性未由任何匹配规则所定义,那么部分属性就可由父代元素样式对象继承。其他属性具有默认值。

如果定义不止一个,就会出现问题,需要通过层叠顺序来解决。

样式表层叠顺序

某个样式属性的声明可能会出现在多个样式表中,也可能在同一个样式表中出现多次。这意味着应用规则的顺序极为重要。这称为“层叠”顺序。根据 CSS2 规范,层叠的顺序为(优先级从低到高):

  1. 浏览器声明
  2. 用户普通声明
  3. 作者普通声明
  4. 作者重要声明
  5. 用户重要声明

浏览器声明是重要程度最低的,而用户只有将该声明标记为“重要”才可以替换网页作者的声明。同样顺序的声明会根据进行排序,然后再是其指定顺序。HTML 可视化属性会转换成匹配的 CSS 声明。它们被视为低优先级的网页作者规则。

特异性

选择器的特异性由 CSS2 规范 定义如下:

  • 如果声明来自于“style”属性,而不是带有选择器的规则,则记为 1,否则记为 0 (= a)
  • 记为选择器中 ID 属性的个数 (= b)
  • 记为选择器中其他属性和伪类的个数 (= c)
  • 记为选择器中元素名称和伪元素的个数 (= d)

将四个数字按 a-b-c-d 这样连接起来(位于大数进制的数字系统中),构成特异性。

您使用的进制取决于上述类别中的最高计数。例如,如果 a=14,您可以使用十六进制。如果 a=17,那么您需要使用十七进制;当然不太可能出现这种情况,除非是存在如下的选择器:html body div div p ...(在选择器中出现了 17 个标记,这样的可能性极低)。

一些示例:

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

规则排序

找到匹配的规则之后,应根据级联顺序将其排序。WebKit 对于较小的列表会使用冒泡排序,而对较大的列表则使用归并排序。对于以下规则,WebKit 通过替换“>”运算符来实现排序:

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

渐进式处理

WebKit 使用一个标记来表示是否所有的顶级样式表(包括 @imports)均已加载完毕。如果在附加过程中尚未完全加载样式,则使用占位符,并在文档中进行标注,等样式表加载完毕后再重新计算。

布局

呈现器在创建完成并添加到呈现树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排。

HTML 采用基于流的布局模型,这意味着大多数情况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下的顺序遍历文档。但是也有例外情况,比如 HTML 表格的计算就需要不止一次的遍历 ()。

坐标系是相对于根框架而建立的,使用的是上坐标和左坐标。

布局是一个递归的过程。它从根呈现器(对应于 HTML 文档的 100db36a723c770d327fc0aef2ce13b1 元素)开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的呈现器计算几何信息。

根呈现器的位置左边是 0,0,其尺寸为视口(也就是浏览器窗口的可见区域)。

所有的呈现器都有一个“layout”或者“reflow”方法,每一个呈现器都会调用其需要进行布局的子代的 layout 方法。

Dirty 位系统

为避免对所有细小更改都进行整体布局,浏览器采用了一种“dirty 位”系统。如果某个呈现器发生了更改,或者将自身及其子代标注为“dirty”,则需要进行布局。

有两种标记:“dirty”和“children are dirty”。“children are dirty”表示尽管呈现器自身没有变化,但它至少有一个子代需要布局。

全局布局和增量布局

全局布局是指触发了整个呈现树范围的布局,触发原因可能包括:

  1. 影响所有呈现器的全局样式更改,例如字体大小更改。
  2. 屏幕大小调整。

布局可以采用增量方式,也就是只对 dirty 呈现器进行布局(这样可能存在需要进行额外布局的弊端)。当呈现器为 dirty 时,会异步触发增量布局。例如,当来自网络的额外内容添加到 DOM 树之后,新的呈现器附加到了呈现树中。

图 :增量布局 - 只有 dirty 呈现器及其子代进行布局 ()。

异步布局和同步布局

增量布局是异步执行的。Firefox 将增量布局的“reflow 命令”加入队列,而调度程序会触发这些命令的批量执行。WebKit 也有用于执行增量布局的计时器:对呈现树进行遍历,并对 dirty 呈现器进行布局。

请求样式信息(例如“offsetHeight”)的脚本可同步触发增量布局。

全局布局往往是同步触发的。

有时,当初始布局完成之后,如果一些属性(如滚动位置)发生变化,布局就会作为回调而触发。

优化

如果布局是由“大小调整”或呈现器的位置(而非大小)改变而触发的,那么可以从缓存中获取呈现器的大小,而无需重新计算。

在某些情况下,只有一个子树进行了修改,因此无需从根节点开始布局。这适用于在本地进行更改而不影响周围元素的情况,例如在文本字段中插入文本(否则每次键盘输入都将触发从根节点开始的布局)。

布局处理

布局通常具有以下模式:

  1. 父呈现器确定自己的宽度。
  2. 父呈现器依次处理子呈现器,并且:
    1. 放置子呈现器(设置 x,y 坐标)。
    2. 如果有必要,调用子呈现器的布局(如果子呈现器是 dirty 的,或者这是全局布局,或出于其他某些原因),这会计算子呈现器的高度。
  3. 父呈现器根据子呈现器的累加高度以及边距和补白的高度来设置自身高度,此值也可供父呈现器的父呈现器使用。
  4. 将其 dirty 位设置为 false。

Firefox 使用“state”对象 (nsHTMLReflowState) 作为布局的参数(称为“reflow”),这其中包括了父呈现器的宽度。Firefox 布局的输出为“metrics”对象 (nsHTMLReflowMetrics),其包含计算得出的呈现器高度。

宽度计算

呈现器宽度是根据容器块的宽度、呈现器样式中的“width”属性以及边距和边框计算得出的。例如以下 div 的宽度:

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

将由 WebKit 计算如下(BenderBox 类,calcWidth 方法):

  • 容器的宽度取容器的 availableWidth 和 0 中的较大值。availableWidth 在本例中相当于 contentWidth,计算公式如下:
    clientWidth() - paddingLeft() - paddingRight()
    clientWidth 和 clientHeight 表示一个对象的内部(除去边框和滚动条)。
  • 元素的宽度是“width”样式属性。它会根据容器宽度的百分比计算得出一个绝对值。
  • 然后加上水平方向的边框和补白。

现在计算得出的是“preferred width”。然后需要计算最小宽度和最大宽度。

如果首选宽度大于最大宽度,那么应使用最大宽度。如果首选宽度小于最小宽度(最小的不可破开单位),那么应使用最小宽度。

这些值会缓存起来,以用于需要布局而宽度不变的情况。

换行

如果呈现器在布局过程中需要换行,会立即停止布局,并告知其父代需要换行。父代会创建额外的呈现器,并对其调用布局。

绘制

在绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。绘制工作是使用用户界面基础组件完成的。

全局绘制和增量绘制

和布局一样,绘制也分为全局(绘制整个呈现树)和增量两种。在增量绘制中,部分呈现器发生了更改,但是不会影响整个树。更改后的呈现器将其在屏幕上对应的矩形区域设为无效,这导致 OS 将其视为一块“dirty 区域”,并生成“paint”事件。OS 会很巧妙地将多个区域合并成一个。在 Chrome 浏览器中,情况要更复杂一些,因为 Chrome 浏览器的呈现器不在主进程上。Chrome 浏览器会在某种程度上模拟 OS 的行为。展示层会侦听这些事件,并将消息委托给呈现根节点。然后遍历呈现树,直到找到相关的呈现器,该呈现器会重新绘制自己(通常也包括其子代)。

绘制顺序

CSS2 规范定义了绘制流程的顺序 。绘制的顺序其实就是元素进入

的顺序。这些堆栈会从后往前绘制,因此这样的顺序会影响绘制。块呈现器的堆栈顺序如下:

  1. 背景颜色
  2. 背景图片
  3. 边框
  4. 子代
  5. 轮廓

Firefox 显示列表

Firefox 遍历整个呈现树,为绘制的矩形建立一个显示列表。列表中按照正确的绘制顺序(先是呈现器的背景,然后是边框等等)包含了与矩形相关的呈现器。这样等到重新绘制的时候,只需遍历一次呈现树,而不用多次遍历(绘制所有背景,然后绘制所有图片,再绘制所有边框等等)。

Firefox 对此过程进行了优化,也就是不添加隐藏的元素,例如被不透明元素完全遮挡住的元素。

WebKit 矩形存储

在重新绘制之前,WebKit 会将原来的矩形另存为一张位图,然后只绘制新旧矩形之间的差异部分。

动态变化

在发生变化时,浏览器会尽可能做出最小的响应。因此,元素的颜色改变后,只会对该元素进行重绘。元素的位置改变后,只会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。一些重大变化(例如增大“html”元素的字体)会导致缓存无效,使得整个呈现树都会进行重新布局和绘制。

呈现引擎的线程

呈现引擎采用了单线程。几乎所有操作(除了网络操作)都是在单线程中进行的。在 Firefox 和 Safari 中,该线程就是浏览器的主线程。而在 Chrome 浏览器中,该线程是标签进程的主线程。

网络操作可由多个并行线程执行。并行连接数是有限的(通常为 2 至 6 个,以 Firefox 3 为例是 6 个)。

事件循环

浏览器的主线程是事件循环。它是一个无限循环,永远处于接受处理状态,并等待事件(如布局和绘制事件)发生,并进行处理。这是 Firefox 中关于主事件循环的代码:

while (!mExiting)    NS_ProcessNextEvent(thread);

CSS2 可视化模型

画布

根据 CSS2 规范 ,“画布”这一术语是指“用来呈现格式化结构的空间”,也就是供浏览器绘制内容的区域。画布的空间尺寸大小

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。