渲染樹與css解析詳細介紹

admin發表於2018-08-06

瀏覽器在構造DOM樹的同時也在構造著另一棵樹-Render Tree,與DOM樹相對應暫且叫它Render樹。

我們知道DOM樹為javascript提供了一些列的訪問介面(DOM API),但這棵樹是不對外的。

它的主要作用就是把HTML按照一定的佈局與樣式顯示出來,用到了CSS的相關知識。從MVC的角度來說,可以將render樹看成是V,dom樹看成是M,C則是具體的排程者,比HTMLDocumentParser等。

新概念Render樹:

[C++] 純文字檢視 複製程式碼
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)

a:3:{s:3:\"pic\";s:43:\"portal/201808/06/011102nq6qa6n30qozqoqo.png\";s:5:\"thumb\";s:0:\"\";s:6:\"remote\";N;}

具體顯示的時候,每一個renderer體現了一個矩形區塊的東西,即我們常說的CSS盒子模型的概念,它本身包含了一些幾何學相關的屬性,如寬度width,高度height,位置position等。每一個renderer還有一個很重要的屬性,就是如何顯示它,display。我們知道元素的display有很多種,常見的就有none,inline,block,inline-block....,不同的display它們之間到底有啥不同呢?我們看一下程式碼:

[C++] 純文字檢視 複製程式碼
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用到的所有詞彙定義規範如下:

[JavaScript] 純文字檢視 複製程式碼
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代表樣式中的class,name代表樣式中的id。

CSS用到的語法BNF格式的定義如下:

[C++] 純文字檢視 複製程式碼
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的時候,瀏覽器是為每一個構造樣式定義的。我們可能定義了極其多的樣式而且有各種不同的規則,那找到元素匹配的樣式規則是挺困難的。瀏覽器有多重演算法錯誤來實現計算工作,具體就不細分析了,一個元素最終經過計算可能匹配到了很多條樣式規則,他們之間存在一定的優先順序,從低到高有:

瀏覽器預設樣式

(1).使用者個性化瀏覽器設定。

(2).HTML開發者定義的一般樣式。

(3).HTML開發者定義的!important樣式。

(4).使用者個性化瀏覽器設定!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

舉例說明:

[CSS] 純文字檢視 複製程式碼
*             {}  /* 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。

相關文章