找回密码
 立即注册
首页 业界区 业界 WebKit Inside: 渲染树

WebKit Inside: 渲染树

泠邸 5 天前
经过CSS的匹配,就要进入渲染树的构建。
渲染树也叫RenderObject树,因为渲染树上每一个节点,都是RenderObject的子类。
首先来看一下RenderObject的继承类图。
1 RenderObject 继承类图


RenderText表示要渲染的文本。
RenderButton表示要渲染的按钮。
RenderBlockFlow表示要渲染的块级元素,比如。
RenderView表示浏览器window中显示的视口(viewport)。
RenderVideo表示要渲染的视频。
RenderImage表示要渲染的图片。
RenderInline表示要渲染的内联元素,比如。
2 渲染树构建时机

渲染树的构建时机在CSS匹配完成之后:
  1. 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对象:
  1. void Document::updateRenderTree(std::unique_ptr styleUpdate){    ...    {      ...        {            // 1. 创建 RenderTreeUpdater            RenderTreeUpdater updater(*this, callbackDisabler);            // 2. 调用 commit 方法,继续渲染树构建            updater.commit(WTFMove(styleUpdate));        }    }}
复制代码
代码注释1,创建RenderTreeUpdater对象。
代码注释2,继续渲染树的构建。
3.3 遍历 DOM 树前的准备

为了进行渲染树的构建,需要找到renderingRoot,对其进行遍历。
通常情况下,renderingRoot就是Document对象。
  1. void RenderTreeUpdater::commit(std::unique_ptr 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节点进行关联:
  1. 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对象。
  1. 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 的匹配原理》中类似,本次只关心渲染树构建的过程。
  1. 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(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的代码:
  1. 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();   }}
复制代码
代码注释1,m_parentStack中加入的Parent对象,并不是《WebKit Inside: CSS 的匹配原理》中的Style::TreeResolver::Parent,而是RenderTreeUpdater::Parent。
与RenderTreeUpdater::Parent相关的类图如下:

代码注释2,当前DOM节点已经创建好了渲染树节点,将当前DOM节点以及其匹配的样式,添加到m_parentStack中。
下图给出了一个遍历DOM树时,m_parentStack变化的例子:

3.5 创建渲染树节点
  1. 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处的函数真正的创建渲染树节点,代码如下:
  1. 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类型的渲染树节点。
比如,这种块级标签,会创建RenderBlockFlow类型的渲染树节点。
创建好的渲染树节点,与其对应的DOM节点以及匹配的CSS样式关系如下:

代码注释3,将当前DOM节点与创建好的渲染树节点相关联。
这样,DOM节点与渲染树节点,可以相互引用了。

3.6 添加渲染树节点

上面代码注释4,将新创建的渲染树节点,添加到渲染树上。
RenderTreeBuilder::attach方法接收3个参数:
第1个参数,是当前要添加渲染树节点的父渲染树节点。
第2个参数,是要添加的渲染树节点。
第3个参数,与HTML伪元素有关,正常情况下为null。
RenderTreeBuilder::attach方法会调用到RenderTreeBuilder::attachInternal方法。
在RenderTreeBuilder::attachInternal方法中,会根据当前渲染树节点的父渲染树节点类型,调用具体的Builder:
  1. void RenderTreeBuilder::attachInternal(RenderElement& parent, RenderPtr child, RenderObject* beforeChild){    ...    // 1. 如果父渲染树节点是 RenderBlockFlow,也就是块级元素,那么调用块级元素的 builder    if (auto* parentBlockFlow = dynamicDowncast(parent)) {        blockFlowBuilder().attach(*parentBlockFlow, WTFMove(child), beforeChild);        return;    }   ...}
复制代码
代码注释1,给出了块级父渲染树节点类型的例子。
如果父渲染树节点是RenderBlockFlow类型,也就是块级元素,那么就调用块级元素的Builder。
在具体的Builder内部,会有一些额外的操作,但是最终的添加过程,还是会调用到RenderTreeBuilder中:
  1. void RenderTreeBuilder::attachToRenderElementInternal(RenderElement& parent, RenderPtr child, RenderObject* beforeChild){   ...   // Take the ownership.   // 1. 将 child 渲染树节点,添加到 parent 渲染树节点下面   auto* newChild = parent.attachRendererInternal(WTFMove(child), beforeChild);   ...}
复制代码
代码注释1,将child渲染树节点,添加到parent渲染树节点下面。
需要注意的是,参数bedoreChild和HTML伪元素有关,正常情况下为null。
添加的主要过程代码为:
  1. RenderObject* RenderElement::attachRendererInternal(RenderPtr 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,也不会在渲染树上。

上面图中,节点由于不会显示在屏幕上,没有出现在渲染树上。
节点因为display属性值为none,不会出现在屏幕上,因此也没有出现在渲染树上。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册