WebKit Inside: CSS 样式表的匹配时机
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 处的布局方法调用就可以成功了。
这种情形下匹配时机如下图所示:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了