瀏覽器在構造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。
對於介面設計來說,一個頁面最難搞的應該就是排版布局了,內容也比較多,我們下文進行說明