@extend 的運作方式

由 Natalie Weizenbaum 於 2013 年 11 月 23 日發佈

這最初是作為 gist 發佈的.

Aaron Leung 正在開發 libsass,並且想知道 @extend 在 Ruby Sass 實作中是如何運作的。我認為与其直接告訴他,不如寫一份公開文件,讓任何移植 Sass 或只是好奇它如何運作的人都能夠了解。

請注意,此說明在許多方面都經過簡化。它旨在解釋基本的正確 @extend 轉換中最複雜的部分,但省略了許多對於實現 Sass 完全相容性至關重要的細節。這應該被視為 @extend 基礎的闡述,可以在此基礎上建構完整支援。要完全理解 @extend,沒有什麼比參考 Ruby Sass 程式碼它的測試 更好。

本文假設讀者熟悉 Selectors Level 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']
  • 如果選擇器 S2 匹配的每個元素也與選擇器 S1 匹配,則選擇器 S1 是選擇器 S2 的「父選擇器」。例如,.foo.foo.bar 的父選擇器,adiv a 的父選擇器,而 * 是所有東西的父選擇器。父選擇器的反義詞是「子選擇器」。

  • 一個操作 unify(複合選擇器, 複合選擇器) => 複合選擇器,它返回一個與兩個輸入選擇器匹配的元素完全匹配的選擇器。例如,unify(.foo, .bar) 返回 .foo.bar。這只需要適用於複合選擇器或更簡單的選擇器。此操作可能會失敗(例如 unify(a, h1)),在這種情況下它應該返回 null

  • 一個操作 trim([選擇器清單]) => 選擇器清單,它移除輸入中是其他複雜選擇器的子選擇器的複雜選擇器。它將輸入作為多個選擇器清單,並且僅檢查這些清單中的子選擇器,因為先前的 @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,在嘗試認真實作時應該參考它。