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

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

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

1 RenderObject 继承类图

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,渲染树开始构建。

调用栈如下图所示:

3 渲染树构建过程

3.1 相关类图

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. 创建 RenderTreeUpdater
RenderTreeUpdater 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. 找到 renderingRoot
auto* 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() 方法返回 RenderView
return &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=156172
it.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_parentStack
m_parentStack.append(Parent(root));
...
while (it != end)
{
...
// 2. 将已经创建渲染树节点的 DOM 节点,添加到 m_parentStack
pushParent(element, elementUpdate);
it.traverseNext();
}
}

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

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

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

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

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样式关系如下:

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

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

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,也就是块级元素,那么调用块级元素的 builder
if (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树类似:

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

4 RenderView 根节点

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

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

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

5 DOM 树与渲染树

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

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

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

不显示在屏幕上包括:

1 该节点不会有对应的CSS样式,不如HEAD节点。

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

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

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

WebKit Inside: 渲染树的更多相关文章

  1. WebKit Inside: DOM树的构建

    当客户端App主进程创建WKWebView对象时,会创建另外两个子进程:渲染进程与网络进程.主进程WKWebView发起请求时,先将请求转发给渲染进程,渲染进程再转发给网络进程,网络进程请求服务器.如 ...

  2. webkit浏览器渲染影响因素分析

    前言:浏览器的渲染对性能影响非常大,特别是在移动端页面,在宏观上,我们可以参考雅虎那20几条军规来操作,但在微观渲染层面,实际还没有一套相对成型的理论做为依据. 本文只是抛砖引玉,带大家进入微观的优化 ...

  3. 【浏览器渲染原理】渲染树构建之渲染树和DOM树的关系(转载 学习中。。。)

    在DOM树构建的同时,浏览器会构建渲染树(render tree).渲染树的节点(渲染器),在Gecko中称为frame,而在webkit中称为renderer.渲染器是在文档解析和创建DOM节点后创 ...

  4. 如何让360、遨游、猎豹等双核浏览器默认以webkit内核渲染网页?

    众知目前国内不少浏览器都自称双核,一般是 IE(Trident)+Webkit.因为 webkit 急速的体验和对 HTML5 的支持,有些情况下开发者可能希望用户优先甚至只使用 webkit 内核渲 ...

  5. 探索未知种族之osg类生物---状态树与渲染树以及节点树之间的关系

    节点树 首先我们来看一个场景构建的实例,并通过它来了解一下“状态节点”StateGraph 和“渲染叶”RenderLeaf 所构成的状态树,“渲染台”RenderStage 和“渲染元”Render ...

  6. RenderTree渲染树

    RenderTree渲染树对类中的静态成员有很重要的关系,这个和多态是有很重要的关系,举个简单的例子,在游戏中,马里奥需要渲染,蘑菇也需要渲染,怪兽也需要渲染,其是串在一个树上的,但是不同的类型怎么将 ...

  7. [js] 渲染树构建、布局及绘制

    渲染树构建.布局及绘制

  8. Simple2D-24 Sprite 渲染树

    如果要开发游戏,单单使用 Painter 绘制图片会变得十分复杂.如果使用 Sprite 对象进行显示,可以简单地实现图片的位移.旋转和缩放,结合 Action 对象可以实现复杂的动画效果.最重要的是 ...

  9. wkhtmltopdf是一个使用webkit网页渲染引擎开发的用来将 html转成 pdf的工具

    wkhtmltopdf是一个使用webkit网页渲染引擎开发的用来将 html转成 pdf的工具,可以跟多种脚本语言进行集成来转换文档. 官网地址 http://wkhtmltopdf.org/ gi ...

  10. 渲染树render tree

    CSSOM树和DOM树连接在一起形成一个render tree,渲染树用来计算可见元素的布局并且作为将像素渲染到屏幕上的过程的输入. DOM树和CSSOM树连接在一起形成render tree . r ...

随机推荐

  1. 我的新书《C#上位机开发实战指南》出版了

    -Begin- 大家好!我是付工. 2022年的时候,我萌生了编写一本上位机书籍的想法,希望能给更多的上位机学习者提供一些帮助,经历了2年多的时间,今年的8月份,这本书终于出版了. 初衷 十年前,我也 ...

  2. Java并发包常用类用法及原理

    com.java.util.concurrent包是java5时添加的,专门处理多线程提供的工具类 一.Atomic 二.Lock 三.BlockingQueue 四.BlockDeque 五.Con ...

  3. Linux 开发环境常用配置

    记录下我个人 Linux 开发环境常用配置,后续可以简单写个小脚本可以把一系列安装配置操作自动化一把. zsh install & conf https://blog.csdn.net/amo ...

  4. 转载:大模型所需 GPU 内存笔记

    转载文章:大模型所需 GPU 内存笔记 引言 在运行大型模型时,不仅需要考虑计算能力,还需要关注所用内存和 GPU 的适配情况.这不仅影响 GPU 推理大型模型的能力,还决定了在训练集群中总可用的 G ...

  5. Fanatastic pg walkthrough 10 Easy

    nmap 发现9090 22 和3000端口 发现漏洞 但是不知道还能读到哪些敏感文件 hacktricks 看看 https://book.hacktricks.xyz/network-servic ...

  6. 《SpringBoot》史上最全SpringBoot相关注解介绍

    @SpringBootApplication @SpringBootApplication看作是 @Configuration.@EnableAutoConfiguration.@ComponentS ...

  7. Windows的MySQL数据库升级(解压包方式)

    1.背景描述 原来的 MySQL 在安装时,是最新的稳定版本 5.7.33 . 经过一段时间后,在原来的 MySQL 版本中,发现存在漏洞. 因为 MySQL 的官方补丁,需要 Oracle 的 si ...

  8. 埋点-App层丢失率

    一.建表语句 create table dws_bhv_habo_measure_lostrate_mb_di( version_flag bigint comment '版本标签 2:web丢失 3 ...

  9. 推荐几款开源且免费的 .NET MAUI 组件库

    前言 今天大姚给大家推荐 3 款开源且免费的 .NET MAUI 组件库. .NET MAUI介绍 .NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架,用于使用 C# 和 XAML ...

  10. vue--Element-UI Table 表格指定列添加点击事件

    最近使用了Element-UI中的Table表格,因为需求需要在指定的列点击跳转,所以必须添加个点击事件,我这里是弹框展示table再点击跳转的,如图所示: 下面是我实现具体的代码(只是代码的一部分, ...