聽說你想寫個渲染引擎 - 樣式樹

大家好,我是微微笑的蝸牛,??。

上一篇文章中,我們解析出了總體的樣式表。

今天,我們來介紹如何生成樣式樹。

簡單來說,樣式樹就是確定 dom 樹中每個節(jié)點的樣式。在樣式表的基礎上,計算出和節(jié)點匹配的樣式規(guī)則,與之關聯(lián)。

樣式樹也是一顆樹,只不過多了樣式信息。那么如何根據(jù)樣式表計算出節(jié)點的樣式呢?

接下來,我們就來好好說說。

聲明節(jié)點的 css 樣式無外乎通過以下幾種方式:

  • 元素
  • id
  • class

而樣式表中的規(guī)則是包含這些信息的。

因此,主要任務在于:如何將節(jié)點聲明的信息和 css 規(guī)則進行匹配,得到所有滿足條件的規(guī)則。

數(shù)據(jù)結構

第一步,我們?nèi)匀幌瓤紤]如何定義數(shù)據(jù)結構。

因為節(jié)點需要關聯(lián)到對應的樣式,那么自然能想到數(shù)據(jù)結構中需要包含節(jié)點信息樣式信息,樣式信息以 map 來存儲。

此外,樣式樹同樣是樹狀結構,包含子節(jié)點。

// 樣式 map
typealias StyleMap = [String: Value]

struct StyleNode {
    // 節(jié)點
    var node: Node
    
    // 關聯(lián)的樣式
    var styleMap: StyleMap
    
        // 子節(jié)點
    var children: [StyleNode]
}

由于一個節(jié)點可能聲明多個規(guī)則,而不同規(guī)則中又可能會存在相同的屬性聲明。

由于規(guī)則中的選擇器存在優(yōu)先級。在匹配時,自然是應該選擇優(yōu)先級高的。

比如下面的栗子,同時有兩條規(guī)則設置了 width 的值,但根據(jù)優(yōu)先級,最后使用的應該是 .test 中的屬性值。

div {
    width: 100px;
}

.test {
    width: 200px;
}

<div class="test"></div>

最后所有匹配樣式的屬性都會放入 map 中。根據(jù) map 的特性,插入相同的 key,后者的值會覆蓋前者。

而對于相同的屬性名來說,我們需要保證高優(yōu)先級的屬性覆蓋低優(yōu)先級的屬性。

這樣一來,就要求高優(yōu)先級的屬性比低優(yōu)先級的后放入 map

所以,當?shù)玫剿衅ヅ湟?guī)則后,還需將規(guī)則按照從低到高的優(yōu)先級排序,保證高優(yōu)先級在后。

為了達到這個目的,定義如下結構,將優(yōu)先級和規(guī)則關聯(lián)起來,用于輔助排序。

// Specifity 同樣用于排序
typealias MatchedRule = (Specifity, Rule)

// Specifity 是上一篇文章中定義的三元組
// 用于選擇器排序,優(yōu)先級從高到低分別是 id, class, tag
typealias Specifity = (Int, Int, Int)

節(jié)點樣式匹配

因為樣式表中會存在多條規(guī)則都能匹配到某節(jié)點的情況。所以呢,確定某節(jié)點樣式的過程,需要遍歷整個樣式表。

單條規(guī)則匹配

首先,我們來看下單條規(guī)則的匹配。

從上篇文章中,可知:css 規(guī)則 = 選擇器列表 + 屬性列表。

只要能匹配選擇器列表中的某個選擇器,那么說明這條規(guī)則就是滿足條件的。因此,重心轉移到了選擇器的匹配上。

1. 選擇器匹配

選擇器的信息包括 tag、id、classes,而從節(jié)點數(shù)據(jù)中我們可以拿到同樣的信息。比如 id、class 可從屬性中獲取,tag 更是不在話下。

這樣一來,就好進行匹配了。不過,還需注意一點,選擇器中的 tag、id、classes 不一定有數(shù)據(jù)。

匹配規(guī)則如下:

  • 選擇器中如果有 tag,那么比對該 tag 和節(jié)點的 tag 是否相同。若不同,則表示不匹配。
  • 選擇器中如果有 id,那么比對該 id 和節(jié)點的 id 是否相同。若不同,則表示不匹配。
  • 選擇器中如果有 class,那么比對該 class 列表是否被節(jié)點聲明的 class 屬性完全包含。若不是,則表示不匹配。
  • 其他情況,則表示匹配。

注意,第三條 class 的匹配。需完全包含,也就是說選擇器中的 class 必須是節(jié)點聲明 class 的子集。

div.test1.test2 {}

// 完全包含
<div class="test1 test2 test3"></div>

單個選擇器匹配代碼如下:

