@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的父選擇器,a是div a的父選擇器,而*是所有東西的父選擇器。父選擇器的反義詞是「子選擇器」。 -
一個操作
unify(複合選擇器, 複合選擇器) => 複合選擇器,它返回一個與兩個輸入選擇器匹配的元素完全匹配的選擇器。例如,unify(.foo, .bar)返回.foo.bar。這只需要適用於複合選擇器或更簡單的選擇器。此操作可能會失敗(例如unify(a, h1)),在這種情況下它應該返回null。 -
一個操作
trim([選擇器清單]) => 選擇器清單,它移除輸入中是其他複雜選擇器的子選擇器的複雜選擇器。它將輸入作為多個選擇器清單,並且僅檢查這些清單中的子選擇器,因為先前的@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,在嘗試認真實作時應該參考它。