• WebKit Inside: CSS 样式表的匹配时机


    WebKit Inside: CSS 的解析 介绍了 CSS 样式表的解析过程,这篇文章继续介绍 CSS 的匹配时机。

    无外部样式表

    内部样式表和行内样式表本身就在 HTML 里面,解析 HTML 标签构建 DOM 树时内部样式表和行内样式就会被解析完毕。因此如果 HTML 里面只有内部样式表和行内样式,那么当 DOM 树构建完毕之后,就可以进行样式表的匹配了。

    假设 HTML 里面的行内样式在

    标签,那么 CSS 匹配样式时机如下图所示:

    image

    如果 HTML 里面除了内部样式表或者行内样式,还有外部样式表,那么情形比较复杂。

    由于引入外部样式表的 标签可以位于 标签中,也可以位于标签中,这两种情形下,匹配时机不一样。

    外部样式表位于 head

    如果 HTML 里面有外部样式表和内部样式表,HTML 代码如下:

    1. <html>
    2. <head>
    3. <meta charset='utf-8' />
    4. <title>EasyHTMLtitle>
    5. <style text="text/css">
    6. /* 内部样式表 */
    7. div {
    8. background-color: red;
    9. }
    10. style>
    11. <link rel="stylesheet" href="cs.css" />
    12. head>
    13. <body>
    14. <div>kkkdiv>
    15. body>
    16. html>

    外部样式表 CSS 文件如下:

    1. div {
    2. background-color: blue;
    3. font-size: 20px;
    4. }

    如果在 DOM 树构建完成之前,外部样式表就已经下载回来并且解析,那么,当 DOM 树构建完成之后,就可以直接进行样式表的匹配。

    但是如果在 DOM 树构建完成之后,外部样式表还没有下载回来,那么即使内部样式表已经解析完成了,也不会进行任何样式表的匹配。调用堆栈如下图所示:

    image

    在函数 TreeResolver::resolveElement中,此时第一行 if里面 m_didSeePendingStyleSheet为真,因此不会进行任何样式的匹配。

    由于没有进行样式匹配,无法构建渲染树,当然也不会布局和绘制,在外部样式表的下载过程中,页面是空白的。因此 CSS 的下载虽然不阻塞 DOM 树的构建,但是阻塞渲染。

    变量m_didSeePendingStyleSheet在函数TreeResolver::resovle里面设置,如果位于 标签里面的外部样式表还未下载成功,这个变量就是 true。设置好 m_didSeePendingStyleSheet变量,函数 TreeResolver::resove 最终会调用到TreeResolver::resolveElement里面。

    TreeResolver::resolve相关代码如下所示:

    1. std::unique_ptr TreeResolver::resolve()
    2. {
    3. ...
    4. // 1. 设置 m_didSeePendingStyleSheet 变量
    5. m_didSeePendingStylesheet = m_document.styleScope().hasPendingSheetsBeforeBody();
    6. ...
    7. // 2. TreeResolver::resolveElement 函数由下面这个函数调用进去
    8. resolveComposedTree();
    9. ...
    10. return WTFMove(m_update);
    11. }

    上面代码注释 1 处设置m_didSeePendingStyleSheet

    代码注释 2 处,函数 TreeResolver::resolveComposedTree会调用到TreeResolver::resolveElement

    当外部样式表下载完毕,仍会回调到函数TreeResolver::resove,调用堆栈如下:image

    由于此时变量m_didSeePendingStyleSheet设置为false,样式表可以正常进行匹配。

    image

    外部样式表位于 body

    把上面 HTML 里面的外部样式表挪到标签,其他不变:

    1. <html>
    2. <head>
    3. <meta charset='utf-8' />
    4. <title>EasyHTMLtitle>
    5. <style text="text/css">
    6. /* 内部样式表 */
    7. div {
    8. background-color: red;
    9. }
    10. style>
    11. head>
    12. <body>
    13. <link rel="stylesheet" href="cs.css" />
    14. <div>kkkdiv>
    15. body>
    16. html>

    这种情形下的匹配时机会发生变化。

    如果位于标签的外部样式标在 DOM 树构建完成之前下载完成,那么匹配时机和上面位于标签的外部样式表一样,也就是 DOM 树构建完成就进行匹配。

    如果 DOM 树构建完成之后,位于标签的外部样式表还未下载成功,此时由于内部样式表已经解析完成,WebKit 会对现有已解析样式表进行匹配,匹配完成之后会构建渲染树,相关代码如下:

    1. void Document::resolveStyle(ResolveStyleType type)
    2. {
    3. ...
    4. Style::TreeResolver resolver(*this, WTFMove(m_pendingRenderTreeUpdate));
    5. // 1. 进行 CSS 样式表匹配
    6. auto styleUpdate = resolver.resolve();
    7. ...
    8. if (styleUpdate) {
    9. // 2. 样式表匹配完成,这里会进行渲染树构建
    10. updateRenderTree(WTFMove(styleUpdate));
    11. frameView.styleAndRenderTreeDidChange();
    12. }
    13. ...
    14. if (m_renderView->needsLayout())
    15. // 3. 渲染树构建完毕,这里会发起布局
    16. frameView.layoutContext().scheduleLayout();
    17. ...
    18. }

    上面代码注释 1 处进行 CSS 样式表匹配。

    代码注释 2 处现有已解析样式表匹配完毕,会进行渲染树的构建。

    代码注释 3 处,如果条件允许,会进行布局计算。

    但是很遗憾,如果位于标签的外部样式表没有下载完成,因此不满足布局条件,代码运行不到上面代码注释 3 处,调用堆栈如下:

    image

    虽然有了渲染树,但是由于没有布局,也就不会进行绘制,在外部样式表下载过程中,页面同样是白色的。CSS 样式表下载依然阻塞渲染

    下面看一下上图判断是否可以布局的代码,代码如下:

    1. bool Document::shouldScheduleLayout() const
    2. {
    3. ...
    4. // 1. 因为 isVisuallyNonEmpty 方法返回了 false,导致了布局条件不满足
    5. if (view() && !view()->isVisuallyNonEmpty())
    6. return false;
    7. ...
    8. return true;
    9. }

    上面代码注释 1 处由于方法LocalFrameView::isVisuallyNonEmpty返回了false,导致布局条件不满足。

    方法LocalFrameView::isVisuallyNonEmpty代码如下:

    bool isVisuallyNonEmpty() const { return m_contentQualifiesAsVisuallyNonEmpty; }

    这个方法返回了变量m_contentQualifiesAsVisuallyNonEmpty的值,这个变量被设置为true的方法为LocalFrameView::checkAndDispatchDidReachVisuallyNonEmptyState,代码如下:

    1. void LocalFrameView::checkAndDispatchDidReachVisuallyNonEmptyState()
    2. {
    3. // 1. qualifiesAsVisuallyNonEmpty 回调函数
    4. auto qualifiesAsVisuallyNonEmpty = [&] {
    5. ...
    6. // 2. isMoreContentExpected 回调函数
    7. auto isMoreContentExpected = [&]() {
    8. ...
    9. auto& resourceLoader = documentLoader->cachedResourceLoader();
    10. // 3. 如果外部样式表已经下载成功,页面没有其他请求,这里返回 false,说明没有其他内容需要加载了
    11. if (!resourceLoader.requestCount())
    12. return false;
    13. // 4. 如果页面还有其他请求,代码运行到这里
    14. auto& resources = resourceLoader.allCachedResources();
    15. for (auto& resource : resources) {
    16. ...
    17. if (resource.value->type() == CachedResource::Type::CSSStyleSheet || resource.value->type() == CachedResource::Type::FontResource)
    18. // 5. 如果正在加载的请求里面有样式表类型后者字体资源,那么这里返回 true,说明还需要等待这些资源加载
    19. return true;
    20. }
    21. return false;
    22. };
    23. // Finished parsing the main document and we still don't yet have enough content. Check if we might be getting some more.
    24. if (finishedParsingMainDocument)
    25. // 6. 调用 isMoreContentExpected 回调函数
    26. return !isMoreContentExpected();
    27. return false;
    28. };
    29. if (m_contentQualifiesAsVisuallyNonEmpty)
    30. return;
    31. // 7. 调用 qualifiesAsVisuallyNonEmpty 回调函数
    32. if (!qualifiesAsVisuallyNonEmpty())
    33. return;
    34. // 8. 这里设置 m_contentQualifiesAsVisuallyNonEmpty 为 true
    35. m_contentQualifiesAsVisuallyNonEmpty = true;
    36. ...
    37. }

    上面代码注释 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方法,这个方法会进行样式表的匹配,渲染树的构建,布局的调用,代码如下:

    1. void Document::resolveStyle(ResolveStyleType type)
    2. {
    3. ...
    4. Style::TreeResolver resolver(*this, WTFMove(m_pendingRenderTreeUpdate));
    5. // 1. 样式表匹配
    6. auto styleUpdate = resolver.resolve();
    7. ...
    8. if (styleUpdate) {
    9. // 2. 构建渲染树
    10. updateRenderTree(WTFMove(styleUpdate));
    11. // 3. 设置 m_contentQualifiesAsVisuallyNonEmpty = true 的方法在这里调用
    12. frameView.styleAndRenderTreeDidChange();
    13. }
    14. ...
    15. if (m_renderView->needsLayout())
    16. // 4. 调用布局方法
    17. frameView.layoutContext().scheduleLayout();
    18. ...
    19. }

    上面代码注释 1 处进行样式表匹配。

    代码注释 2 进行渲染树构建。

    代码注释 3 这个方法内部会调用LocalFrameView::checkAndDispatchDidReachVisuallyNonEmptyState方法设置变量m_contentQualifiesAsVisuallyNonEmpty。由于外部样式表已经下载成功,此时变量m_contentQualifiesAsVisuallyNonEmpty就会被设置成true

    由于上面的设置,后续代码注释 4 处的布局方法调用就可以成功了。

    这种情形下匹配时机如下图所示:image

  • 相关阅读:
    SpringCloud&Eureka理论与入门
    RemObjects Remoting SDK for Delphi
    比Tensorflow还强?
    EtherCAT从站EEPROM组成信息详解(1):字0-7ESC寄存器配置区
    ELK快速搭建图文详细步骤
    OpenCV分水岭分割算法2
    2024-2-22 学习笔记(Yolo-World, Yolov8-OBB,小样本分类,CNN/Transfomer选择)
    Mybatis 快速入门之 动态sql和分页
    2022-11-21 mysql列存储引擎-架构实现缺陷梳理-P1
    【单片机毕业设计】【mcuclub-jj-049】基于单片机的收纳箱的设计
  • 原文地址:https://blog.csdn.net/chaoguo1234/article/details/133643455