@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 的超选择器,adiv 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 aa 的子选择器。

  • 一个操作 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

细心的读者会注意到此伪代码中使用了未定义的函数:weaveweave 比其他基本操作复杂得多,因此我想详细解释一下 

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 中,在尝试进行严肃的实现时应参考该代码