// 節(jié)點的 id,tag,class 是否與選擇器 simpleSelector 匹配,若一個不匹配,則返回 false
func matchSelector(node: ElementData, simpleSelector: SimpleSelector) -> Bool {
    
    // tag,css 中存在 tag 且不相等
    if simpleSelector.tagName != nil && node.tagName != simpleSelector.tagName {
        return false
    }
    
    // id
    let id = node.getId()
    
    // css 中存在 id 且不相等
    if simpleSelector.id != nil && id != simpleSelector.id {
        return false
    }
    
    // class
    let classes = node.getClasses()
    let selectorClasses = simpleSelector.classes
    
    // 節(jié)點元素的 class 中全部包含 selector 中的 class
    for cls in selectorClasses {
        if !classes.contains(cls) {
            return false
        }
    }
    
    return true
}

這樣,單個選擇器的匹配就完成了。

2. 選擇器列表匹配

選擇器列表的匹配自然也水到渠成,循環(huán)遍歷列表,逐個判斷是否匹配。

當匹配到一條規(guī)則后,就可返回。因為在上一篇關于 css 解析的文章中,選擇器列表已經(jīng)是按照從高到低的優(yōu)先級排序,所以只需匹配到即可。

最后返回優(yōu)先級和規(guī)則的二元組,用于排序。

func matchRule(node: ElementData, rule: Rule) -> MatchedRule? {
    // 遍歷 rule 的 selectors
    for selector in rule.selectors {
        if case .Simple(let simpleSelector) = selector {
            
            // 如果匹配
            if matchSelector(node: node, simpleSelector: simpleSelector) {
                return (selector.specificity(), rule)
            }
        }
    }
    
    return nil
}

多條規(guī)則匹配

既然單條規(guī)則匹配已經(jīng)完成,那么多條的就簡單啦。

遍歷整個樣式表,判斷是否匹配即可,最后返回匹配的多條規(guī)則。

// 遍歷整個樣式表,找出匹配的規(guī)則
func matchRules(node: ElementData, styleSheet: StyleSheet) -> [MatchedRule] {
    
    let rules = styleSheet.rules.compactMap { (rule) -> MatchedRule? in
        let result = matchRule(node: node, rule: rule)
        return result
    }
    
    return rules
}

生成樣式 map

上面已經(jīng)得到了匹配的規(guī)則列表,這一步需要將規(guī)則中的所有屬性放入 map 中。

不過,且慢,還記得上邊提到的優(yōu)先級問題嗎?高優(yōu)先級需后放入。

所以,首先還得將規(guī)則列表按照從低到高的優(yōu)先級排序,保證最終屬性值的正確性。

代碼很簡單,如下所示:

// 生成樣式 map
func genStyleMap(node: ElementData, styleSheet: StyleSheet) -> StyleMap {
    var styleMap = StyleMap()
    
    // 獲取匹配的 rule
    var rules = matchRules(node: node, styleSheet: styleSheet)
    
    // 從低優(yōu)先級到高優(yōu)先級排序,這樣放入 map 中時高優(yōu)先級會覆蓋低優(yōu)先級
    rules.sort {
        $0.0 < $1.0
    }
    
    // 遍歷匹配 rule 的所有屬性聲明
    for (_, rule) in rules {
        let declarations = rule.declarations
        for declaration in declarations {
            // 逐個放入 map
            styleMap[declaration.name] = declaration.value
        }
    }
    
    return styleMap
}

生成樣式樹

最后一步,就是生成最終產(chǎn)物 —— 樣式樹。既然已經(jīng)能夠得到單個節(jié)點的樣式,那么對于樹狀結構來說,遞歸遍歷即可得到整棵樹的樣式。

不過有一點要注意,只有元素才存在樣式,文本節(jié)點是沒有的,生成一個空 map 給它。

// 生成樣式樹
func genStyleTree(root: Node, styleSheet: StyleSheet) -> StyleNode {
    
    var styleMap: StyleMap
    
    let nodeType = root.nodeType
    
    switch nodeType {
    
    // 文本節(jié)點無樣式
    case .Text(_):
        styleMap = [:]
    case .Element(let node):
                // 生成樣式 map
        styleMap = genStyleMap(node: node, styleSheet: styleSheet)
    }
   
    // 子節(jié)點遞歸生成關聯(lián)樣式
    let childrenStyleNodes = root.children.map { (child) -> StyleNode in
        genStyleTree(root: child, styleSheet: styleSheet)
    }
    
    return StyleNode(node: root, styleMap: styleMap, children: childrenStyleNodes)
}

測試代碼

// 樣式關聯(lián)處理
let styleProcessor = StyleProcessor()
let styleNode = styleProcessor.genStyleTree(root: root, styleSheet: styleSheet)
print(styleNode)

將 html 解析和 css 解析的結果作為輸入,便可得到樣式樹。

完整代碼可點此查看。

總結

這一篇文章,主要介紹了如何進行節(jié)點樣式的匹配,重點在于選擇器的匹配,最后輸出樣式樹。

下一篇,將介紹如何生成布局樹,也就是確定元素的位置,大小等。敬請期待~

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容