瀏覽器在構造DOM樹的同時也在構造着另一棵樹-Render Tree,與DOM樹相對應暫且叫它Render樹吧,我們知道DOM樹為javascript提供了一些列的訪問接口(DOM API),但這棵樹是不對外的。它的主要作用就是把HTML按照一定的布局與樣式顯示出來,用到了CSS的相關知識。從MVC的角度來說,可以將render樹看成是V,dom樹看成是M,C則是具體的調度者,比HTMLDocumentParser等。
新概念Render樹
每一個Render樹的節點稱之為renderer或者render object,查看WEBKIT的源代碼我們可以發現Renderer一個基礎的類定義,這個類是所有renderer對象的基類。
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 }
從中我們可以發現renderer包含了一個dom對象以及為其計算好的樣式規則,提供了布局以及顯示方法。具體效果圖如下:(firefox的Frames對應renderers,content對應dom)
具體顯示的時候,每一個renderer體現了一個矩形區塊的東西,即我們常說的CSS盒子模型的概念,它本身包含了一些幾何學相關的屬性,如寬度width,高度height,位置position等。每一個renderer還有一個很重要的屬性,就是如何顯示它,display。我們知道元素的display有很多種,常見的就有none,inline,block,inline-block....,不同的display它們之間到底有啥不同呢?我們看一下代碼:
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源碼了,上面只是列出了片段。
DOM樹與Render樹
可以這么說,沒有DOM樹就沒有Render樹,但是它們之間可不是簡單的一對一的關系。我們已經知道了render樹是用於顯示的,那不可見的元素當然不會在這棵樹中出現了,譬如<header>,您還能想到哪些呢?除此之外,diplay等於none的也不會被顯示在這棵樹里頭,但是visibility等於hidden的元素是會顯示在這棵樹里頭的,可以自己想一下為什么。說了這么多render樹,我們還沒見一下它的真容呢,它到底會是個什么模樣呢?我們看一下圖。
與DOM對象類型很豐富啊,什么head,title,div,而Render樹相對來說就比較單一了,畢竟它的職責就是為了以后的顯示渲染用嘛。從上圖我們還可以看出,有些DOM元素沒有對應的renderer,而有些DOM元素卻對應了好幾個renderer,對應多個renderer的情況是普遍存在的,就是為了解決一個renderer描述不清楚如何顯示出來的問題,譬如select元素,我們就需要三個renderer,one for the display area, one for the drop down list box and one for the button。
上圖中還有一種關系未可看出,即renderer與dom元素的位置也可能是不一樣的。說的就是那些添加了float:ETC或者position:absolute的元素,因為它們脫離了正常的文檔流順序,構造Render樹的時候會針對它們實際的位置進行構造。
DOM樹可能會被我們隨時更新,不僅限於解析階段,譬如$elment.append啦或者$elment.addClass啦,我們看到頁面立即進行了顯示刷新,瀏覽器針對這種情況進行了相關處理。Dom樹的根節點我們知道是doument,Render樹的根節點不同瀏覽器可能有不同的叫法,webkit叫它RenderView,firefox叫它ViewPortFrame。
CSS的解析
CSS用到的所有詞匯定義規范如下:
comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num [
0
-9
]+|[
0
-9
]*
"."
[
0
-9
]+
nonascii [\
200
-\
377
]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z
0
-9
-]|{nonascii}|{escape}
name {nmchar}+
ident {nmstart}{nmchar}*
|
注:ident代表樣式中的class,name代表樣式中的id。
CSS用到的語法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*]
')'
]
;
|
樣式計算
每個HTML元素上,我們可能定義了很多不同類型的樣式,如字體啦,顏色啦,布局啦等等。即使元素上不被我們定義樣式,瀏覽器或者用戶個性設置也會為它默認創造一些樣式。
樣式計算一項極其復雜的過程,我們定義樣式的時候可以采用類似類的定義方式為一批元素設置樣式,但是解析構造renderer的時候,瀏覽器是為每一個構造樣式定義的。我們可能定義了極其多的樣式而且有各種不同的規則,那找到元素匹配的樣式規則是挺困難的。瀏覽器有多重算法錯誤來實現計算工作,具體就不細分析了,一個元素最終經過計算可能匹配到了很多條樣式規則,他們之間存在一定的優先順序,從低到高有:
- 瀏覽器默認樣式
- 用戶個性化瀏覽器設置
- HTML開發者定義的一般樣式
- HTML開發者定義的!important樣式
- 用戶個性化瀏覽器設置!important樣式
更詳細的優先計算公式
- count 1 if the declaration is from is a 'style' attribute rather than a rule with a selector, 0 otherwise (= a)
- count the number of ID attributes in the selector (= b)
- count the number of other attributes and pseudo-classes in the selector (= c)
- count the number of element names and pseudo-elements in the selector (= d)
具體可見http://www.w3.org/TR/CSS2/cascade.html#specificity
舉例說明
* {} /* 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 */
布局
上面確定了renderer的樣式規則后,然后就是重要的顯示因素布局了。當renderer構造出來並添加到render樹上之后,它並沒有位置跟大小信息,為它確定這些信息的過程,我們就稱之為布局。HTML采用了一種流式布局的布局模型,從上到下,從左到右順序布局,布局的起點是從render樹的根節點開始的,對應dom樹的document節點,其初始位置為0,0,詳細的布局過程為: 每個renderer的寬度由父節點的renderer確定。 父節點遍歷子節點,確定子節點的位置(x,y),調用子節點的layout方法確定其高度。 父節點根據子節點的height,margin,padding確定自身的自身的高度。
為了避免因為局部小范圍的DOM修改或者樣式改變引起整個頁面整體的布局重新構造,瀏覽器采用了一種dirty bit system的技術,使其盡可能的只改變元素本身或者包含的子元素的布局。當然有些情況無可避免的要重新構造整個頁面的布局,如適合於整體的樣式的改變影響了所有renderer,如body{font-size:111px} 字體大小發生了改變,還有一種情況就是瀏覽器窗口進行了調整,resize。
對於界面設計來說,一個頁面最難搞的應該就是排版布局了,內容也比較多,我們下文進行說明