WebKit Insie: Active 樣式表

chaoguo1234發表於2023-10-07

WebKit Inside: CSS 樣式表的匹配時機介紹了當 HTML 頁面有不同 CSS 樣式表引入時,CSS 樣式表開始匹配的時機。後續文章繼續介紹 CSS 樣式表的匹配過程,但是在匹配之前,首先需要收集頁面裡面的 Active 樣式表。

1 Active 樣式表

在一個 HTML 檔案裡面,可能會使用<style>標籤與<link>標籤引入許多樣式表,但是這些樣式表並不一定都同時在文件裡面生效。有時根據業務需求,可能會只使用頁面裡的部分樣式表。比如有一個換膚需求,頁面裡面可能會使用<link>標籤引入 4 張樣式表,程式碼如下:

<link href="reset.css" rel="stylesheet" />

<link href="default.css" rel="stylesheet" title="Default Style" />
<link href="fancy.css" rel="alternate stylesheet" title="Fancy" />
<link href="basic.css" rel="alternate stylesheet" title="Basic" />

上面樣式表reset.css所在的<link>標籤有rel="stylesheet"屬性,沒有title屬性,這種樣式表被稱為 Persisten 樣式表,會一直被啟用。

樣式表default.css所在的<link>標籤有rel="stylesheet"title屬性,這種樣式表被稱為 Preferred 樣式表。Preferred 樣式表是預設啟用。一個頁面只能有一個 Preferred 樣式表。

樣式表fancy.cssbasic.css所在<link>標籤有rel="alternate stylesheet"title屬性,這種樣式表被稱為 Alternate 樣式表。這些樣式表預設下是不啟用的,但是可以提供給使用者選擇。一旦使用者選擇了一個 Alternate 樣式表,Preferred 樣式表就會別禁用。

根據 <link>標籤語法,樣式表reset.cssdefault.css會在頁面裡面使用,而樣式表fancy.cssbasic.css會暫時不使用。

一般這種場景會給一個按鈕讓使用者切換皮膚,當使用者選擇切換到Fancy皮膚時,樣式表default.css就失效,樣式表fancy.css就會啟用。但是不管使用者如何切換,樣式表reset.css始終有效。更多資訊可以參考 MDN Alternative Style Sheet[1]

在使用者沒有換膚之前,樣式表reset.cssdefault.css樣式表就屬於 Active 樣式表,當使用者選擇切換之後,樣式表reset.cssfancy.css就是 Active 樣式表。

在進行 CSS 樣式表匹配之前,WebKit 首先要收集頁面裡面所有的 Active 樣式表,然後依次遍歷這些 Active 樣式表的 CSS Rule 進行匹配。

2 相關類圖

image
上面類圖裡Style::SCope類持有負責進行樣式表匹配的Style::Resolver類,同時它內部還有 3 個重要的資料成員:
m_styleSheetCandidateNodes是一個雜湊連結串列,用來按順序儲存 HTML 檔案裡面的 <style><link>節點,也就是 HTMLStyleElment物件和HTMLLinkElement物件。

m_activeStyleSheets是一個 Vector,類似陣列,用來順序儲存頁面裡面的 Active 樣式表。

m_styleSheetsForStyleSheetList也是一個 Vector,用來順序儲存頁面裡面的所有樣式表。

3 獲取 Candidate Node

無論內部樣式表,還是外部樣式表,當 WebKit 解析到 <style>標籤或者<link>標籤時,都會呼叫Style::Scope::addStyleSheetCandidateNode方法,將自己新增到Style::Scope的例項變數m_styleSheetCandidateNode裡面。

以內部樣式表為例,下面是呼叫堆疊:

image

函式Style::Scope::addStyleSheetCandidateNode的程式碼如下:

void Scope::addStyleSheetCandidateNode(Node& node, bool createdByParser)
{
    if (!node.isConnected())
        return;
    
    // Until the <body> exists, we have no choice but to compare document positions,
    // since styles outside of the body and head continue to be shunted into the head
    // (and thus can shift to end up before dynamically added DOM content that is also
    // outside the body).
    // 1. createByParser 代表當前的 node 是從 HTML 檔案裡解析出來的,而不是透過 JavaScript 程式碼動態建立的;
    // m_document.bodyOrFrameset 方法判斷當前頁面是否解析出了 <body> 標籤和 <frameset> 標籤;
    // 如果前這兩個條件為真,那麼節點直接新增到變數 m_styleSheetCandidateNodes;
    // 還有一種情形 m_styleSheetCandidateNodes 當前還沒有新增任何節點
    if ((createdByParser && m_document.bodyOrFrameset()) || m_styleSheetCandidateNodes.isEmptyIgnoringNullReferences()) {
        m_styleSheetCandidateNodes.add(node);
        return;
    }

    // Determine an appropriate insertion point.
    // 2. 如果上述條件不滿足,就會走到這裡,這裡會將當前節點與 m_styleSheetCandidateNodes 裡已有的 <style> 或者 <link>
    // 節點進行位置比較,以便按照正確的順序把當前節點插入到 m_styleSheetCandidateNodes.
    auto begin = m_styleSheetCandidateNodes.begin();
    auto end = m_styleSheetCandidateNodes.end();
    auto it = end;
    RefPtr<Node> followingNode;
    do {
        // 3. // 從後向前遍歷
        --it;
        Ref<Node> n = *it;
        unsigned short position = n->compareDocumentPosition(node);
        // 4. DOCUMENT_POSITION_FOLLOWING 表示當前節點 node 位於節點 n 後面
        if (position == Node::DOCUMENT_POSITION_FOLLOWING) {
            if (followingNode)
                // 5. 注意 followwingNode 位於節點 n 的後面,這裡將節點 node 插入到 followwingNode 前面,
                // 也就是剛好插儒道節點 n 的後面
                m_styleSheetCandidateNodes.insertBefore(*followingNode, node);
            else
                // 6. 如果節點 node 位於節點 n 後面,但是節點插入之前節點 n 後面已經沒有其他節點了,那麼就直接
                // 將節點 node 新增到節點 n 後面
                m_styleSheetCandidateNodes.appendOrMoveToLast(node);
            return;
        }
        followingNode = WTFMove(n);
    } while (it != begin);

    LOG_WITH_STREAM(StyleSheets, stream << "Scope " << this << " addStyleSheetCandidateNode() " << node);

    // 7. 如果遍歷到 m_styleSheetCandidateNodes 最前面,上面程式碼也沒有找到合適的位置,
    // 那麼就將節點 node 插入到最前面.
    m_styleSheetCandidateNodes.insertBefore(*followingNode, node);
}

上面程式碼註釋 1 是向變數m_styleSheetCandidateNodes新增 node 節點的第一處程式碼。變數createByParser代表當前節點 node 是從 HTML 檔案裡解析出來的,而不是透過 JavaScript 程式碼動態建立出來的。函式document.bodyOrFrameset代表當前是否已經解析出了<body>標籤後者<frameset>標籤。如果滿足前面這兩個條件,或者當前m_styleSheetCandidateNodes裡為空,那麼就將當前 node 新增進去。

如果上面條件都不滿足,程式碼會執行到註釋 2 處。註釋 2 後面的程式碼會將當前 node 節點與m_styleSheetCandidateNodes變數裡已有的<style>後者<link>標籤的位置相比較,以便按照正確的位置將當前節點 node 插入到m_styleSheetCandidateNodes

那什麼是正確的位置呢?

因為樣式表的位置影響著樣式表裡 CSS Rule 中宣告的優先順序。比如 HTML 頁面透過<link>標籤引入了 2 個樣式表 A 與 B,其中樣式表 B 位於 樣式表 A 後面。如果樣式表 A 有如下 CSS Rule:

div {
	background-color: red;
}

樣式表 B 的 CSS Rule 和樣式表一樣,只是設定背景色為藍色:

div {
	background-color: blue;
}

由於樣式表 A 和 B 都是 Author 樣式表[2],而且 Specificity[3] 也一樣,因此宣告的優先順序取決於它們所在的位置。由於樣式表 B 比樣式表 A 更靠後,因此最終會應用樣式表 B 中的背景色。

上面程式碼註釋 3 處就是從後向前遍歷m_styleSheetCandidateNodes,以便找到這個正確位置。

註釋 4 處比較節點n與節點node的位置關係[4]。如果節點node位於節點n的後面,也就是DOCUMENT_POSITION_FOLLOWING,那麼就可以插入節點。

DOCUMENT_POSITION_FOLLOWING的意義是按照 DOM 樹的 Tree Order[5][6] 進行遍歷,節點node位於位於節點n之後。Tree Order 就是按照先序-深度優先(preorder,depth-first)遍歷。

假設有如下的 HTML:

<html>
	<head>
		<link rel="stylesheet" href="./test1.css" />
		<link rel="stylesheet" href="./test2.css" />
	</head>
	<body>
		<div>Hello</div>
		<p>World</p>
	</body>
</html>

其 DOM 樹結構如下:

image
按照 Tree Order 先序-深度優先遍歷,那麼就是先遍歷根節點,然後遍歷從左起第一棵子樹,然後是第二棵子樹,然後是第三棵子樹...。

遍歷的順序如上圖所示,遍歷結果如下:html->head->title->link->link->body->div->p。從遍歷結果可以看到,第 2 個 <link>標籤位於第 1 個<link>標籤後面,也就是第 2 個 <link>標籤following 第 1 個 <link>標籤。

從遍歷結果上看,按照先序-深度優先遍歷的位置關係,正好是 HTML 檔案裡面各標籤的書寫位置關係。

程式碼註釋 5、6、7 都是將節點node插入到m_styleSheetCandidateNodes合適的位置,也就是說 HTML 裡面是按照什麼順序引入的樣式表,m_styleSheetCandidateNodes就是按照同樣的順序儲存的<style>或者<link>標籤節點。

4 獲取 Active 樣式表

無論內部樣式表還是外部樣式表,當其解析完成之後,都會呼叫對應的checkLoaded方法。

內部樣式表的呼叫如下:

void InlineStyleSheetOwner::createSheet(Element& element, const String& text)
{
    ...
    auto contents = StyleSheetContents::create(String(), parserContextForElement(element));
    m_sheet = CSSStyleSheet::createInline(contents.get(), element, m_startTextPosition);
    ...
    // 1. 解析內部樣式表
    contents->parseString(text);
    ...
    // 2. 呼叫 checkLoaded 方法
    contents->checkLoaded();
    ...
}

上面程式碼註釋 1 解析內部樣式表。
程式碼註釋 2 呼叫checkLoaded方法。

外部樣式表的呼叫如下:

void HTMLLinkElement::setCSSStyleSheet(const String& href, const URL& baseURL, const String& charset, const CachedCSSStyleSheet* cachedStyleSheet)
{
    ...
    auto styleSheet = StyleSheetContents::create(href, parserContext);
    initializeStyleSheet(styleSheet.copyRef(), *cachedStyleSheet, MediaQueryParserContext(document()));

    // FIXME: Set the visibility option based on m_sheet being clean or not.
    // Best approach might be to set it on the style sheet content itself or its context parser otherwise.
    // 1. 解析外部樣式表
    if (!styleSheet.get().parseAuthorStyleSheet(cachedStyleSheet, &document().securityOrigin())) {
       ...
    }
    ...
    // 2. 呼叫 checkLoaded 方法
    styleSheet.get().checkLoaded();
    ...
}

上面程式碼註釋 1 解析外部樣式表。

註釋 2 解析呼叫checkLoaded方法。

這兩種情形的 checkLoaded方法最終會呼叫到Scope::didChangeActiveStyleSheetCandidates方法,程式碼如下:

void Scope::didChangeActiveStyleSheetCandidates()
{
    scheduleUpdate(UpdateType::ActiveSet);
}

Scope::didChangeActiveStyleSheetCandidates方法內部只呼叫了一個方法Scope::scheduleUpdate,傳給它的引數是UpdateType::ActiveSet

UpdateType型別是一個列舉,定義在StyleScope.h,其定義如下:

  // 定義在 StyleScope.h
  enum class UpdateType : uint8_t { 
    ActiveSet, // 代表一個樣式表解析完成,稱為了 Active 樣式表
    ContentsOrInterpretation 
};

列舉UpdateType::ActiveSt代表一個 Active 樣式表可用了。

方法 Scope::scheduleUpdate方法如下:

void Scope::scheduleUpdate(UpdateType update)
{
    ...
    if (!m_pendingUpdate || *m_pendingUpdate < update) {
        // 1. 這裡設定 m_pendingUpdate 
        m_pendingUpdate = update;
        ...
    }
    ...
     // 2. 啟動 Timer,Timer 的回撥函式觸發 Active 樣式表的收集.
    // 引數 0 代表不延時,立即觸發
    m_pendingUpdateTimer.startOneShot(0_s);
}

上面程式碼註釋 1 設定Style::Scope物件的一個變數m_pendingUpdate,這個變數在後續觸發 Active 樣式表收集使用。

程式碼註釋 2 啟用一個 Timer,Timer 的回撥函式觸發 Active 樣式表的收集流程。

Timer 的回撥函式如下:

void Scope::pendingUpdateTimerFired()
{
    /// 1. 觸發 Active 樣式表收集
    flushPendingUpdate();
}

上面程式碼註釋 1 呼叫Style::Scope::flushPendingUpdate方法觸發 Active 樣式表收集。

方法Style::Scope::flushPendingUpdate程式碼如下:

inline void Scope::flushPendingUpdate()
{
    ...
    // 1. m_pendingUpdate 已經在方法 Style::Scope::scheduleUpdate 裡設定
    if (m_pendingUpdate)
        flushPendingSelfUpdate();
}

上面程式碼註釋 1 處變數m_pendingUpdate已經在方法Style::Scope::scheduleUpdate裡面設定成了UpdateType::ActiveSset,所以這裡直接呼叫方法Style::Scope::flushPendingSelfUpdate方法。

Style::Scope::flushPendingSelfUpdate方法程式碼如下:

void Scope::flushPendingSelfUpdate()
{
    ASSERT(m_pendingUpdate);
    auto updateType = *m_pendingUpdate;
    // 1. 清除 m_pendingUpdate 變數,給其置空
    clearPendingUpdate();
    // 2. 收集 Active 樣式表
    updateActiveStyleSheets(updateType);
}

上面程式碼註釋 1 首先清除變數m_pendingUpdate,給其置空。

程式碼註釋 2 呼叫Style::Scope::updateActiveStyleShhets方法開始真正的收集 Active 樣式表。

方法Style::Scope::updateActiveStyleSheets程式碼如下:

void Scope::updateActiveStyleSheets(UpdateType updateType)
{
    ...
    // 1. 收集 Active 樣式表.
    // collection 變數裡面儲存著收集到的 Active 樣式表和頁面裡面所有樣式表.
    auto collection = collectActiveStyleSheets();
    // 2. 變數 activeCSSStyleSheets 裡面儲存收集到的 Active 樣式表,collection 變數裡的 Active 樣式表會賦值給這個變數
    Vector<RefPtr<CSSStyleSheet>> activeCSSStyleSheets;
    // 3. 上面已經收集了最新新增的 Active 樣式表,這裡進行過濾,剔除那些比如樣式表長度為 0 的樣式表,
    // 過濾後的結果儲存在 activeCSSStyleSheets 中.
    filterEnabledNonemptyCSSStyleSheets(activeCSSStyleSheets, collection.activeStyleSheets);
    ...
    // 4. 將 Active 樣式表儲存到 m_activeStyleSheets
    m_activeStyleSheets.swap(activeCSSStyleSheets);
    // 5. 將所有樣式表儲存到 m_styleSheetsForStyleSheetList
    m_styleSheetsForStyleSheetList.swap(collection.styleSheetsForStyleSheetList);
    ...
}

上面程式碼註釋 1 呼叫方法Style::Scope::collectActiveStyleSheet收集頁面裡面的 Active 樣式表和所有樣式表,將結果儲存在變數collection中。

程式碼註釋 2 宣告的變數activeCSSStyleSheets會儲存變數collection中的 Active 樣式表。

程式碼註釋 3 現將變數collection中的 Active 樣式表進行過濾,剔除那些比如樣式表長度為 0 的樣式表,過濾後的結果儲存在activeCSSStyleSheets變數中。

程式碼註釋 4 將變數activeCSStyleSheet的值交換給例項變數m_activeStyleSheets,也就是m_activeStyleSheets現在儲存著頁面裡面的 Active 樣式表。

同理,程式碼註釋 5 將頁面裡面所有的樣式表儲存在例項變數m_styleSheetsForStyleSheetList裡。

下面看一下 Active 樣式表的收集過程,也就是函式Style::Scope::collectActiveStyleSheet,程式碼如下:

auto Scope::collectActiveStyleSheets() -> ActiveStyleSheetCollection
{
    ...
    // 1. 儲存 Active 樣式表
    Vector<RefPtr<StyleSheet>> sheets;
    // 2. 儲存 HTML 頁面裡面所有樣式表
    Vector<RefPtr<StyleSheet>> styleSheetsForStyleSheetsList;

    // 3. 遍歷之前儲存在 m_styleSheetCandidateNodes 裡的 <style> 或者 <link> 標籤節點物件
    for (auto& node : m_styleSheetCandidateNodes) {
        RefPtr<StyleSheet> sheet;
        if (is<ProcessingInstruction>(node)) {
            // 4. ProcessingInstruction 就是諸如 <?xml> 這樣的標籤
            ...
            
        } else if (is<HTMLLinkElement>(node) || is<HTMLStyleElement>(node) || is<SVGStyleElement>(node)) {
            Element& element = downcast<Element>(node);
            ...
            // Get the current preferred styleset. This is the
            // set of sheets that will be enabled.
            if (is<SVGStyleElement>(element))
                sheet = downcast<SVGStyleElement>(element).sheet();
            else if (is<HTMLLinkElement>(element))
                // 5. 獲取外部樣式表
                sheet = downcast<HTMLLinkElement>(element).sheet();
            else
                // 6. 獲取內部樣式表
                sheet = downcast<HTMLStyleElement>(element).sheet();

            if (sheet)
                // 7. 將樣式表新增到 styleSheetsForStyleSheetsList
                styleSheetsForStyleSheetsList.append(sheet);

            // Check to see if this sheet belongs to a styleset
            // (thus making it PREFERRED or ALTERNATE rather than
            // PERSISTENT).
            auto& rel = element.attributeWithoutSynchronization(relAttr);
            if (!enabledViaScript && sheet && !title.isEmpty()) {
                ...
                // 8. 如果 <link> 標籤的 rel 屬性包含 alternate,並且有 title,這裡將 sheet 設定為 null,
                // 後面也新增不到 Active 樣式表了.
                if (title != m_preferredStylesheetSetName)
                    sheet = nullptr;
            }
            // 9. 如果 <link> 標籤的 rel 屬性包含了 alternate,並且沒有 title 屬性,那麼也將 sheet 設定為 null,
            // 後面也新增不到 Active 樣式表了.
            if (rel.contains("alternate"_s) && title.isEmpty())
                sheet = nullptr;
            ...
        }
        if (sheet)
            // 10. 將當前樣式表新增到 Active 樣式表中
            sheets.append(WTFMove(sheet));
    }
    ...
    // 11. 將結果返回
    // sheets 儲存 Active 樣式表
    // styleSheetsForStyleSheetsList 儲存所有樣式表
    return { WTFMove(sheets), WTFMove(styleSheetsForStyleSheetsList) };
}

上面程式碼註釋 1 宣告變數sheets用來儲存頁面裡面的 Active 樣式表。

程式碼註釋 2 宣告變數styleSheetsForStyleSheetsList用來儲存頁面裡面的所有樣式表。

程式碼註釋 3 遍歷之前儲存在m_styleSheetCandidateNodes例項變數裡面的<style>標籤和<link>標籤節點物件。

程式碼註釋 4 處理Processing Instruct[7],不在收集 Active 樣式表考慮之內。

程式碼註釋 5 和程式碼註釋 6 根據遍歷的節點物件,從其上面獲取到對應的樣式表物件sheet

程式碼註釋 7 將上面獲取到的註釋表物件儲存到變數styleSheetsForStyleSheetsList,這樣styleSheetsForStyleSheetsList裡面就是儲存的是頁面裡面所有的樣式表。

程式碼註釋 8 處理 Alternate 樣式表,也就是<link>標籤的rel屬性包含alternate,並且title屬性有值,此時程式碼將變數sheet設定為null,這樣後續這張樣式表就新增不到 Active 樣式表裡面了。

程式碼註釋 9 同樣也是處理 Alternate 樣式表,使其後續無法新增到 Active 樣式表裡面。

程式碼註釋 10 將獲取到的樣式表新增到sheets變數,也就是 Active 樣式表中。

程式碼註釋 11 將收集的 Active 樣式表和頁面裡面所有樣式表返回出去。

5 小結

要獲取 HTML 樣式表裡的 Active 樣式表,首先就要獲取頁面裡面的<style>標籤節點物件和<link>標籤節點物件。這些節點物件在<style>標籤和<link>標籤插入到 DOM 樹時按照 TreeOrder 順序儲存在Style::Scope的例項變數m_styleSheetCandidateNodes中。

然後,當內部樣式表或者外部樣式表解析成功之後,會觸發 Active 樣式表的收集,收集過程就是遍歷Style::Scope的例項變數m_styleSheetCandidateNodes,將<style>標籤節點或者<link>標籤節點關聯的樣式表收集到Style::Scope的例項變數m_activeStyleSheets中。


  1. MDN Alternative Style Sheet ↩︎

  2. MDN Introducing the CSS Cascade ↩︎

  3. MDN Specificity ↩︎

  4. MDN compareDocumentPosition ↩︎

  5. https://dom.spec.whatwg.org/#concept-tree-order ↩︎

  6. https://dom.spec.whatwg.org/#dom-node-document_position_following ↩︎

  7. MDN ProcessingInstruction ↩︎

相關文章