大家好,我是微微笑的蜗牛,🐌。
上一篇文章中,我们解析出了总体的样式表。
今天,我们来介绍如何生成样式树。
简单来说,样式树就是确定 dom 树中每个节点的样式。在样式表的基础上,计算出和节点匹配的样式规则,与之关联。
样式树也是一颗树,只不过多了样式信息。那么如何根据样式表计算出节点的样式呢?
接下来,我们就来好好说说。
声明节点的 css 样式无外乎通过以下几种方式:
- 元素
- id
- class
而样式表中的规则是包含这些信息的。
因此,主要任务在于:如何将节点声明的信息和 css 规则进行匹配,得到所有满足条件的规则。
数据结构
第一步,我们仍然先考虑如何定义数据结构。
因为节点需要关联到对应的样式,那么自然能想到数据结构中需要包含节点信息和样式信息,样式信息以 map 来存储。
此外,样式树同样是树状结构,包含子节点。
// 样式 map
typealias StyleMap = [String: Value]
struct StyleNode {
// 节点
var node: Node
// 关联的样式
var styleMap: StyleMap
// 子节点
var children: [StyleNode]
}
由于一个节点可能声明多个规则,而不同规则中又可能会存在相同的属性声明。
由于规则中的选择器存在优先级。在匹配时,自然是应该选择优先级高的。
比如下面的栗子,同时有两条规则设置了 width
的值,但根据优先级,最后使用的应该是 .test
中的属性值。
div {
width: 100px;
}
.test {
width: 200px;
}
<div class="test"></div>
最后所有匹配样式的属性都会放入 map 中。根据 map 的特性,插入相同的 key,后者的值会覆盖前者。
而对于相同的属性名来说,我们需要保证高优先级的属性覆盖低优先级的属性。
这样一来,就要求高优先级的属性比低优先级的后放入 map。
所以,当得到所有匹配规则后,还需将规则按照从低到高的优先级排序,保证高优先级在后。
为了达到这个目的,定义如下结构,将优先级和规则关联起来,用于辅助排序。
// Specifity 同样用于排序
typealias MatchedRule = (Specifity, Rule)
// Specifity 是上一篇文章中定义的三元组
// 用于选择器排序,优先级从高到低分别是 id, class, tag
typealias Specifity = (Int, Int, Int)
节点样式匹配
因为样式表中会存在多条规则都能匹配到某节点的情况。所以呢,确定某节点样式的过程,需要遍历整个样式表。
单条规则匹配
首先,我们来看下单条规则的匹配。
从上篇文章中,可知:css 规则 = 选择器列表 + 属性列表。
只要能匹配选择器列表中的某个选择器,那么说明这条规则就是满足条件的。因此,重心转移到了选择器的匹配上。
1. 选择器匹配
选择器的信息包括 tag、id、classes,而从节点数据中我们可以拿到同样的信息。比如 id、class 可从属性中获取,tag 更是不在话下。
这样一来,就好进行匹配了。不过,还需注意一点,选择器中的 tag、id、classes 不一定有数据。
匹配规则如下:
- 选择器中如果有 tag,那么比对该 tag 和节点的 tag 是否相同。若不同,则表示不匹配。
- 选择器中如果有 id,那么比对该 id 和节点的 id 是否相同。若不同,则表示不匹配。
- 选择器中如果有 class,那么比对该 class 列表是否被节点声明的 class 属性完全包含。若不是,则表示不匹配。
- 其他情况,则表示匹配。
注意,第三条 class 的匹配。需完全包含,也就是说选择器中的 class 必须是节点声明 class 的子集。
div.test1.test2 {}
// 完全包含
<div class="test1 test2 test3"></div>
单个选择器匹配代码如下:
// 节点的 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
// 节点元素的 class 中全部包含 selector 中的 class
for cls in selectorClasses {
if !classes.contains(cls) {
return false
}
}
return true
}
这样,单个选择器的匹配就完成了。
2. 选择器列表匹配
选择器列表的匹配自然也水到渠成,循环遍历列表,逐个判断是否匹配。
当匹配到一条规则后,就可返回。因为在上一篇关于 css 解析的文章中,选择器列表已经是按照从高到低的优先级排序,所以只需匹配到即可。
最后返回优先级和规则的二元组,用于排序。
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
}
多条规则匹配
既然单条规则匹配已经完成,那么多条的就简单啦。
遍历整个样式表,判断是否匹配即可,最后返回匹配的多条规则。
// 遍历整个样式表,找出匹配的规则
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
上面已经得到了匹配的规则列表,这一步需要将规则中的所有属性放入 map 中。
不过,且慢,还记得上边提到的优先级问题吗?高优先级需后放入。
所以,首先还得将规则列表按照从低到高的优先级排序,保证最终属性值的正确性。
代码很简单,如下所示:
// 生成样式 map
func genStyleMap(node: ElementData, styleSheet: StyleSheet) -> StyleMap {
var styleMap = StyleMap()
// 获取匹配的 rule
var rules = matchRules(node: node, styleSheet: styleSheet)
// 从低优先级到高优先级排序,这样放入 map 中时高优先级会覆盖低优先级
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
}
生成样式树
最后一步,就是生成最终产物 —— 样式树。既然已经能够得到单个节点的样式,那么对于树状结构来说,递归遍历即可得到整棵树的样式。
不过有一点要注意,只有元素才存在样式,文本节点是没有的,生成一个空 map 给它。
// 生成样式树
func genStyleTree(root: Node, styleSheet: StyleSheet) -> StyleNode {
var styleMap: StyleMap
let nodeType = root.nodeType
switch nodeType {
// 文本节点无样式
case .Text(_):
styleMap = [:]
case .Element(let node):
// 生成样式 map
styleMap = genStyleMap(node: node, styleSheet: styleSheet)
}
// 子节点递归生成关联样式
let childrenStyleNodes = root.children.map { (child) -> StyleNode in
genStyleTree(root: child, styleSheet: styleSheet)
}
return StyleNode(node: root, styleMap: styleMap, children: childrenStyleNodes)
}
测试代码
// 样式关联处理
let styleProcessor = StyleProcessor()
let styleNode = styleProcessor.genStyleTree(root: root, styleSheet: styleSheet)
print(styleNode)
将 html 解析和 css 解析的结果作为输入,便可得到样式树。
总结
这一篇文章,主要介绍了如何进行节点样式的匹配,重点在于选择器的匹配,最后输出样式树。
下一篇,将介绍如何生成布局树,也就是确定元素的位置,大小等。敬请期待~