@extend 的工作原理
Natalie Weizenbaum 于 2013 年 11 月 23 日发布
这最初发布为 一个 gist.
Aaron Leung 正在开发 libsass,并且想知道 Sass 的 Ruby 实现中 @extend
是如何实现的。与其直接告诉他,我更愿意写一篇公开文档来介绍它,这样任何移植 Sass 或只是对它的工作原理感兴趣的人都可以 了解。
请注意,此解释在许多方面都进行了简化。它的目的是解释基本正确的 @extend
转换中最复杂的部分,但省略了许多细节,而这些细节对于希望获得完整 Sass 兼容性的用户来说非常重要。这应该被认为是对 @extend
基础的阐述,在此基础上可以构建完整的支持。要全面了解 @extend
,除了查阅 Ruby Sass 代码 和 其 测试 外别无他法。
本文档假设读者熟悉 选择器级别 4 规范中定义的选择器术语。在整个文档中,选择器将与它们的组件列表或集合互换使用。例如,复杂选择器可以被视为复合选择器列表或简单选择器列表的列表 。
基本元素基本元素永久链接
以下是实现 @extend
所需的一组基本对象、定义和操作。它们的实现留给读者作为练习 。
-
选择器对象显然是必要的,因为
@extend
完全是关于选择器的。选择器需要被彻底且语义化地解析。实现需要了解各种不同形式选择器背后的相当多的含义 。 -
还需要一个我称为“子集映射”的自定义数据结构。子集映射有两个操作:
Map.set(Set, Object)
和Map.get(Set) => [Object]
。前者将一个值与映射中的一组键关联。后者查找与一组键的子集关联的所有值。例如map.set([1, 2], 'value1') map.set([2, 3], 'value2') map.set([3, 4], 'value3') map.get([1, 2, 3]) => ['value1', 'value2']
-
如果选择器
S1
匹配的选择器S2
匹配的所有元素,则选择器S1
是选择器S2
的“超选择器”。例如,.foo
是.foo.bar
的超选择器,a
是div a
的超选择器,*
是所有内容的超选择器。超选择器的逆是“子选择器” 。 -
一个操作
unify(Compound Selector, Compound Selector) => Compound Selector
,它返回一个选择器,该选择器恰好匹配两个输入选择器匹配的元素。例如,unify(.foo, .bar)
返回.foo.bar
。这只需要对复合选择器或更简单的选择器起作用。此操作可能会失败(例如unify(a, h1)
),在这种情况下,它应该返回null
。 -
一个操作
trim([Selector List]) => Selector List
,它删除输入中作为其他复杂选择器的子选择器的复杂选择器。它将输入作为多个选择器列表,并且仅跨这些列表检查子选择器,因为先前的@extend
过程不会生成列表内的子选择器。例如,如果传递给它的是[[a], [.foo a]]
,它将返回[a]
,因为.foo a
是a
的子选择器。 -
一个操作
paths([[Object]]) => [[Object]]
,它返回每一步选择列表中所有可能路径的列表。例如,paths([[1, 2], [3], [4, 5, 6]])
返回[[1, 3, 4], [1, 3, 5], [1, 3, 6], [2, 3, 4], [2, 3, 5], [2, 3, 6]]
。
算法算法永久链接
@extend
算法需要两个步骤:一个步骤记录样式表中声明的 @extend
,另一个步骤使用这些 @extend
转换选择器。这是必要的,因为 @extend
可能会影响样式表中较早的选择器 。
记录步骤记录步骤永久链接
在伪代码中,此步骤可以描述如下
let MAP be an empty subset map from simple selectors to (complex selector, compound selector) pairs
for each @extend in the document:
let EXTENDER be the complex selector of the CSS rule containing the @extend
let TARGET be the compound selector being @extended
MAP.set(TARGET, (EXTENDER, TARGET))
转换步骤转换步骤永久链接
转换步骤比记录步骤更复杂。它在伪代码中进行了描述
let MAP be the subset map from the recording pass
define extend_complex(COMPLEX, SEEN) to be:
let CHOICES be an empty list of lists of complex selectors
for each compound selector COMPOUND in COMPLEX:
let EXTENDED be extend_compound(COMPOUND, SEEN)
if no complex selector in EXTENDED is a superselector of COMPOUND:
add a complex selector composed only of COMPOUND to EXTENDED
add EXTENDED to CHOICES
let WEAVES be an empty list of selector lists
for each list of complex selectors PATH in paths(CHOICES):
add weave(PATH) to WEAVES
return trim(WEAVES)
define extend_compound(COMPOUND, SEEN) to be:
let RESULTS be an empty list of complex selectors
for each (EXTENDER, TARGET) in MAP.get(COMPOUND):
if SEEN contains TARGET, move to the next iteration
let COMPOUND_WITHOUT_TARGET be COMPOUND without any of the simple selectors in TARGET
let EXTENDER_COMPOUND be the last compound selector in EXTENDER
let UNIFIED be unify(EXTENDER_COMPOUND, COMPOUND_WITHOUT_TARGET)
if UNIFIED is null, move to the next iteration
let UNIFIED_COMPLEX be EXTENDER with the last compound selector replaced with UNIFIED
with TARGET in SEEN:
add each complex selector in extend_complex(UNIFIED_COMPLEX, SEEN) to RESULTS
return RESULTS
for each selector COMPLEX in the document:
let SEEN be an empty set of compound selectors
let LIST be a selector list comprised of the complex selectors in extend_complex(COMPLEX, SEEN)
replace COMPLEX with LIST
细心的读者会注意到此伪代码中使用了未定义的函数:weave
。weave
比其他基本操作复杂得多,因此我想详细解释一下 。
WeaveWeave永久链接
在高级别上,“weave”操作非常容易理解。最好将其视为扩展“带括号的选择器”。想象一下,您可以编写 .foo (.bar a)
,它将匹配每个既有 .foo
父元素又有 .bar
父元素的 a
元素。weave
使此操作成为 可能。
为了匹配此 a
元素,您需要将 .foo (.bar a)
扩展为以下选择器列表:.foo .bar a, .foo.bar a, .bar .foo a
。这匹配了 a
可能同时具有 .foo
父元素和 .bar
父元素的所有可能方式。但是,weave
实际上并没有发出 .foo.bar a
;包含像这样的合并选择器会导致输出大小呈指数级增长,并且实用性非常 小。
此带括号的选择器作为复杂选择器列表传递给 weave
。例如,.foo (.bar a)
将作为 [.foo, .bar a]
传递。类似地,(.foo div) (.bar a) (.baz h1 span)
将作为 [.foo div, .bar a, .baz h1 span]
传递。
weave
通过从左到右遍历带括号的选择器来工作,构建所有可能的 前缀列表,并在遇到每个带括号的组件时添加到此列表中。以下是 伪代码
let PAREN_SELECTOR be the argument to weave(), a list of complex selectors
let PREFIXES be an empty list of complex selectors
for each complex selector COMPLEX in PAREN_SELECTOR:
if PREFIXES is empty:
add COMPLEX to PREFIXES
move to the next iteration
let COMPLEX_SUFFIX be the final compound selector in COMPLEX
let COMPLEX_PREFIX be COMPLEX without COMPLEX_SUFFIX
let NEW_PREFIXES be an empty list of complex selectors
for each complex selector PREFIX in PREFIXES:
let WOVEN be subweave(PREFIX, COMPLEX_PREFIX)
if WOVEN is null, move to the next iteration
for each complex selector WOVEN_COMPLEX in WOVEN:
append COMPLEX_SUFFIX to WOVEN_COMPLEX
add WOVEN_COMPLEX to NEW_PREFIXES
let PREFIXES be NEW_PREFIXES
return PREFIXES
这还包括另一个未定义的函数 subweave
,其中包含编织选择器的大部分逻辑。它是整个 @extend
算法中最复杂的逻辑部分之一——它处理选择器组合器、超选择器、主题选择器等等。但是,语义非常简单,编写它的基线版本非常 容易。
如果 weave
编织多个复杂选择器,则 subweave
只编织两个。它编织在一起的复杂选择器被认为具有隐式相同的尾随复合选择器;例如,如果传递给它的是 .foo .bar
和 .x .y .z
,它将它们编织在一起,就像它们是 .foo .bar E
和 .x .y .z E
一样。此外,它在大多数情况下不会合并这两个选择器,因此在这种情况下它只会返回 .foo .bar .x .y .z, .x .y .z .foo .bar
。一个极其简单的实现可以只返回这两个参数的两种顺序,并且在大多数情况下都是正确的 。
深入探讨 subweave
的全部复杂性超出了本文档的范围,因为它几乎完全属于本文档有意避免的高级功能类别。它的代码位于 lib/sass/selector/sequence.rb
中,在尝试进行严肃的实现时应参考该代码 。