WebKit Inside: 渲染树

news/2025/4/1 16:51:49/文章来源:https://www.cnblogs.com/chaoguo1234/p/18800600

经过CSS的匹配,就要进入渲染树的构建。

渲染树也叫RenderObject树,因为渲染树上每一个节点,都是RenderObject的子类。

首先来看一下RenderObject的继承类图。

1 RenderObject 继承类图

image

RenderText表示要渲染的文本。

RenderButton表示要渲染的按钮。

RenderBlockFlow表示要渲染的块级元素,比如<div>

RenderView表示浏览器window中显示的视口(viewport)。

RenderVideo表示要渲染的视频。

RenderImage表示要渲染的图片。

RenderInline表示要渲染的内联元素,比如<span>

2 渲染树构建时机

渲染树的构建时机在CSS匹配完成之后:

void Document::resolveStyle(ResolveStyleType type)
{...{// 1. CSS 匹配Style::TreeResolver resolver(*this, WTFMove(m_pendingRenderTreeUpdate));auto styleUpdate = resolver.resolve();...if (styleUpdate) {// 2. 渲染树构建updateRenderTree(WTFMove(styleUpdate));...}}...
}

代码注释1CSS进行匹配。

代码注释2,渲染树开始构建。

调用栈如下图所示:

image

3 渲染树构建过程

3.1 相关类图

image

Document代表文档对象,从继承图上看,其继承自ContainerNode

Element代表DOM树节点对象,从继承图上看,其继承自ContainerNode

StyleUpdate存储所有DOM节点匹配的CSS样式。

RenderStyle存储单个DOM节点匹配的CSS样式。

RenderTreeUpdater负责整个渲染树的构建过程。

RenderTreeBuilder负责将渲染树节点添加到渲染树上,它持有RenderViewRenderView是渲染树的根节点。

RenderTreeBuilder内部持有不同渲染树节点类型的构建器,比如块级渲染树节点构建器RnederTreeBuilder::BlockFlow

3.2 创建 RenderTreeUpdater

渲染树的构建入口函数为Document::updateRnederTree

在这个函数内部,创建了RenderTreeUpdater对象:

void Document::updateRenderTree(std::unique_ptr<Style::Update> styleUpdate)
{...{...{// 1. 创建 RenderTreeUpdaterRenderTreeUpdater updater(*this, callbackDisabler);// 2. 调用 commit 方法,继续渲染树构建updater.commit(WTFMove(styleUpdate));}}
}

代码注释1,创建RenderTreeUpdater对象。

代码注释2,继续渲染树的构建。

3.3 遍历 DOM 树前的准备

为了进行渲染树的构建,需要找到renderingRoot,对其进行遍历。

通常情况下,renderingRoot就是Document对象。

void RenderTreeUpdater::commit(std::unique_ptr<Style::Update> styleUpdate)
{...// 1. 存储 CSS 匹配结果m_styleUpdate = WTFMove(styleUpdate);...// 2. 遍历所有的 root 节点for (auto& root : m_styleUpdate->roots()) {if (&root->document() != m_document.ptr())continue;// 3. 找到 renderingRootauto* renderingRoot = findRenderingRoot(*root);if (!renderingRoot)continue;// 4. 遍历 renderingRoot,构造渲染树updateRenderTree(*renderingRoot);}...
}

代码注释1,存储CSS匹配结果。

代码注释2,遍历StyleUpdate对象中的roots数组。

从下文可以知道,正常情况下,roots数组里只有Document对象。

代码注释3,判断当前的root节点是否是一个合格的renderingRoot

代码注释4,遍历找到的renderingRoot,也就是Document对象。

3.3.1 StyleUpdate 的 root 数组

那么StyleUpdate对象中的roots数组中存储的是什么呢?

CSS匹配的过程中,当匹配完一个DOM节点的CSS样式后,会将CSS样式与这个DOM节点进行关联:

void TreeResolver::resolveComposedTree()
{...while (it != end) {...if (resolutionType) {...// 1. 匹配当前 DOM 节点 element 的样式auto [elementUpdate, elementDescendantsToResolve] = resolveElement(element, style, *resolutionType);...// 2. style 为当前 DOM 节点 element 匹配的样式style = elementUpdate.style.get();...if (style || element.hasDisplayNone())// 3. 样式匹配成功,将匹配的样式与当前的 DOM 节点相关联m_update->addElement(element, parent.element, WTFMove(elementUpdate));...}...it.traverseNext();}popParentsToDepth(1);
}void Update::addElement(Element& element, Element* parent, ElementUpdate&& elementUpdate)
{...// 4. 向 StyleUpdate 对象中的 m_roots 数组添加对象addPossibleRoot(parent);...// 5. 关联当前 DOM 节点与其匹配的样式m_elements.add(&element, WTFMove(elementUpdate));
}void Update::addPossibleRoot(Element* element)
{if (!element) {// 6. 当匹配 HTML 节点时,element = nil,Document 对象增加到 m_roots 数组中m_roots.add(m_document.ptr());return;}if (element->needsSVGRendererUpdate() || m_elements.contains(element))// 7. 正常情况下,由于满足 m_elements.contains(element) 条件,直接返回,m_roots 里始终只有 Document 对象return;m_roots.add(element);
}

代码注释1,匹配当前DOM节点的CSS样式。

代码注释2style为当前DOM节点匹配成功的CSS样式。

代码注释3,样式匹配成功,将样式与当前的DOM节点相关联。

也就是,将当前DOM节点与匹配的样式,存储到StyleUpdatem_elements Map中。

代码注释4,将当前DOM节点的父节点,添加到StyleUpdate对象的m_roots数组中(前提是要满足对应的条件)。

代码注释5,将关联当前DOM节点与匹配的样式。

代码注释6,当匹配HTML节点时,它的父节点是null,因此会运行到这里,此时m_roots数组会存储Document对象。

代码注释7,正常情况下,由于会满足m_elements.contains条件,会直接返回。

比如,当匹配BODY节点时,其父节点HMTL已经存储在StyleUpdatem_elements Map中,因此会直接返回。

所以,正常情况下,StyleUpdatem_roots数组,只会有Document对象。

3.3.2 确认 renderingRoot

从上文可以知道,StyleUpdateroots数组中,正常情况下,只有Document对象。

因此,这里的node参数就是Document对象。

static ContainerNode* findRenderingRoot(ContainerNode& node)
{if (node.renderer())// 1. Document 节点的 renderer() 方法返回 RenderViewreturn &node;return findRenderingAncestor(node);
}

代码注释1,判断当前node是否有关联的RenderObject对象。

Document对象关联的RenderObject就是RenderView,因此这里直接返回。

3.4 遍历 DOM 树

渲染树是根据DOM树渲染创建出来的。

为了创建渲染树,需要遍历DOM树.

遍历DOM树的过程与《WebKit Inside: CSS 的匹配原理》中类似,本次只关心渲染树构建的过程。

void RenderTreeUpdater::updateRenderTree(ContainerNode& root)
{ASSERT(root.renderer());ASSERT(m_parentStack.isEmpty());m_parentStack.append(Parent(root));auto descendants = composedTreeDescendants(root);auto it = descendants.begin();auto end = descendants.end();// FIXME: https://bugs.webkit.org/show_bug.cgi?id=156172it.dropAssertions();// 1. 遍历 DOM 树while (it != end) {popParentsToDepth(it.depth());auto& node = *it;...auto& element = downcast<Element>(node);...auto* elementUpdate = m_styleUpdate->elementUpdate(element);...// 2. 只有匹配到 CSS 样式的 DOM 节点,才有对应的渲染树节点if (elementUpdate)// 3. 创建当前 DOM 节点对应的渲染树节点updateElementRenderer(element, *elementUpdate);...pushParent(element, elementUpdate);it.traverseNext();}popParentsToDepth(0);
}

代码注释1,遍历DOM树。

代码注释2elementUpdate中存储着当前节点匹配成功的CSS样式,这里只有成功匹配的DOM节点,才能创建对应的渲染树节点。

因此,那些没有样式的HTML节点,比如HEAD,是不会出现在渲染树中的。

代码注释3,创建当前DOM节点对应的渲染树节点。

3.4.1 RenderTreeUpdater::Parent

上面代码中,注意到m_parentStack的代码:

void RenderTreeUpdater::updateRenderTree(ContainerNode& root)
{...// 1. 将 root 节点,也就是 Document 添加到 m_parentStackm_parentStack.append(Parent(root));...while (it != end) {...// 2. 将已经创建渲染树节点的 DOM 节点,添加到 m_parentStackpushParent(element, elementUpdate);it.traverseNext();}
}

代码注释1m_parentStack中加入的Parent对象,并不是《WebKit Inside: CSS 的匹配原理》中的Style::TreeResolver::Parent,而是RenderTreeUpdater::Parent

RenderTreeUpdater::Parent相关的类图如下:

image

代码注释2,当前DOM节点已经创建好了渲染树节点,将当前DOM节点以及其匹配的样式,添加到m_parentStack中。

下图给出了一个遍历DOM树时,m_parentStack变化的例子:

image

3.5 创建渲染树节点

void RenderTreeUpdater::updateElementRenderer(Element& element, const Style::ElementUpdate& elementUpdate)
{if (!elementUpdate.style)// 1. 没有匹配 CSS 样式的 DOM 节点不会创建对应的渲染树节点return;...// 2. 如果当前 DOM 节点 display 属性为 none,也不会创建渲染树节点bool shouldCreateNewRenderer = !element.renderer() && !hasDisplayContentsOrNone && !(element.isInTopLayer() && renderTreePosition().parent().style().hasSkippedContent());if (shouldCreateNewRenderer) {...// 3. 创建当前 DOM 节点的渲染树节点createRenderer(element, WTFMove(elementUpdateStyle));...return;}...
}

代码注释1,判断当前DOM节点有没有匹配CSS样式。

没有匹配CSS样式的DOM节点不会创建对应的渲染树节点。

代码注释2,判断当前DOM节点是否可见。

如果当前DOM节点的display属性值为none,那么也不会创建对应的渲染树节点。

代码注释3,为当前的DOM节点创建对应的渲染树节点,并添加到渲染树上。

3.5.1 渲染树节点

上面代码注释3处的函数真正的创建渲染树节点,代码如下:

void RenderTreeUpdater::createRenderer(Element& element, RenderStyle&& style)
{...// 1. 获取当前创建的渲染树节点,要插入的位置RenderTreePosition insertionPosition = computeInsertionPosition();// 2. 创建当前 DOM 节点的渲染树节点auto newRenderer = element.createElementRenderer(WTFMove(style), insertionPosition);if (!newRenderer)return;if (!insertionPosition.parent().isChildAllowed(*newRenderer, newRenderer->style()))return;...// 3. 将创建的渲染树节点,与对应的 DOM 节点关联element.setRenderer(newRenderer.get());...// 4. 将创建的渲染树节点,添加到渲染树上m_builder.attach(insertionPosition.parent(), WTFMove(newRenderer), insertionPosition.nextSibling());...
}

代码注释1,获取当前要创建的渲染树节点,其插入的位置。

RenderTreePosition前面介绍过,它持有当前DOM节点的父节点,以及父渲染树节点。

代码注释2,创建当前DOM节点的渲染树节点。

不同的DOM树节点,会覆写createElementRender方法,从而创建不同的渲染树节点。

比如,<img>节点会创建RenderImage类型的渲染树节点。

比如,<div>这种块级标签,会创建RenderBlockFlow类型的渲染树节点。

创建好的渲染树节点,与其对应的DOM节点以及匹配的CSS样式关系如下:

image

代码注释3,将当前DOM节点与创建好的渲染树节点相关联。

这样,DOM节点与渲染树节点,可以相互引用了。

image

3.6 添加渲染树节点

上面代码注释4,将新创建的渲染树节点,添加到渲染树上。

RenderTreeBuilder::attach方法接收3个参数:
1个参数,是当前要添加渲染树节点的父渲染树节点。

2个参数,是要添加的渲染树节点。

3个参数,与HTML伪元素有关,正常情况下为null

RenderTreeBuilder::attach方法会调用到RenderTreeBuilder::attachInternal方法。

RenderTreeBuilder::attachInternal方法中,会根据当前渲染树节点的父渲染树节点类型,调用具体的Builder:

void RenderTreeBuilder::attachInternal(RenderElement& parent, RenderPtr<RenderObject> child, RenderObject* beforeChild)
{...// 1. 如果父渲染树节点是 RenderBlockFlow,也就是块级元素,那么调用块级元素的 builderif (auto* parentBlockFlow = dynamicDowncast<RenderBlockFlow>(parent)) {blockFlowBuilder().attach(*parentBlockFlow, WTFMove(child), beforeChild);return;}...
}

代码注释1,给出了块级父渲染树节点类型的例子。

如果父渲染树节点是RenderBlockFlow类型,也就是块级元素,那么就调用块级元素的Builder

在具体的Builder内部,会有一些额外的操作,但是最终的添加过程,还是会调用到RenderTreeBuilder中:

void RenderTreeBuilder::attachToRenderElementInternal(RenderElement& parent, RenderPtr<RenderObject> child, RenderObject* beforeChild)
{...// Take the ownership.// 1. 将 child 渲染树节点,添加到 parent 渲染树节点下面auto* newChild = parent.attachRendererInternal(WTFMove(child), beforeChild);...
}

代码注释1,将child渲染树节点,添加到parent渲染树节点下面。

需要注意的是,参数bedoreChildHTML伪元素有关,正常情况下为null

添加的主要过程代码为:

RenderObject* RenderElement::attachRendererInternal(RenderPtr<RenderObject> child, RenderObject* beforeChild)
{child->setParent(this);......{CheckedPtr lastChild = m_lastChild.get();if (lastChild)lastChild->setNextSibling(child.get());child->setPreviousSibling(lastChild.get());}m_lastChild = child.get();return child.release();
}

如果之前看过《WebKit Inside: DOM 树的构建》,会发现渲染树在内存中的结构,和DOM树类似:

image

但是习惯上,常常会将渲染树画成下面的逻辑结构,这样更方便:

image

4 RenderView 根节点

上面提到RenderView是渲染树的根节点。

那根节点RenderView是什么时候创建的呢?

答案就是,创建Document对象时,会将RenderView创建出来:

image

5 DOM 树与渲染树

从前面的介绍可以知道,渲染树是遍历DOM树创建出来的。

但是,并不是每一个DOM树上的节点,在渲染树上都有对应的节点。

如果DOM树上的节点,不会显示在屏幕上,那么,渲染树上就不会有相应的节点。

不显示在屏幕上包括:
1 该节点不会有对应的CSS样式,不如HEAD节点。

2 即使有CSS样式,但是display属性值为none,也不会在渲染树上。

image

上面图中,<head>节点由于不会显示在屏幕上,没有出现在渲染树上。

<h2>节点因为display属性值为none,不会出现在屏幕上,因此也没有出现在渲染树上。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/908125.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用Chat2DB操作WINCC的Microsoft SQL Server数据库

配置WINCC软件自动安装的Microsoft SQL Server 数据库,使之可以被外部程序访问2025年3月30日 11:59 周日 记录 WINCC软件安装时,会同时安装对应版本的Microsoft SQL Server 数据库。 该记录主要是配置WINCC软件自动安装的Microsoft SQL Server 数据库,使之可以被外部程序访问…

GO单元测试-工具

本文介绍如何在GO语言中编写单元测试,主要内容包括:标准库中的testing包,第三方框架testify和mockery工具,monkey patching框架gomonkey,以及如何查看覆盖率。GO单元测试 摘要 本文介绍如何在GO语言中编写单元测试,主要内容包括:标准库中的testing包,第三方框架testify…

36.7K star!拖拽构建AI流程,这个开源LLM应用框架绝了!

`Flowise` 是一款革命性的低代码LLM应用构建工具,开发者通过可视化拖拽界面,就能快速搭建基于大语言模型的智能工作流。该项目在GitHub上线不到1年就斩获**36.7K星标**,被开发者誉为"AI时代的乐高积木"。36.7K star!拖拽构建AI流程,这个开源LLM应用框架绝了! 只…

Tandis 解法集()

1-x 的关卡都比较简单。 2-1: 现在我们有一个常驻的旋转操作。 和某个 1-x 的关卡一样处理即可。 2-2: 开始起飞了。 操作类似于一个极坐标变换:把方块平放即可得到圆。 2-3如果要通过这种变换得到一个中心对称图形,那左侧放的一定也是长度恰好为 8 的倒下的柱体。 而柱体的切…

Java24发布,精心总结

Java 24作为2025年3月发布的最新版本,延续了Java平台每半年发布一次的节奏,带来了24项重要改进。本文将按照核心改进领域分类,详细解析每个特性的技术原理和实际价值,帮助开发者全面了解这一版本的能力边界和应用场景。 不过Java24是自Java 21 以来的第三个非长期支持版本,…

一台电脑上快速切换git账号

如果你的一台笔记本,既要开发公司的项目,同时你又要参与github,或者是gitee上的开源项目。你就需要使用不同的账号来提交代码。如何快速、高效的切换和管理不同的git账号? 本人使用的就是这种方式,只要配置好,会自动切换的。本文来自博客园,作者:Eular,转载请注明原文…

从 0 到 1 打造代码扫描工具:实战指南与技术解析

在团队协作开发的场景中,代码规范的重要性不言而喻。当团队规模逐渐扩大,如何确保每个人提交的代码都符合规范,比如不能 import *、代码嵌套不能超3层,代码包层级依赖结构约定、 不能修改核心文件等,成为了一个亟待解决的问题。今天,咱们就来聊聊如何开发一个代码扫描工具…

深入解析Java Web开发中的异常处理机制:策略、实践与案例分析

一、引言 1. Java Web开发概述 Java Web开发是基于Java语言构建网络应用程序的过程,它通过Java Servlet、JSP(JavaServer Pages)、Spring MVC等技术,实现动态网页的生成和交互。Java Web应用广泛应用于企业级系统、电子商务平台、在线教育等领域。在这些应用中,用户通过浏…

Java Web开发中的请求与响应机制

一、Java Web开发基础概念 (一)Java Web开发概述 Java Web开发是指使用Java语言及相关技术开发基于Web的应用程序。它主要通过Java Servlet、JSP(JavaServer Pages)、Spring MVC等技术实现客户端与服务器之间的交互。Java Web应用广泛应用于企业级应用、电子商务平台、在线…

GUI猜数字

序言 本文将会介绍“GUI猜数字”这款原创软件的界面、功能、编译等。详见后文~下载地址开始前先晾出下载地址: https://biaozyx.lanzouq.com/i31nk2rcftsj (提取码:guiGN)内容展示 所含文件 GuessNumber.exe(Windows可执行程序) GuessNumber.py(源代码,可用后面教程编译…

Apple Final Cut Pro 11.1 - 专业后期制作 (视频剪辑)

Apple Final Cut Pro 11.1 - 专业后期制作 (视频剪辑)Apple Final Cut Pro 11.1 - 专业后期制作 (视频剪辑) Final Cut Pro 11.1.0 + Compressor 4.10.0 + Motion 5.10.0 请访问原文链接:https://sysin.org/blog/apple-final-cut-pro/ 查看最新版。原创作品,转载请保留出处。…

Adobe Photoshop 2025 v26.5 (macOS, Windows) - 照片和设计软件

Adobe Photoshop 2025 v26.5 (macOS, Windows) - 照片和设计软件Adobe Photoshop 2025 v26.5 (macOS, Windows) - 照片和设计软件 Acrobat、After Effects、Animate、Audition、Bridge、Character Animator、Dimension、Dreamweaver、Illustrator、InCopy、InDesign、Lightroo…