大家好,我是微微笑的蝸牛,??。
上一篇文章中,我們解析出了總體的樣式表。
今天,我們來介紹如何生成樣式樹。
簡單來說,樣式樹就是確定 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é)點樣式的匹配,重點在于選擇器的匹配,最后輸出樣式樹。
下一篇,將介紹如何生成布局樹,也就是確定元素的位置,大小等。敬請期待~