WebCore Rendering

本文翻譯自WebCore Rendering系列

本文是一系列博文,旨在幫助人們理解 WebCore 的渲染系統。當我完成這些文章我將發布這些文章,它們也可以通過網站的文檔區獲得。

DOM Tree

一個頁面將被解析成一個含有多個節點的樹,其被稱為文檔對象模型(DOM).所有節點的基類為Node

Node可以被細分為如下幾類,這些與渲染代碼相關的節點類型包括

  • Document:樹的根節點總是 document 。有三個 document 類,Document,HTMLDocument和SVGDocument。第一個是用於所有非 SVG 的 XML document 。第二個用於所有繼承自Document的 HTML document 。第三個用於所有繼承自Document的 SVG document 。
  • Elements:所有出現在HTML和XML源代碼中的標籤(tag)都會轉化為 element 。從渲染的角度來看, element 是一個帶有 tag 的節點,其可以被轉化(cast)為一個特定的子類,以便於在渲染時查詢必要的信息。
  • Text: element 中間出現的原始文本可以被轉化為 text 節點。這些 text 節點存儲了原始文本,渲染樹可以查詢這些節點以獲得其文本信息

Render Tree

渲染的核心在於 render tree , render tree 非常類似於DOM,因為其也是一個對象組成的樹,其中每個對象都對應於 document , elements 或者 text 節點。這個渲染樹可以包含與 DOM 無對應的額外的對象。

render tree 的基類是RenderObject, DOM 節點的RenderObject可以通過Node的renderer()方法獲得。

以下是遍歷render tree時常用的方法

RenderObject *firstChild() const;nRenderObject *lastChild() const;nRenderObject *previsousSibling() const;nRenderObject *nextSibling() const;n

下面是一個用於遍歷所有孩子節點的循環。這是 render tree 代碼中最常用的遍歷方法

for(RenderObject *child=firstChild();child;child=child->nextSibling()){n ...n}n

Creating the Render Tree

Render 樹的生成是通過一個在 DOM 樹上的 attachment 的操作完成的。當一個 document 被解析和添加 DOM 節點時, DOM 節點上一個叫做attach的方法被調用了

void attach()n

attach方法計算了DOM節點的樣式信息。如果CSS的display屬性為none或者其為一個display屬性為none的後代,那麼並不會生成renderer。節點的子類和CSS的display屬性一起來決定改為節點生成什麼樣的renderer。

attach是一個自頂向下的遞歸操作。一個父節點總是在其後代生成renderer之前生成renderer。

Destroying Render Tree

當DOM節點從document中移除或document被移除(比如當tab/window 被關閉了)時renderers就銷毀了。DOM節點上一個叫做detach的方法被調用以銷毀renderer。

detach是一個自底向上的遞歸操作。後代節點的銷毀總是在於父節點之前。

Accessing Style Information

在attach過程中,DOM查詢CSS以獲得元素的樣式信息。獲得的樣式信息存儲在一個叫做RenderStyle的對象中。

每一個WebKit支持的CSS屬性都可以通過該對象查詢。RenderStyle是一個引用計數對象。如果一個DOM節點生成了一個renderer,其將通過setStyle方法設置該renderer的樣式信息

void setStyle(RenderStyle* )n

這些renderer添加了對RenderStyle的一個引用,這樣就能維持樣式信息直到其重新獲得一個樣式或者被銷毀。

RenderStyle可以通過RenderObject的style方法來獲取

RenderStyle* style() constn

The CSS Box Model

RenderObject的一個主要子類就是RenderBox。該子類代表了符合CSS盒子模型的對象。這包括含有 border,padding,margins,width,height 的任何對象。現在一些沒有符合 CSS 盒子模型的對象(比如 SVG 對象)仍然是RenderBox的子類。這是一個錯誤,將在以後對 render tree 的代碼重構中進行修復。

該圖來自於CSS2.1標準,描述了CSS盒子。以下方法可以用來獲取 border/margin/padding/ 的值。除非為了獲得原始的樣式信息這裡不應該調用RenderStyle,因為RenderObject實際的計算值可能與其大有不同(特別是對於 tables ,其可以覆蓋 cells 的 padding 和重疊 cells 的 borders)。

int marginTop() const;nint marginBottom() const;nint marginLeft() const;nint marginRight() const;nnint paddingTop() const;nint paddingBottom() const;nint paddingLeft() const;nint paddingRight() const;nnint borderTop() const;nint borderBottom() const;nint borderLeft() const;nint borderRight() const;n

width和height方法給出了盒子中包含border的寬度和高度信息

int width() const;nint height() const;n

client box是盒子中除了borders和scrollbars的區域。其包含了padding。

int clientLeft() const { return borderLeft();}nint clientTop() const { return borderTop();}nint clientWidth() const;nint clientHeight() const;n

術語 content box 是用來描述 CSS 盒子中除去 borders 和 padding 的區域。

IntRect contentBox() const;nint contentWidth() const { return clientWidth() - paddingLeft() - paddingRigth();}nint contentHeight() const { return clientHeight() - paddingTop() - paddingBottom(); }n

當一個盒子擁有一個水平或垂直的滾動條,其被放在border和padding之間。一個滾動條的尺寸被從client width 和 client height 中除去了。滾動條不是content box 的一部分。滾動條區域的尺寸和當前滾動的位置都可以從RenderObject獲得。我將在單獨的章節講述滾動。

int scrollLeft() const;nint scrollTop() const;nint scrollWidth() const;nint scrollHeight() const;n

盒子也具有 x 和 y 坐標。這些位置是相對於其祖先,其用來決定盒子應該在哪擺放。這條規則有很多特殊情況,而且這也是 render tree 中最令人迷惑的地方。

int xPos() const;nint yPos() const;n

Blocks and Inlines

上節我們講述了 CSS 盒子的基本結構。本節將討論RenderBox的各個子類和 block 和 inline 的概念。

block flow 是一個用於包含行(例如段落)或者包含其他塊的框(box),其垂直排列。HTML 中的常見block flow 包括 p 和 div

inline flow 是一個對象,其用於作為行的一部分。HTML中常見的flow element 包括 a,b,ispan

在 WebCore 中,有三個renderer類,其涉及到block和inline flows。分別是RenderBlock 和 RenderInline 和它們的父類RenderFlow。

一個 inline flow 通過 display 屬性可以轉化為 block flow(反之亦然)。

div { display: inline }nspan { display: block}n

除了 block 和 inline flow,還有一種元素可以當做 block 或者 inline : 替換元素。 一個替換元素是一個元素,其並未在 CSS 中指明該如何渲染。這個對象內容該如何渲染交由元素自身決定。常見的替換元素包括images,form,iframes,plugins和applets。

一個替換元素可以作為block-level或者inline-level。當一個替換元素作為一個block,其垂直的進行排列就像其代表了自己的段落。當一個替換元素作為inline,其被視為一個段落中的一行的一部分。替換元素默認是inline。

表單控制(form control)是一個特例。它們作為替換元素,但是因為它們由引擎實現,表單實際上是作為RenderBlock的子類。因此,替換這個概念並不限制在一個單一的子類,並用RenderObject的一個比特位代表。isReplaced方法可以用來查詢一個對象是否為替換元素。

bool isReplaced() constn

Images,plugins,frames,applets都繼承自一個子類,其實現了替換元素的行為。該子類為RenderReplaced。

The Inline Block

CSS 中國最令人奇怪的是 inline-block 。Inline block 是block flow但是其做落於一行中(註:block flow強調其內部的block垂直排列,並沒有強調其自身如何排列)。事實上從外部來看它們就像替換元素一樣,但是從內部來看它們是block flow。CSS 的 display 屬性可以用來生成 inline block。如果詢問 Inline block 元素是否為替換元素,其會返回true(即調用該對象的isReplaced方法,會返回 true)。

div { display : inline-block}n

Tables

HTML中的Tables默認為block-level。但是其可以利用 CSS 的 display 屬性轉化為 inlines。

table { display : inline-table }n

從外面來看 inline-table 就像替換元素(事實上其isReplaced也返回true)。但是從內部來看其仍然為table。

在WebCore中RenderTable類代表了一個table。其繼承自RenderBlock,原因將在position該節講述。

Text

原始文本用RenderText類表示。Text 在WebCore中被視為inline,因為其被置於行內。

Getting Block and Inline Information

判斷block 還是inline 狀態最基本的方法是isInline。該方法詢問一個對象是否被作為行的一部分。其不關心該元素內部是什麼樣(如image,inline flow,inline block,inline-table)。

bool isInline() constn

一個最常見的錯誤是當考慮render tree時總是假設一個對象是認為 isInline 意味著一個對象是inline flow,text,或者為 inline replaced 元素。但是因為inline-block 和 inline-table存在,該方法對於這些對象也會返回true。

查詢一個對象是否為block還是inline flow,應該採用如下方法。

bool isInlineFlow() constnbool isBlockFlow() constn

這些方法實際上詢問的是對象的內部結構。一個inline-block實際上是block flow而非 inline flow。其外部是inline而內部是block flow。

我們可以通過如下方法查詢blocks 和 inlines的具體的類類型。

bool isRenderBlock() constnbool isRenderInline() constn

isRenderBlock 方法在定位時很有用,因為 block flow 和 table 都作為定位對象的容器。

查詢一個元素是否為inline block 或者 inline table。可以用如下方法。

bool isInlineBlockOrInlineTable() constn

Children of Block Flows

Block flow 有一個關於其孩子節點的簡單的不變式,其render tree必須遵守。該規則總結如下。

block flow 的所有 in-flow 的孩子必須都是block或者block flow的所有in-flow的孩子都必須是inlines

換句話說就是,一旦你排除了浮動和定位元素,render tree 中所有block flow 的孩子調用isInline時必須都返回true,或者都返回false。render tree 為了維護不變數有時會需要修改其結構。

childrenInline方法被用來查詢block flow的孩子是inline還是block

bool childrenInline() const n

The CSS Box Model

inline flow的所有孩子的有一個更為簡單的不變式。

inline flow 的所有 in flow 孩子必須都是 inlines

Anonymous Blocks

為了維護block flow 孩子的不變式(只有inline 孩子或者只有 block 孩子)。render tree 會構造一個匿名塊(anoymous blocks)。考慮如下代碼

<div>nSome textn<div>nSome more textn</div>n</div>n

上例中,外層的div含有兩個孩子,一些text 和一個div。第一個孩子是inline的。但是第二個孩子是block。因為這樣的組合違反了不變式,render tree會構造一個匿名塊去包含這個文本。該render tree因此成為如下這樣:

<div>n<anonymous block>nSome textn</anoymous block>n<div>nSome more textn</div>n</div>n

isAnoymousBlock方法可以用來查詢一個renderer是否為anymous block flow。

bool isAnonymousBlock() constn

當一個block有inline 孩子和一個block 對象其就會嘗試生成匿名塊去包裹所有的inlines。連續的inlines可以共用一個公共的匿名塊。因此匿名塊的數目儘可能小。RenderBlock有一個makeChildrenNonInline方法去完成上述操作。

void makeChildrenNonInline(RenderObject* insertionPoint)n

Blocks inside Inline Flows

HTML中你能見到最難以忍受的就是將一個block放在inline flow中。如下所示:

<i>Italic only <b> italic and boldn<div>nWow,a block!n</div>n<div>nWow another blockn</div>nMore italic and bold text</b>nMore italic text</i>n

上述的兩個div違反了所有的bold元素的孩子都是inline這個不變數。render tree必須執行一個非常複雜的步驟去修復自身。需要構造三個匿名塊。第一個塊包含所有的div之前的inlines。第二個匿名塊包裹divs,第三個匿名塊包含剩下的div之後的inlines

<anonymous pre block>n<i>Italic only <b>italic and bold</b</i>n</anonymous pre block>n<anonymous middle block>n<div>nWow, a block!n</div>n<div>nWow, another block!n</div>n</anonymous middle block>n<anonymous post block>n<i><b>More italic and bold text</b> More italic text</i>n</anonymous post block>n

注意到bold和italic renderers必須拆分為兩個render對象,因為他們在匿名塊之前和之後。對於bold元素,其孩子先在block之前,然後在block內,接著在Block之後。render tree通過一個continuation鏈連接這些對象。

RenderFlow* continuation() constnbool isInlineContinuation() constn

在block之前的bold renderer可以通過b元素的renderer()方法獲得,該renderer將中間的匿名塊作為其continuation(),中間匿名塊將第二個bold renderer作為其continuation。這樣代碼就可以檢查代表DOM元素的renderers的操作就比較簡單了。

執行遞歸切割inline flow和生成continuation鏈的方法是splitFlow。

Layout Basics

當renderes被生成並加入到樹中時,其並不包含position或者size信息。決定盒子的位置和大小的過程叫做layout。所有的renderer多有一個layout方法。

void layout()n

Layout 是一個遞歸的操作。一個叫做FrameView的類代表了document中的視圖信息,其也含有一個layout方法。frameview負責管理renderer tree的布局。

FrameView可以執行兩種布局操作。第一種(也是最常見的)就是整個render tree的布局。此時render tree的根節點調用其layout方法,然後整個render tree 都得到更新。第二種layout類型是render tree的局部更新。第二種用在部分子樹的更新不影響全局的情況下。如今subtree layout僅被用於text fields(但是可能在將來被用於 帶有overflow:auto屬性的block或者類似結構)。

Dirty Bits

Layout有一個dirty bit 系統去決定一個對象是否需要layout。當有新的renderers插入樹里時,它們dirty自己和其祖先鏈中的相關連接。有三個單獨的bits在render tree 中使用。

bool needsLayout() const{n return m_needsLayout || m_normalChildNeedsLayout ||n m_postChildNeedsLayout;n}nbool selfNeedsLayout() const{n return m_needsLaytout;n}nbool postChildNeedsLayout() const{n return m_possChildNeedsLayout;n}nbool normalChildNeedsLayout() const { return m_normalChildNeedsLayout; }n

當renderer本身是dirty時使用第一位,可以通過selfNeedsLayout進行查詢,當該bit被置true,相關的祖先renderers設置對應的bits表示它們有個dirty孩子。置為的bit類型取決於之前變髒的link的定位狀態,posChildNeedsLayout用來表明一個定位的孩子dirted,normalChildNeedsLayout被用來指示一個in flow孩子變髒了。通過區別這兩種孩子layout可以在只有定位元素移動的情況下進行優化。

The Containing Block

」相關聯的祖先鏈(the relevant ancestor)「指的是什麼?當一個對象被標記為需要進行layout,被dirted的祖先鏈基於CSS的包含塊概念(containing block)。包含塊也被用來為孩子建立坐標。Renderers有xPos和yPos坐標,這些都是相對於包含塊。那麼到底什麼事包含塊

這就是包含塊的定義

從WebCore render tree角度來講,我所理解的包含塊如下

一個renderer的包含塊是負責決定renderer的位置的祖先k。

換句話來說,當render tree 發生layout,該render負責定位所有以它作為包含塊的renderers。

render tree的根元素為RenderView,該類根據CSS2.1規範對應於初始包含快(initial containing block)。這也是調用Document的render()返回的render。

初始包含塊總是與視口尺寸大小相同。在桌面瀏覽器里,這就是瀏覽器窗口的可見區域。其也總是被置於坐標(0,0)處。下圖展現了包含塊在document的定位。黑色帶有邊框的盒子代表了RenderView,灰色的盒子代表了整個document。

如果滾動document,那麼初始包含快將會隨著移動。其總是在document的上部且與視口大小相同。人們總覺得疑惑的地方是他們以為初始包含塊應該脫離文檔並為視口的一部分。

這是CSS2.1中初始包含塊的相關規範

這些規則可以總結如下:

  • renderer的root元素(如元素)總是把RenderView作為其包含塊。

  • 如果renderer的CSS position為relative或者static,那麼其包含塊為最近的block-level祖先。

  • 如果renderer 的 CSS position為fixed,那麼其包含塊為RenderView,技術上來說RenderView並不作為視口,因此RenderView必須調整fixed 定位的元素來應對document的滾動位置。這種情況下可以簡單的把RenderView當作視口的包含塊。

  • 如果一個renderer的position為absolute,那麼是最近的非static定位的block-level祖先。如果不存在這樣的祖先,那麼包含塊為RenderView。

    render tree有兩個方便的方法用於查詢一個對象的position是否為absolute或者fixed或relative。

    bool isPositioned() const;nbool isRelPositioned() const;n

    代碼中術語positioned指代absolute和fixed對象。術語relPositioned指代relative 定位對象。

    render tree 有一個方法來獲取renderer的包含塊。

    RenderBlock* containingBlock() constn

    當一個對象被標註為需要layout,其遍container chain,設置normalChildNeedsLayoutbit或者posChildNeedsLayoutbit。鏈中的之前連接的isPositioned狀態決定了該設置哪個bit位。container chain 大致對應於containing block chain,儘管中間的inlines 依舊dirted。因為該差別,一個方法叫做container替代containBlock。

    RenderObject* container() constn

    所有的layout方法以setNeedsLayout(false)結尾,這樣做的原因在於在離開 layout方法前清空renderer 的dirty bit,因此未來layout調用不會誤以為該對象仍然dirty。

Anatomy of a Layout Method

layout 方法類似於下面

