WebKit Inside: CSS 的解析 介紹了 CSS 樣式表的解析過程,這篇文章繼續介紹 CSS 的匹配時機。
無外部樣式表
內部樣式表和行內樣式表本身就在 HTML 裡面,解析 HTML 標籤構建 DOM 樹時內部樣式表和行內樣式就會被解析完畢。因此如果 HTML 裡面只有內部樣式表和行內樣式,那麼當 DOM 樹構建完畢之後,就可以進行樣式表的匹配了。
假設 HTML 裡面的行內樣式在 <div>
標籤,那麼 CSS 匹配樣式時機如下圖所示:
如果 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 樹構建完成之後,外部樣式表還沒有下載回來,那麼即使內部樣式表已經解析完成了,也不會進行任何樣式表的匹配。呼叫堆疊如下圖所示:
在函式 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
,呼叫堆疊如下:
由於此時變數m_didSeePendingStyleSheet
設定為false
,樣式表可以正常進行匹配。
外部樣式表位於 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 處,呼叫堆疊如下:
雖然有了渲染樹,但是由於沒有佈局,也就不會進行繪製,在外部樣式表下載過程中,頁面同樣是白色的。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 處的佈局方法呼叫就可以成功了。
這種情形下匹配時機如下圖所示: