WebKit Inside: CSS 的解析 介绍了 CSS 样式表的解析过程,这篇文章继续介绍 CSS 的匹配时机。
无外部样式表
内部样式表和行内样式表本身就在 HTML 里面,解析 HTML 标签构建 DOM 树时内部样式表和行内样式就会被解析完毕。因此如果 HTML 里面只有内部样式表和行内样式,那么当 DOM 树构建完毕之后,就可以进行样式表的匹配了。
假设 HTML 里面的行内样式在
如果 HTML 里面除了内部样式表或者行内样式,还有外部样式表,那么情形比较复杂。
由于引入外部样式表的 标签可以位于
标签中,也可以位于
标签中,这两种情形下,匹配时机不一样。
外部样式表位于 head
如果 HTML 里面有外部样式表和内部样式表,HTML 代码如下:
EasyHTML
/* 内部样式表 */
div {
background-color: red;
}
kkk
外部样式表 CSS 文件如下:
div {
background-color: blue;
font-size: 20px;
}
如果在 DOM 树构建完成之前,外部样式表就已经下载回来并且解析,那么,当 DOM 树构建完成之后,就可以直接进行样式表的匹配。
但是如果在 DOM 树构建完成之后,外部样式表还没有下载回来,那么即使内部样式表已经解析完成了,也不会进行任何样式表的匹配。调用堆栈如下图所示:
在函数 TreeResolver::resolveElement
中,此时第一行 if
里面 m_didSeePendingStyleSheet
为真,因此不会进行任何样式的匹配。
由于没有进行样式匹配,无法构建渲染树,当然也不会布局和绘制,在外部样式表的下载过程中,页面是空白的。因此 CSS 的下载虽然不阻塞 DOM 树的构建,但是阻塞渲染。
变量m_didSeePendingStyleSheet
在函数TreeResolver::resovle
里面设置,如果位于 标签里面的外部样式表还未下载成功,这个变量就是
true
。设置好 m_didSeePendingStyleSheet
变量,函数 TreeResolver::resove
最终会调用到TreeResolver::resolveElement
里面。
TreeResolver::resolve
相关代码如下所示:
std::unique_ptr 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 里面的外部样式表挪到标签,其他不变:
EasyHTML
/* 内部样式表 */
div {
background-color: red;
}
kkk
这种情形下的匹配时机会发生变化。
如果位于标签的外部样式标在 DOM 树构建完成之前下载完成,那么匹配时机和上面位于
标签的外部样式表一样,也就是 DOM 树构建完成就进行匹配。
如果 DOM 树构建完成之后,位于标签的外部样式表还未下载成功,此时由于内部样式表已经解析完成,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. 渲染树构建完毕,这里会发起布局
frameVi服务器托管网ew.layoutContext().scheduleLayout();
...
}
上面代码注释 1 处进行 CSS 样式表匹配。
代码注释 2 处现有已解析样式表匹配完毕,会进行渲染树的构建。
代码注释 3 处,如果条件允许,会进行布局计算。
但是很遗憾,如果位于标签的外部样式表没有下载完成,因此不满足布局条件,代码运行不到上面代码注释 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. isM服务器托管网oreContentExpected 回调函数
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
。
因此,如果位于标签的外部样式表还在下载,那么就会在上面代码注释 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 处的布局方法调用就可以成功了。
这种情形下匹配时机如下图所示:
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net
大家好,本系列会从0开始,开发一个基于WebGPU的路径追踪渲染器,使用深度学习降噪、DLSS等AI技术实现实时渲染;并且基于自研的低代码开发平台,让用户可以通过可视化拖拽的方式快速搭建自定义的Web3D引擎 目录 回顾目前的技术积累 为什么要从0开发WebG…