经过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));...}}...
}
代码注释1
,CSS
进行匹配。
代码注释2
,渲染树开始构建。
调用栈如下图所示:
3 渲染树构建过程
3.1 相关类图
Document
代表文档对象,从继承图上看,其继承自ContainerNode
。
Element
代表DOM
树节点对象,从继承图上看,其继承自ContainerNode
。
StyleUpdate
存储所有DOM
节点匹配的CSS
样式。
RenderStyle
存储单个DOM
节点匹配的CSS
样式。
RenderTreeUpdater
负责整个渲染树的构建过程。
RenderTreeBuilder
负责将渲染树节点添加到渲染树上,它持有RenderView
,RenderView
是渲染树的根节点。
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
样式。
代码注释2
,style
为当前DOM
节点匹配成功的CSS
样式。
代码注释3
,样式匹配成功,将样式与当前的DOM
节点相关联。
也就是,将当前DOM
节点与匹配的样式,存储到StyleUpdate
的m_elements
Map
中。
代码注释4
,将当前DOM
节点的父节点,添加到StyleUpdate
对象的m_roots
数组中(前提是要满足对应的条件)。
代码注释5
,将关联当前DOM
节点与匹配的样式。
代码注释6
,当匹配HTML
节点时,它的父节点是null
,因此会运行到这里,此时m_roots
数组会存储Document
对象。
代码注释7
,正常情况下,由于会满足m_elements.contains
条件,会直接返回。
比如,当匹配BODY
节点时,其父节点HMTL
已经存储在StyleUpdate
的m_elements
Map
中,因此会直接返回。
所以,正常情况下,StyleUpdate
的m_roots
数组,只会有Document
对象。
3.3.2 确认 renderingRoot
从上文可以知道,StyleUpdate
的roots
数组中,正常情况下,只有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
树。
代码注释2
,elementUpdate
中存储着当前节点匹配成功的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();}
}
代码注释1
,m_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,也就是块级元素,那么调用块级元素的 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
渲染树节点下面。
需要注意的是,参数bedoreChild
和HTML
伪元素有关,正常情况下为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
,不会出现在屏幕上,因此也没有出现在渲染树上。