css为什么不能选择相邻的前一个元素
使用场景
UI 设计了一个特殊的 Tab 页效果,需要在选中某个 Tab 后,改变相邻两个 Tab 的样式。因此需要选中后一个兄弟元素与前一个兄弟元素。
为什么没有前一个兄弟元素的选择器
CSS 中有~
来选中所有后续兄弟元素,+
来选中紧邻的后续兄弟元素,为什么没有语法能选中紧邻的前一个兄弟元素?
查阅资料后发现,诸如 查找子元素的直接父元素、查找子元素的祖先元素、查找某元素的前一个紧邻兄弟元素 这样的选择器,CSS 中都未实现。因为这样的选择器会导致回溯。
浏览器设计之初就考虑了渐进显示,文档加载了多少就显示多少内容,而不用等整个文档下载完成。
渐进显示在 CSS 上的原理就是一个节点所适用的样式只取决于它和它之前的节点(父节点或它之前的兄弟节点)。而上面举例的选择器都恰好相反,即当浏览器解析到一个新节点时,可能改变之前节点所适用的样式,这违背了前面的原理,一个节点的样式被他之后的节点改变了。这就要求在解析一个新节点后,得回头重新计算之前节点所匹配的样式,这就是所谓的“回溯”。最坏的情况就是导致大量的重新计算,相当于重新渲染整个网页。
因此 CSS 直到 2.1 都没有引入任何可能引起回溯的选择器,但从 CSS3 开始,如 :last-child
等类似选择器,会造成回溯。
CSS2 中所有选择器在读入一个起始标签(start tag)后,就确定了是否匹配。但对于 CSS3 中的 :last-child
伪类要判断这条规则是否适用,至少要在结束标签(end tag)后再读入一个标签才能确定,如果是父元素的结束标签,则匹配成功,否则匹配失败。假设浏览器渐进显示了部分内容,我们对某个元素应用了 :last-child
伪类样式,这可能出现加载过程中元素并未应用到伪类提供的样式,直到加载完成后才应用成功。
方案
那么没有方便的 previous sibling 选择器,还有什么方案来实现呢?结论:使用 JS 实现,知道结论的情况下,我们也可以考虑下是否有纯 CSS 实现的方案。
一、使用 :not
~
:last-child
选择器组合选出前一个兄弟元素
先选取出 选中元素 的 后续兄弟元素,再取反,此时选取出的是 选中元素 及 其前面的元素,此时再筛选掉选中元素,那么选取中的就是 选中元素前的所有元素。再取最后一个元素,即为选中元素前的紧邻兄弟元素。写法如下:element:not(.selected-element ~ element):not(.selected-element):last-child {}
。
然而这个方法在 :last-child
之前都是没有问题的,加入 :last-child
就不生效了,这是由于 :last-child
的实现原理导致的,:last-child
需要读入父元素的 end tag 才能确定,此时选取出的元素,后面并没有父元素的 end tag,故不会生效。
二、使用:has()
伪类
使用 CSS Selector Level 4 中的 :has()
伪类,如类似这样的写法 .previous-element:has(+ .selected-element) {}
,不过可惜的是,现在尚未有浏览器实现这一伪类。
三、使用 JS
使用 JS 来辅助改变样式,可以说是最简单和常规的方案了。在 for 循环渲染一组元素后,判断当前元素下标是否是选中元素的前一个元素下标,是则应用样式,否则不应用。
参考资料
如何给 W3C 组织提关于 Web 标准的建议 - 贺师俊的回答