听说你想写个渲染引擎 - 样式树

577 阅读6分钟

大家好,我是微微笑的蜗牛,🐌。

上一篇文章中,我们解析出了总体的样式表。

今天,我们来介绍如何生成样式树。

简单来说,样式树就是确定 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 解析的结果作为输入,便可得到样式树。

完整代码可查看:github.com/silan-liu/t…

最后

您要是觉得文章有帮助的话,可以点击下方名片关注公众号「微微笑的蜗牛」。

在公众号聊天框中回复「蜗牛」,可添加微信进行交流~