惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
P
Privacy International News Feed
Security Latest
Security Latest
Spread Privacy
Spread Privacy
NISL@THU
NISL@THU
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Last Watchdog
The Last Watchdog
S
Schneier on Security
Engineering at Meta
Engineering at Meta
Google Online Security Blog
Google Online Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Know Your Adversary
Know Your Adversary
Cyberwarzone
Cyberwarzone
C
Cyber Attacks, Cyber Crime and Cyber Security
G
GRAHAM CLULEY
A
Arctic Wolf
博客园 - 聂微东
I
Intezer
腾讯CDC
罗磊的独立博客
T
Tailwind CSS Blog
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
L
Lohrmann on Cybersecurity
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
博客园 - 【当耐特】
P
Palo Alto Networks Blog
Simon Willison's Weblog
Simon Willison's Weblog
W
WeLiveSecurity
N
News and Events Feed by Topic
SecWiki News
SecWiki News
S
Security Affairs
T
Threat Research - Cisco Blogs
The GitHub Blog
The GitHub Blog
F
Fortinet All Blogs
T
Troy Hunt's Blog
N
News and Events Feed by Topic
P
Proofpoint News Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
酷 壳 – CoolShell
酷 壳 – CoolShell
M
MIT News - Artificial intelligence
I
InfoQ
Hacker News: Ask HN
Hacker News: Ask HN
T
The Blog of Author Tim Ferriss
Schneier on Security
Schneier on Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
Vercel News
Vercel News
博客园 - 三生石上(FineUI控件)
D
Darknet – Hacking Tools, Hacker News & Cyber Security

博客园 - chaoguo1234

如何自定义 UIScrollView 的减速系数 Plist 二进制格式 UIImageView 设置图片不生效的原因排查 Any metadata 的内存布局 Mach-O 文件结构 Block Copy 的内存布局详解 __block 变量内存布局详解 Block 内存布局详解 XNU Inside: iOS 模拟器 WebKit Insie: WebKit 调试(二) NSMutableDictionary 的内存布局 NSDictionary 内存布局 WebKit Inside: GPU 进程调试 WebKit Inside: px 与 pt WebKit Inside: CSS 的匹配原理 dyld: 神秘的 __dso_handle 一文搞懂 ARM 64 系列: ADD(立即数版) 一文搞懂 ARM 64: AUTIBSP 一文搞懂 ARM 64 系列: PACISB XCode 编译 PAG 源码 一文搞懂 ARM 64 系列: 一文搞懂 ARM 64 系列: 函数调用传参与返回值 一文搞懂 ARM 64 系列: 寄存器
WebKit Inside: 渲染树
chaoguo1234 · 2025-03-30 · via 博客园 - chaoguo1234

经过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. 创建 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相关的类图如下:

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

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,不会出现在屏幕上,因此也没有出现在渲染树上。