ブラウザはどのようにRenderObjectを生成するのか

·
Cover for ブラウザはどのようにRenderObjectを生成するのか

RenderObjectとは?

スタイルマッチングが完了すると、完全なCSSスタイル内容を持つDOMツリーが得られます。しかし、このDOMツリー上の内容は、画面上に直接描画するためのデータとしては使用できません。そのため、次のステップでは、DOMツリーを基にして新しいツリー — RenderObjectツリーを生成する必要があります。

DOMとRenderObjectの対応関係

DOMツリーと比較して、RenderObjectツリーの各ノードは、DOMのStyle内のプロパティを画面に直接描画できる内容に計算します。各DOMが独自のElementクラスを持つのとは異なり、RenderObjectの種類はそれほど多くありません。ほとんどの場合、以下のものだけを見ることになります:

  • RenderInline
  • RenderBlockFlow
  • RenderFlexibleBox
  • RenderText
  • RenderImage

DOMからこれらのRenderObjectを生成するルールは以下の通りです:

DOM種類RenderObject種類
DOM内の純粋なテキストRenderText
displayがflexの要素RenderFlexibleBox
displayがblock、inline-blockの要素RenderBlockFlow
displayがinlineの要素RenderInline
要素RenderImage

匿名ボックスの生成ルール

例外的なケースがなければ、DOMとRenderObjectは1対1で対応しますが、実際の実装では、Web仕様で定義された特殊な動作により、このプロセスはそれほど単純ではありません。

以下のケースでは、RenderObjectとDOMは1対1の関係にはなりません:

  • Block、Flexコンテナ要素の直接の子要素がTextの場合
  • Block、Inline要素がinline-levelとblock-level要素の両方を含む場合
    • 特に、Inline要素がblock-level要素を含む場合、inline要素は2つ以上のRenderObjectに対応することになります

簡単にまとめると:匿名ボックスが生成されるすべてのシナリオで、RenderObjectとDOMは1対1で対応しません。

これらの匿名ボックスを生成する状況について詳しく見ていきましょう

Block、Flexコンテナ要素の直接の子要素がTextの場合

このケースでの匿名ボックスの戦略は比較的単純で、Textの外部に直接Block Boxをラップするだけです:

ただし、Flexコンテナ要素とBlockコンテナには違いがあります。Flexコンテナ要素は直接のText子要素を持つことができませんが、Blockは持つことができます:

Block、Inline要素がinline-levelとblock-level要素の両方を含む場合

Block要素の場合、事情は比較的単純です。直接の子要素にinline-levelとblock-levelの両方が含まれている場合、すべてのinline-level子要素を匿名Block Boxでラップするだけです:

Inline要素の場合、事情はより複雑になります。

まず、Inline要素は親要素のBlock Box(つまりBlock Container)を見つけ、自身を複数の「クローン」に分割し、これらの「クローン」とinline-level子要素を匿名Block Boxでラップします。

同時に、block-level子要素も匿名block boxでラップされ、Block Containerの直接の子要素として存在します(つまり「昇格」します):

n個のblock-level子要素がある場合、Inline要素はn+1個の「クローン」を持つことになります:

RenderObject生成モジュールの責務

  • DOMに基づいて対応するRenderObjectを生成する
  • 匿名RenderObjectを作成する

ブラウザでは匿名ボックスはどのように生成されるのか?

ブラウザでは、これらのステップは新しく生成されたRenderObjectを他のRenderObjectにマウントするプロセス中に発生します。

WebCoreでは、このコードロジックの部分はRenderTreeBuilder::attachInternal()にあります。Blinkでは、LayoutTreeBuilderForElement::CreateLayoutObject()にあります(より具体的には、LayoutBlockFlow / LayoutInline / LayoutBlockのAddChildメソッドにあります)。

このコードロジックの部分は以下のように簡単に説明できます:

  1. 現在の要素がinlineの場合
    1. 「すべての子要素がblock-level」の場合
      1. inline-level子要素を追加:子要素を最後の「クローン」に追加
      2. block-level子要素を追加:1.b.IIと同じ処理
    2. 「すべての子要素がinline-level」または「子要素なし」の場合
      1. inline-level子要素を追加:直接追加
      2. block-level子要素を追加:このブロックのblock containerを見つけ(注意:このブロックの直接の親要素でない可能性があり、複数の層にまたがる可能性があります)、このブロックより前のすべてのinline-levelボックス(親要素のものを含み、間にblock boxがないもの)を匿名block box(ここでは匿名block box 1と呼びます)でラップし、block containerの前のブロックの後に挿入します。同時に、現在のblock boxを匿名block box(ここでは匿名block box 2と呼びます)でラップし、匿名block box 2を匿名block box 1の後に挿入し、両方ともblock containerの子要素とします
  2. 現在の要素がblockの場合
    1. 「すべての子要素がinline-level」の場合:
      1. inline-level子要素を追加:直接追加
      2. block-level子要素を追加:前のすべてのinline boxをblock boxでラップ(BlinkコードはMakeChildrenNonInlineを参照)
    2. 「すべての子要素がblock」または「子要素なし」の場合:
      1. inline-level子要素を追加:inlineの外部に匿名block boxをラップ。すでにinlineをラップするための匿名blockがある場合は、その匿名blockに追加
        1. このinlineの前の要素が純粋なテキストの場合、テキストを匿名のinline blockでラップ
      2. block-level子要素を追加:直接追加
  3. 現在の要素がflexの場合
    1. 非テキスト子要素を追加:直接追加(flexコンテナの子要素のdisplayがinline、inline-blockの場合はblockに変更されます)
    2. テキスト要素を追加:テキストの外部にblock boxをラップ

DOMがどのようなRenderObjectツリーを生成するかを確認したい場合は、WebKitのDumpRenderTreeツールを使用して検証できます:

<path-to-webkit>/WebKitBuild/Debug/DumpRenderTree <ウェブページURL>

例えば、上記のDOM構造:

<div>
    <span>
        <span>inline level box</span>
        <div>block level box</div>
        <span>inline level box</span>
        <div>block level box</div>
        <span>inline level box</span>
        <div>block level box</div>
        <span>inline level box</span>
    </span>
</div>

は以下のような結果を出力します: