WebKit Inside: CSS 樣式表的匹配時機

chaoguo1234發表於2023-10-05

WebKit Inside: CSS 的解析 介紹了 CSS 樣式表的解析過程,這篇文章繼續介紹 CSS 的匹配時機。

無外部樣式表

內部樣式表和行內樣式表本身就在 HTML 裡面,解析 HTML 標籤構建 DOM 樹時內部樣式表和行內樣式就會被解析完畢。因此如果 HTML 裡面只有內部樣式表和行內樣式,那麼當 DOM 樹構建完畢之後,就可以進行樣式表的匹配了。

假設 HTML 裡面的行內樣式在 <div>標籤,那麼 CSS 匹配樣式時機如下圖所示:

image

如果 HTML 裡面除了內部樣式表或者行內樣式,還有外部樣式表,那麼情形比較複雜。

由於引入外部樣式表的 <link>標籤可以位於 <head>標籤中,也可以位於<body>標籤中,這兩種情形下,匹配時機不一樣。

外部樣式表位於 head

如果 HTML 裡面有外部樣式表和內部樣式表,HTML 程式碼如下:

<html>
	<head>
		<meta charset='utf-8' />
		<title>EasyHTML</title>
		<style text="text/css">
		/* 內部樣式表 */
		div {
			background-color: red;
		}
		</style>
		<!-- 外部樣式表-->
		<link rel="stylesheet" href="cs.css" />
		
	</head>
	<body>
		<div>kkk</div>
	</body>
</html>

外部樣式表 CSS 檔案如下:

div {
	background-color: blue;
	font-size: 20px;
}

如果在 DOM 樹構建完成之前,外部樣式表就已經下載回來並且解析,那麼,當 DOM 樹構建完成之後,就可以直接進行樣式表的匹配。

但是如果在 DOM 樹構建完成之後,外部樣式表還沒有下載回來,那麼即使內部樣式表已經解析完成了,也不會進行任何樣式表的匹配。呼叫堆疊如下圖所示:

image

在函式 TreeResolver::resolveElement中,此時第一行 if裡面 m_didSeePendingStyleSheet為真,因此不會進行任何樣式的匹配。

由於沒有進行樣式匹配,無法構建渲染樹,當然也不會佈局和繪製,在外部樣式表的下載過程中,頁面是空白的。因此 CSS 的下載雖然不阻塞 DOM 樹的構建,但是阻塞渲染。

變數m_didSeePendingStyleSheet在函式TreeResolver::resovle裡面設定,如果位於 <head>標籤裡面的外部樣式表還未下載成功,這個變數就是 true。設定好 m_didSeePendingStyleSheet變數,函式 TreeResolver::resove 最終會呼叫到TreeResolver::resolveElement裡面。

TreeResolver::resolve相關程式碼如下所示:

std::unique_ptr<Update> TreeResolver::resolve()
{
    ...
    // 1. 設定 m_didSeePendingStyleSheet 變數
    m_didSeePendingStylesheet = m_document.styleScope().hasPendingSheetsBeforeBody();
    ...
    // 2. TreeResolver::resolveElement 函式由下面這個函式呼叫進去
    resolveComposedTree();
    ...
    return WTFMove(m_update);
}

上面程式碼註釋 1 處設定m_didSeePendingStyleSheet

程式碼註釋 2 處,函式 TreeResolver::resolveComposedTree會呼叫到TreeResolver::resolveElement

當外部樣式表下載完畢,仍會回撥到函式TreeResolver::resove,呼叫堆疊如下:
image

由於此時變數m_didSeePendingStyleSheet設定為false,樣式表可以正常進行匹配。

image

外部樣式表位於 body

把上面 HTML 裡面的外部樣式表挪到<body>標籤,其他不變:

<html>
	<head>
		<meta charset='utf-8' />
		<title>EasyHTML</title>
		<style text="text/css">
		/* 內部樣式表 */
		div {
			background-color: red;
		}
		</style>
	</head>
	<body>
		<!-- 外部樣式表-->
		<link rel="stylesheet" href="cs.css" />
		<div>kkk</div>
	</body>
</html>

這種情形下的匹配時機會發生變化。

如果位於<body>標籤的外部樣式標在 DOM 樹構建完成之前下載完成,那麼匹配時機和上面位於<head>標籤的外部樣式表一樣,也就是 DOM 樹構建完成就進行匹配。

如果 DOM 樹構建完成之後,位於<body>標籤的外部樣式表還未下載成功,此時由於內部樣式表已經解析完成,WebKit 會對現有已解析樣式表進行匹配,匹配完成之後會構建渲染樹,相關程式碼如下:

void Document::resolveStyle(ResolveStyleType type)
{
    ...
    Style::TreeResolver resolver(*this, WTFMove(m_pendingRenderTreeUpdate));
    // 1. 進行 CSS 樣式表匹配
    auto styleUpdate = resolver.resolve();
    ...
    if (styleUpdate) {
        // 2. 樣式表匹配完成,這裡會進行渲染樹構建
        updateRenderTree(WTFMove(styleUpdate));
        frameView.styleAndRenderTreeDidChange();
    }
    ...
    if (m_renderView->needsLayout())
        // 3. 渲染樹構建完畢,這裡會發起佈局
        frameView.layoutContext().scheduleLayout();
    ...
}

上面程式碼註釋 1 處進行 CSS 樣式表匹配。

程式碼註釋 2 處現有已解析樣式表匹配完畢,會進行渲染樹的構建。

程式碼註釋 3 處,如果條件允許,會進行佈局計算。

但是很遺憾,如果位於<body>標籤的外部樣式表沒有下載完成,因此不滿足佈局條件,程式碼執行不到上面程式碼註釋 3 處,呼叫堆疊如下:

image

雖然有了渲染樹,但是由於沒有佈局,也就不會進行繪製,在外部樣式表下載過程中,頁面同樣是白色的。CSS 樣式表下載依然阻塞渲染

下面看一下上圖判斷是否可以佈局的程式碼,程式碼如下:

bool Document::shouldScheduleLayout() const
{
    ...
    // 1. 因為 isVisuallyNonEmpty 方法返回了 false,導致了佈局條件不滿足
    if (view() && !view()->isVisuallyNonEmpty())
        return false;
   ...
    return true;
}

上面程式碼註釋 1 處由於方法LocalFrameView::isVisuallyNonEmpty返回了false,導致佈局條件不滿足。

方法LocalFrameView::isVisuallyNonEmpty程式碼如下:

bool isVisuallyNonEmpty() const { return m_contentQualifiesAsVisuallyNonEmpty; }

這個方法返回了變數m_contentQualifiesAsVisuallyNonEmpty的值,這個變數被設定為true的方法為LocalFrameView::checkAndDispatchDidReachVisuallyNonEmptyState,程式碼如下:

void LocalFrameView::checkAndDispatchDidReachVisuallyNonEmptyState()
{
    // 1. qualifiesAsVisuallyNonEmpty 回撥函式
    auto qualifiesAsVisuallyNonEmpty = [&] {
        ...
        // 2. isMoreContentExpected 回撥函式
        auto isMoreContentExpected = [&]() {
            ...
            auto& resourceLoader = documentLoader->cachedResourceLoader();
            // 3. 如果外部樣式表已經下載成功,頁面沒有其他請求,這裡返回 false,說明沒有其他內容需要載入了
            if (!resourceLoader.requestCount())
                return false;

            // 4. 如果頁面還有其他請求,程式碼執行到這裡
            auto& resources = resourceLoader.allCachedResources();
            for (auto& resource : resources) {
                ...
                if (resource.value->type() == CachedResource::Type::CSSStyleSheet || resource.value->type() == CachedResource::Type::FontResource)
                    // 5. 如果正在載入的請求裡面有樣式表型別後者字型資源,那麼這裡返回 true,說明還需要等待這些資源載入
                    return true;
            }
            return false;
        };

        // Finished parsing the main document and we still don't yet have enough content. Check if we might be getting some more.
        if (finishedParsingMainDocument)
            // 6. 呼叫 isMoreContentExpected 回撥函式
            return !isMoreContentExpected();

        return false;
    };

    if (m_contentQualifiesAsVisuallyNonEmpty)
        return;

    // 7. 呼叫 qualifiesAsVisuallyNonEmpty 回撥函式
    if (!qualifiesAsVisuallyNonEmpty())
        return;

    // 8. 這裡設定 m_contentQualifiesAsVisuallyNonEmpty 為 true
    m_contentQualifiesAsVisuallyNonEmpty = true;
    ...
}

上面程式碼註釋 1 處定義了qualifiesAsVisuallyNonEmpty回撥函式。

程式碼註釋 2 定義了isMoreContentExpected回撥函式。

程式碼註釋 7 處呼叫了回撥函式qualifiesAsVisuallyNonEmpty

qualifiesAsVisuallyNonEmpty回撥函式里面,呼叫了回撥函式isMoreContentExpected,如程式碼註釋 6 所示。

回撥函式isMoreContentExpected裡面會判斷當前是否還有其他請求,如果程式碼註釋 3 所示。如果沒有其他請求了,isMoreContentExpected 函式返回 false,表明沒有其他內容要載入了。因此,此時程式碼會執行到程式碼註釋 8 處,將變數m_contentQualifiesAsVisuallyNonEmpty設定為true

如果頁面還有其他資源的請求,比如外部樣式表還在請求,那麼回撥函式isMoreContentExpected會執行到程式碼註釋 5 處。這裡會判斷請求資源型別是否是樣式表或者字型資源,如果是這兩種資源之一,這裡返回 true。這樣,程式碼會執行到註釋 7 處,直接返回而不設定變數m_contentQualifiesAsVisuallyNonEmpty

因此,如果位於<body>標籤的外部樣式表還在下載,那麼就會在上面程式碼註釋 7 返回,所以不會進行佈局。

如果外部樣式表下載成功並解析之後,會呼叫Document::resolveStyle方法,這個方法會進行樣式表的匹配,渲染樹的構建,佈局的呼叫,程式碼如下:

void Document::resolveStyle(ResolveStyleType type)
{
        ...
        Style::TreeResolver resolver(*this, WTFMove(m_pendingRenderTreeUpdate));
        // 1. 樣式表匹配
        auto styleUpdate = resolver.resolve();
        ...
        if (styleUpdate) {
            // 2. 構建渲染樹
            updateRenderTree(WTFMove(styleUpdate));
            // 3. 設定 m_contentQualifiesAsVisuallyNonEmpty = true 的方法在這裡呼叫
            frameView.styleAndRenderTreeDidChange();
        }
        ...
        if (m_renderView->needsLayout())
            // 4. 呼叫佈局方法
            frameView.layoutContext().scheduleLayout();
        ...
}

上面程式碼註釋 1 處進行樣式表匹配。

程式碼註釋 2 進行渲染樹構建。

程式碼註釋 3 這個方法內部會呼叫LocalFrameView::checkAndDispatchDidReachVisuallyNonEmptyState方法設定變數m_contentQualifiesAsVisuallyNonEmpty。由於外部樣式表已經下載成功,此時變數m_contentQualifiesAsVisuallyNonEmpty就會被設定成true

由於上面的設定,後續程式碼註釋 4 處的佈局方法呼叫就可以成功了。

這種情形下匹配時機如下圖所示:
image

相關文章