评论

手把手教你实现一个浏览器引擎(四)Style [译文]

通过系列文章,让你轻松实现简易的浏览器引擎。

第四部分 Style

欢迎回到关于构建你自己的玩具浏览器引擎的系列文章。如果你是刚开始收看文章,你可以从下面找到以前的文章:

本文将会介绍在CSS标准中所谓的 分配属性值(assigning vproperty values) 或者 样式模块(style module)。这个模块将拿DOM节点和CSS规则作为输入,将他们互相匹配,为所给的所有节点确定每个CSS属性的值。

这部分不会包含很多代码,因为我还没有实现相对复杂的部分。然而,我觉得剩下的仍然相当有趣,我还会解释如何实现一些缺失的部分。

样式树 The Style Tree

我将Robinson的样式模块的输出称为 样式树(style tree), 在树里的每个节点都指向一个DOM节点,加上它的CSS属性值:

// Map from CSS property names to values.
type PropertyMap = HashMap<String, Value>;

// A node with associated style data.
struct StyledNode<'a> {
    node: &'a Node, // pointer to a DOM node
    specified_values: PropertyMap,
    children: Vec<StyledNode<'a>>,
}

'a是什么意思呢?这是都是生命周期,是Rust保证指针是内存安全的且不需要进行垃圾收集的一部分。如果你不是使用Rust工作的话,你可以忽略他们。他们对代码的意义影响不是很大。

其实,我们可以给dom:Node的结构增加字段,而不是创建一个新的树。但是我想抱着样式的代码和之前的“课程”相对独立。这也是给了我一个机会讲一下在大部分渲染引擎都会出现的 平行树(parallel trees)

浏览器引擎模块经常会将一个树当做输入,然后输出一个不同但是相关的树 。比如,Gecko的 布局代码(layout code) 利用 DOM树 生成 框架树(frame tree),然后将用于构建 视图树(view tree)。Blink 和 WebKit 将 DOM树 转换成 渲染树(render tree)。在这些引擎的后面阶段还会生成很多树,包括 层级树(layer tree)部件树(widget tree)

在完成其他几个阶段之后,我们的玩具浏览器引擎的流水线(pipeline)将会看起来像是这样:

在我的实现方式里,DOM树里的每个节点在样式树里都只有一个节点。但是在更复杂的流水线阶段,几个输入的节点可能会合并成一个输出节点。或者,一个输入节点可能会扩展为多个输出节点,或者被完全跳过。例如,样式树可能移除一个display属性设置成none的元素。(相反,我将在布局阶段移除这些,因为我的代码反而比较简单)

匹配选择器 Selector Matching

构建 样式树(style tree) 的第一步就是匹配选择器。因为我的CSS解析器只支持简单选择器,因此这将是很简单。可以通过查看元素本身来判断简单选择器是否与元素匹配。匹配复合选择器则需要遍历整个DOM树来查看元素的 兄弟元素(siblings)父元素(parents) 等等。

fn matches(elem: &ElementData, selector: &Selector) -> bool {
    match *selector {
        Simple(ref simple_selector) => matches_simple_selector(elem, simple_selector)
    }
}

为了提供帮助,我们将给我们的 DOM元素类型 增加一些遍历的ID和class访问器。class属性可以包含多个用空格分隔的类名,最后将这些类名通过哈希表的形式返回。

impl ElementData {
    pub fn id(&self) -> Option<&String> {
        self.attributes.get("id")
    }

    pub fn classes(&self) -> HashSet<&str> {
        match self.attributes.get("class") {
            Some(classlist) => classlist.split(' ').collect(),
            None => HashSet::new()
        }
    }
}

要测试一个简单选择器是否匹配了元素,只需要查看每个选择器的 组件(component) ,如果一个元素没有一个匹配上的class,ID或者标签名就返回false

fn matches_simple_selector(elem: &ElementData, selector: &SimpleSelector) -> bool {
    // Check type selector
    if selector.tag_name.iter().any(|name| elem.tag_name != *name) {
        return false;
    }

    // Check ID selector
    if selector.id.iter().any(|id| elem.id() != Some(id)) {
        return false;
    }

    // Check class selectors
    let elem_classes = elem.classes();
    if selector.class.iter().any(|class| !elem_classes.contains(&**class)) {
        return false;
    }

    // We didn't find any non-matching selector components.
    return true;
}

Rust笔记:这个函数使用any这个方法,如果迭代器包含一个可以通过提供的测试函数的元素则返回true。类似于 Python 或者 Haskell 里的 any 函数,JavaScript 里的 some 方法

构建样式树 Build the Style Tree

接下来,我们需要遍历整个DOM树,对于树中的每个元素,我们将在样式表中搜索匹配的规则。

比较两个匹配相同元素的规则时,我们需要使用每个匹配项中 最高明确性(highest specificity) 的选择器。因为我们CSS解析器储存的选择器是按明确性从高往低排序的,所以我们可以在找到匹配的选择器后立即停止,然后返回其明确性以及规则的指针。

type MatchedRule<'a> = (Specificity, &'a Rule);

// If `rule` matches `elem`, return a `MatchedRule`. Otherwise return `None`.
fn match_rule<'a>(elem: &ElementData, rule: &'a Rule) -> Option<MatchedRule<'a>> {
    // Find the first (highest-specificity) matching selector.
    rule.selectors.iter()
        .find(|selector| matches(elem, *selector))
        .map(|selector| (selector.specificity(), rule))
}

