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

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

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

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

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

完整代码可点此查看

总结

这一篇文章,主要介绍了如何进行节点样式的匹配,重点在于选择器的匹配,最后输出样式树。

下一篇,将介绍如何生成布局树,也就是确定元素的位置,大小等。敬请期待~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容