void layout()n{n ASSERT(needsLayout());n //Determin the width and horizontal margins of this objectn ...nn for(RenderObject* child = firstChild();child;child=child- >nextSibling()){n //Determine if the child needs to get a relayout despite the bit not being set.n // place the childn child->layoutIfNeeded()n}n// now the intrinsic height of the object is known because the children are placed n// Determine the final heightn...nsetNeedsLayout(false);n}n

我們將在下節詳細闡述layout方法。

Absolute/Fixed and Relative Positioning

CSS的position屬性用來相對於對象的包含塊進行定位。其含有四個值:static,absolute,fixed,relative。static 定位是默認的定位方式,意味著該對象該對象利用常規的block和line layout規則進行定位。

Relative Positionning

relative定位和static定位十分類似除了其CSS的left,top,right,bottom屬性可以用來移動對象。isRelPositioned方法可以用來查詢一個renderer是否為relative定位

bool isRelPositioned() constn

其偏移屬性也可以通過下列方法獲得

int relativePositionOffsetX() const;nint relativePositionOffsetY() const;n

relative 定位只不過是paint-time translation。就layout而言,該對象仍然處於原來的地方。下例利用relative偏移了行的一部分。如你所視,這些行就像那個對象仍然處在原位置。(註:這裡揭示了relative定位與其他定位的顯著不同之處,relative定位並不改變layout,也即改變relative位置並不會觸發reflow而只會發生repaint,repaint效率顯著優於reflow)。

<div stylex="border:5px solid black;padding:20px;width:300px">nHere is a line of text.n<span stylex="position:relative:top:-10px;background-color:#eeeeee">nThis part is shifted<br> up a bit.n</span>nbut the rest of the line is in its original position.n</div>n

Absolute and Fixed Positioning

fixed定位對象的定位相對於視口。absolute定位對象相對於包含塊,即最近的position非static的祖先塊。如果不存在這樣的祖先,那麼使用初始包含塊(the RenderView)

isPositioned可以用來判斷一個renderer是absolute或者為fixed定位。

bool isPositioned() constn

當一個對象是absolute/fixed定位,其變為block-level。即使其CSS display屬性設置為inline或者inline-block/table,一旦一個對象被定位後,其display就變為block-level。isInline對於定位元素總是返回false。

RenderStyle可以得出display值,有時候當需要原始的display屬性時,可以調用如下屬性獲得其display屬性

EDisplay() display() const;nEDisplay() originalDisplay() const;n

The Positioned Object List

每個塊都有一個定位對象列表,其包含了以他作為包含塊的所有的absolute/fixed定位的renderers。該塊負責定位所有這些定位元素。下列方法可以用來管理定位對象列表。

void insertPositionedObject(RenderObject*)nvoid removePositionedObject(RenderObject*)n

layoutOnlyPositionedObjects方法被用來定位positioned對象。如果只有定位對象發生改變,那麼該方法返回true。這表示layout方法不必layout所有的常規孩子,可以早一點返回。

bool layoutOnlyPositionedObjectsn

layoutPositionedObejcts方法負責定位所有的定位對象,其含有一個boolean參數來表示是否需要對所有對象進行relayout。在某些情況下relayout是必須的。

bool layoutPositionedObjects(bool relayoutChildren)n

Coordinates of Positioned Objects

定位元素的坐標是相對於包含塊的padding邊緣的。例如下例指明了絕對定位對象的left和top坐標為(0,0)會導致對象被放在包含塊的左上角。

<div stylex="position:relative;border:5px solid black;width:300px;height:300px;">n<div stylex="position:absolute;width:200px;height:200px;background-color:purple"></div>n</div>n

在WebCore中,坐標位置總是相對於border邊緣,所以上例中對象的坐標為(5,5)。

當在CSS中忽略某坐標,WebCore必須為定位對象決定一個合適的位置。CSS對此有一系列複雜的規則,我們將在未來詳細討論。

Inline Relative Positioned Containers

一個relative 定位的inline是可能為其後代定位元素充當包含塊的。這是另一個極端複雜的例子。暫時我們只需知道可能存在這種情況,因此代碼必須處理好這種情況。

Float

float是一個意圖移動到一個段落的左側或右側的render。段落里的內容都會避讓float對象。

HTML中有對應結構表明float行為。例如image的align屬性可以用來浮動image

<img align = left src="...">n

一個浮動元素可以佔據多行。本例中,即使浮動元素在一個段落中聲明,其也可能突出到段落到下一個段落。

因為floats可以影響到多個blocks,WebCore利用block flows的一個float對象列表來追蹤所有的入侵到該block的浮動的renderers。一個浮動元素因此可以在多個block flow的浮動列表裡。Line布局必須要考慮到float的位置,以便能收縮自身來避讓float元素。通過浮動對象列表,block可以了解其需要避讓哪些floats。

一個浮動對象列表包含了如下數據結構:

struct FloatingObject{n enum Type{n FloatLeft,n FloatRight,n };n FloatingObject(Type type)n :node(0),n startY(0),n endY(0),n left(0),n width(0),n m_type(type),n ,noPaint(false)n {n```}nType type() { nreturn static_cast<type>(m_type)n}n RenderObject* node;n int startY;n int endY;n int left;n int width;n unsigned m_type:1;n bool noPaint:1;n}n

如你所視,該結構包含了一個矩形框(top,bottom,left,width)。每個列表項都含有其位置和尺寸(其獨立於float對象的位置和尺寸)的原因在於這些坐標都是相對於持有對象列表的對象的。這樣使得便於查詢每個block都可以輕易的查詢基於自己坐標空間float的位置,而不用做一系列轉換。

另外float的margins也被存儲在float列表項里,因為行框不僅需要避讓box的border也需要避讓其margin。這對於float對象很重要,這使得其可以在與行框之間留有空隙。

以下方法作用於floating對象列表上:

void insertFloatingObject(RenderObject*);nvoid removeFloatingObject(RenderObject*);nvoid clearFloats();nvoid positionNewFloats();n

前兩個函數自不必說,它們用於在列表中插入或刪除特定的float對象。clearFloats將會清空float對象列表。

當一個對象插入到列表裡時,其是未定位的(unpositioned)。其垂直坐標被設置為-1。positionNewFloats方法被用來擺放所有的floats。CSS有一系列關於floats該如何擺放的規則。該方法保證floats按照上述規則擺放。

floats有許多幫助函數。當我講到block 和line 布局時我會進一步闡釋它們。

Clearance

CSS提供了一個方法來指明對象應該處於所有的浮動對象之下,或者所有的左浮動對象之下,或所有的右浮動對象之下。clear屬性指明了這些行為,其值可以為none,left,right或both。

本段落處於上面的float對象之下,是因為使用了clear:left。clearance在block和line布局時被計算得到。clear屬性也可以用於float對象,以確保其處於所有之前的浮動對象之下(當然這裡只left,right或者both)。


推薦閱讀:

從URL輸入到頁面展現發生了什麼?
bootstrap 用來構建大型互聯網網站前端布局可行性如何?
我的 2017 年技術回顧

TAG:CSS | WebKit | 前端开发 |