为了找到与某个元素匹配的所有规则,我们调用filter_map方法,它可以线性扫描整个样式表,检查每个规则并丢弃不匹配的规则。真正的浏览器引擎会通过基于标签名,ID,class等将规则存储在多个哈希表,从而实现快速匹配。

// Find all CSS rules that match the given element.
fn matching_rules<'a>(elem: &ElementData, stylesheet: &'a Stylesheet) -> Vec<MatchedRule<'a>> {
    stylesheet.rules.iter().filter_map(|rule| match_rule(elem, rule)).collect()
}

一旦有了匹配的规则,我们就可以找到元素的 指定值(specified value)。将每个规则的属性值插入到HashMap。将规则按 明确性(specificity) 排序,这样较高明确性的规则会在较低的之后进行处理,可以在HashMap将它们的值覆盖。

// Apply styles to a single element, returning the specified values.
fn specified_values(elem: &ElementData, stylesheet: &Stylesheet) -> PropertyMap {
    let mut values = HashMap::new();
    let mut rules = matching_rules(elem, stylesheet);

    // Go through the rules from lowest to highest specificity.
    rules.sort_by(|&(a, _), &(b, _)| a.cmp(&b));
    for (_, rule) in rules {
        for declaration in &rule.declarations {
            values.insert(declaration.name.clone(), declaration.value.clone());
        }
    }
    return values;
}

现在我们拥有了遍历DOM树并构建样式树所需的一切。需要注意的是,选择器匹配仅适用于元素,因此文本节点的指定值只是一个空的map

// Apply a stylesheet to an entire DOM tree, returning a StyledNode tree.
pub fn style_tree<'a>(root: &'a Node, stylesheet: &'a Stylesheet) -> StyledNode<'a> {
    StyledNode {
        node: root,
        specified_values: match root.node_type {
            Element(ref elem) => specified_values(elem, stylesheet),
            Text(_) => HashMap::new()
        },
        children: root.children.iter().map(|child| style_tree(child, stylesheet)).collect(),
    }
}

以上是Robinson关于构建样式树的所有代码。接下来,我将谈论一些明显的遗漏。

层叠 Cascade

由网页作者提供的样式表称为 作者样式表(author stlye sheets),除此之外,浏览器还通过 用户代理样式表(user agent style sheets) 提供了默认样式。还有它可以允许用户通过 用户样式表(user style sheets) 添加自定义样式(比如 Gecko 的 userContent.css)。

层叠(cascade) 定义了这三个“数据源”哪个优先权更高。层叠有六个级别:每个数据源都有”普通(normal)“声明,加上每个数据源还有!important声明。

Robinson的样式代码没有实现层叠的功能。它仅读取一个样式表。缺少默认样式表意味着HTML元素不会有你预期的任何默认样式。例如:<head>元素的内容不会被隐藏,除非你明确在你的样式表中添加这个规则:

head { display: none }

实现层叠应该相当容易:只需跟踪每条规则的起源,并根据明确性排序,其中明确性要加入数据源和重要性这些权重。一个简化的两级层叠应该足以支持最常见的情况:普通用户代理样式 和 普通作者样式。

计算值 Computed Values

除了上述提到的“指定值(sepecified values)”之外,CSS还定义了 初始值(initial values)计算值(computed values)使用值(used values)实际值(actual values)CSS标准链接

初始值是层叠中没有指定的属性默认值。计算值则是基于指定值的,但可能会应用一些特定属性的规范化规则。

根据CSS规范的定义,正确实现这些属性需要为每个属性使用的单独的代码。这是现实浏览器引擎的必要工作,但希望在我的玩具项目中避免这些。在后面阶段,在缺少指定值的时候,使用这些值的代码会(通过某种方式)使用默认值来模拟初始值。

使用值 和 实际值则是在布局期间和布局之后计算的,我将在以后的文章中介绍。

继承 Inheritance

如果文本节点不匹配任何选择器,那么它如何得到颜色和字体和其他样式呢?答案就是 继承(inheritance)。

继承属性后,没有层叠值(cascaded value)的任何节点都将收到该属性的父元素的值。默认情况下,某些属性(如颜色)都是继承过来的;其他的属性只有在层叠值指定特殊值inherit时才会继承父元素。

我的代码没有支持继承。要实现这个,可以将父元素的样式数据传入specified_values函数,并使用硬编码的查找表来决定应继承哪些属性。

样式属性 Style Attributes

任何HTML元素都可以包括一个style属性,其中包含一系列的CSS声明。它们没有选择器,因为这些声明自动应用到这个元素本身。

<span style="color: red; background: yellow;">

如果你想支持style属性,让specified_values函数检查属性即可。如果存在该属性,则将它从CSS解析器传入parse_declarations。由于这些属性比任何CSS选择器都更明确,因此在在 普通作者声明(normal author declarations) 之后应用这些 结果声明(resulting declarations)

原文链接:https://limpet.net/mbrubeck/2014/08/23/toy-layout-engine-4-style.html

最后一次编辑于  2020-02-13  
点赞 1
收藏
评论
登录 后发表内容