重学前端(4)- 浏览器是如何工作的(3)
浏览器是如何把 CSS 规则应用到节点上,并给这棵朴素的 DOM 树添加上 CSS 属性
整体过程
首先 CSS 选择器这个名称,可能会给你带来一定的误解,觉得好像 CSS 规则是 DOM 树构建好了以后,再进行选择并给它添加样式的。实际上,这个过程并不是这样的。
构建 DOM 的过程是:从父到子,从先到后,一个一个节点构造,并且挂载到 DOM 树上的,那么这个过程中,我们是否能同步把 CSS 属性计算出来呢?
在这个过程中,我们依次拿到上一部构造好的元素,去检查它匹配到了哪些规则,再根据规则的优先级,做覆盖和调整。所以,从这个角度看,所谓的选择器,应该被理解成“匹配器”才更合适。
总结了选择器的各种符号我们回顾一下:
- 空格: 后代,选中它的子节点和所有子节点的后代节点。
- >: 子代,选中它的子节点。
- +:直接后继选择器,选中它的下一个相邻节点。
- ~:后继,选中它之后所有的相邻节点。
- ||:列,选中表格中的一列。
这里我们主要介绍浏览器是如何实现这些规则的。
这里的选择器有个特点,那就是选择器的出现顺序,必定跟构建 DOM 树的顺序一致。这是一个 CSS 设计的原则,即保证选择器在 DOM 树构建到当前节点时,已经可以准确判断是否匹配,不需要后续节点信息。
也就是说,未来也不可能会出现“父元素选择器”这种东西,因为父元素选择器要求根据当前节点的子节点,来判断当前节点是否被选中,而父节点会先于子节点构建。
理解了 CSS 构建的大概过程,我们下面来看看具体的操作。
首先,我们必须把 CSS 规则做一下处理。作为一门语言,CSS 需要先经过词法分析和语法分析,变成计算机能够理解的结构。
这部分具体的做法属于编译原理的内容,这里就不做赘述了。我们这里假设 CSS 已经被解析成了一棵可用的抽象语法树。我们在之前的 CSS 课程中已经介绍过compound-selector 的概念,一个 compound-selector 是检查一个元素的规则,而一个复合型选择器,则是由数个compound-selector 通过前面讲的符号连接起来的。
后代选择器 “空格”
我们先来分析一下后代选择器,我们来一起看一个例子:
a#b .cls { width: 100px; }
可以把一个 CSS 选择器按照 compound-selector 来拆成数段,每当满足一段条件的时候,就前进一段。
比如,在上面的例子中,当我们找到了匹配 a#b 的元素时,我们才会开始检查它所有的子代是否匹配 .cls。
除了前进一段的情况,我们还需要处理后退的情况,比如,我们这样一段代码:
<a id=b>
<span>1<span>
<span class="cls">2<span>
</a>
<span class="cls">3<span>
当遇到 </a> 时,必须使得规则 a#b .cls 回退一步,这样第三个 span 才不会被选中。后代选择器的作用范围是父节点的所有子节点,因此规则是在匹配到本标签的结束标签时回退。
后继选择器“ ~ ”
接下来我们看下后继选择器,跟后代选择器不同的地方是,后继选择器只作用于一层,我们来看一个例子:
.cls~* { border:solid 1px green; } <div> <span>1</span> <span class="cls">2</span> <span> 3 <span>4</span> </span> <span>5</span> </div>
这里 .cls 选中了 span 2 然后 span 3 是它的后继,但是 span 3 的子节点 span 4 并不应该被选中,而 span 5 也是它的后继,因此应该被选中。
按照 DOM 树的构造顺序,4 在 3 和 5 中间,我们就没有办法像前面讲的后代选择器一样通过激活或者关闭规则来实现匹配。
但是这里有个非常方便的思路,就是给选择器的激活,带上一个条件:父元素。注意,这里后继选择器,当前半段的 .cls 匹配成功时,后续 * 所匹配的所有元素的父元素都已经确定了(后继节点和当前节点父元素相同是充分必要条件)。在我们的例子中,那个 div 就是后继节点的父元素。
子代选择器“ >”
实际上,有了前面讲的父元素这个约束思路,我们很容易实现子代选择器。区别仅仅是拿当前节点作为父元素,还是拿当前节点的父元素作为父元素。
div>.cls { border:solid 1px green; } <div> <span>1</span> <span class="cls">2</span> <span> 3 <span>4</span> </span> <span>5</span> </div>
我们看这段代码,当 DOM 树构造到 div 时,匹配了 CSS 规则的第一段,因为是子代选择器,我们激活后面的 .cls 选择条件,并且指定父元素必须是当前 div。于是
后续的构建 DOM 树构建过程中,span 2 就被选中了。
直接后继选择器“ +”
直接后继选择器的实现是上述中最为简单的了,因为它只对唯一一个元素生效,所以不需要像前面几种一样反复激活和关闭规则。
一个最简单的思路是,我们可以把它当作检查元素自身的选择器来处理。即我们把 #id+.cls 都当做检查某一个元素的选择器。
另外的一种思路是:给后继选择器加上一个 flag,使它匹配一次后失效。