Chromium网页DOM Tree创建过程分析

发表于 5年以前  | 总阅读数:1887 次

在Chromium中,Render进程是通过Browser进程下载网页内容的,后者又是通过共享内存将下载回来的网页内容交给前者的。Render进程获得网页内容之后,会交给WebKit进行处理。WebKit所做的第一个处理就是对网页内容进行解析,解析的结果是得到一棵DOM Tree。DOM Tree是网页的一种结构化描述,也是网页渲染的基础。本文接下来就对网页DOM Tree的创建过程进行详细分析。

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

网页的DOM Tree的根节点是一个Document。Document是依附在一个DOM Window之上。DOM Window又是和一个Frame关联在一起的。Document、DOM Window和Frame都是WebKit里面的概念,其中Frame又是和Chromium的Content模块中的Render Frame相对应的。Render Frame是和网页的Frame Tree相关的一个概念。关于网页的Frame Tree,可以参考前面Chromium Frame Tree创建过程分析一文。

上面描述的各种对象的关系可以通过图1描述,如下所示:

图1 Frame、DOM Window和Document的关系

从前面Chromium Frame Tree创建过程分析一文可以知道,有的Render Frame只是一个Proxy,称为Render Frame Proxy。Render Frame Proxy描述的是在另外一个Render进程中进行加载和渲染的网页。这种网页在WebKit里面对应的Frame和DOM Window分别称为Remote Frame和Remote DOM Window。由于Render Frame Proxy描述的网页不是在当前Render进程中加载和渲染,因此它是没有Document的。

相应地,Render Frame描述的是在当前Render进程中进行加载和渲染的网页,它是具有Document的,并且这种网页在WebKit里面对应的Frame和DOM Window分别称为Local Frame和Local DOM Window。

从图1我们还可以看到,在Render Frame和Local Frame之间,以及Render Frame Proxy和Remote Frame之间,分别存在一个Web Local Frame和Web Remote Frame。Web Local Frame和Web Remote Frame是属于WebKit Glue层的概念。从前面Chromium网页加载过程简要介绍和学习计划一文可以知道,WebKit Glue层的作用是将WebKit的对象类型转化为Chromium的对象类型,这样Chromium的Content层就可以用统一的、自有的方式管理所有的对象。关于Chromium的层次划分和每一个层次的作用,可以参考前面Chromium网页加载过程简要介绍和学习计划一文。

除了根节点,也就是Document节点,DOM Tree的每一个子结点对应的都是网页里面的一个HTML标签。并不是所有的HTML标签都是需要渲染的,例如script标签就不需要进行渲染。对于需要渲染的HTML标签,它们会关联有一个Render Object。这些Render Object会形成一个Render Object Tree,如图2所示:

图2 DOM Tree与Render Object Tree、Render Layer Tree和Graphics Layer Tree的关系

为了便于执行绘制操作,具有相同坐标空间的Render Object会绘制在同一个Render Layer中。这些Render Layer又会形成一个Render Layer Tree。绘制操作是由图形渲染引擎执行的。对于图形渲染引擎来说,Layer是一个具有后端存储的概念。在软件渲染模式中,Layer的后端存储实际上就是一个内存缓冲区。在硬件渲染模式中,Layer的后端存储实际上就是一个FBO。为了节约资源,WebKit不会为每一个Render Layer都分配一个后端存储,而是会让某些Render Layer共用其它的Render Layer的后端存储。那些具有自己的后端存储的Render Layer,又称为Graphics Layer。这些Graphics Layer又形成了一个Graphics Layer Tree。

Render Object Tree、Render Layer Tree和Graphics Layer Tree都是和网页渲染相关概念,它们是从DOM Tree发展而来的。因此,在分析网页的渲染机制之前,有必要了解网页的DOM Tree的创建过程。

DOM Tree的创建发生在WebKit解析网页内容的过程中。WebKit在解析网页内容的时候,会用到一个栈。每当碰到一个HTML标签的起始Token,就会将其压入栈中,而当碰到该HTML标签的结束Token时,就会将其弹出栈。在这些HTML标签的压栈和出栈过程中,就可以得到一棵DOM Tree。以图2所示的DOM Tree片段为例,它对应的网页内容为:

<div>
        <p>
            <div></div>
        </p>
        <span></span>
    </div>

各个标签的压栈和出栈过程如图3所示:

图3 网页内容解析过程中的HTML标签压栈和出栈操作

接下来,我们就结合源码分析WebKit在解析网页内容的过程中创建DOM Tree的过程。从前面Chromium网页URL加载过程分析一文可以知道,Browser进程一边下载网页的内容,一边将下载回来的网页交给Render进程的Content模块。Render进程的Content模块经过简单的处理之后,又会交给WebKit进行解析。WebKit是从ResourceLoader类的成员函数didReceiveData开始接收Chromium的Content模块传递过来的网页内容的,因此我们就从这个函数开始分析WebKit解析网页内容的过程,也就是网页DOM Tree的创建过程。

ResourceLoader类的成员函数didReceiveData的实现如下所示:

void ResourceLoader::didReceiveData(blink::WebURLLoader*, const char* data, int length, int encodedDataLength)
    {
        ......

        m_resource->appendData(data, length);
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/fetch/ResourceLoader.cpp中。

ResourceLoader类的成员变量m_resource描述的是一个RawResource对象。这个RawResource对象的创建过程可以参考前面Chromium网页URL加载过程分析一文。ResourceLoader类的成员函数didReceiveData调用这个RawResource对象的成员函数appendData处理下载回来的网页内容。

RawResource类的成员函数appendData的实现如下所示:

void RawResource::appendData(const char* data, int length)
    {
        Resource::appendData(data, length);

        ResourcePtr<RawResource> protect(this);
        ResourceClientWalker<RawResourceClient> w(m_clients);
        while (RawResourceClient* c = w.next())
            c->dataReceived(this, data, length);
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/fetch/RawResource.cpp中。

RawResource类的成员函数appendData主要是调用保存在成员变量m_clients中的每一个RawResourceClient对象的成员函数dataReceived,告知它们从Web服务器中下载回来了新的数据。

从前面Chromium网页URL加载过程分析一文可以知道,在RawResource类的成员变量m_clients中,保存有一个DocumentLoader对象。这个DocumentLoader对象是从RawResourceClient类继承下来的,它负责创建和加载网页的文档对象。接下来我们就继续分析它的成员函数dataReceived的实现,如下所示:

void DocumentLoader::dataReceived(Resource* resource, const char* data, int length)
    {
        .....

        commitData(data, length);

        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/loader/DocumentLoader.cpp中。

DocumentLoader类的成员函数dataReceived主要是调用另外一个成员函数commitData处理从Web服务器下载回来的网页数据,后者的实现如下所示:

void DocumentLoader::commitData(const char* bytes, size_t length)
    {
        ensureWriter(m_response.mimeType());
        ......
        m_writer->addData(bytes, length);
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/loader/DocumentLoader.cpp中。

DocumentLoader类的成员函数commitData首先调用成员函数ensureWriter确定成员变量m_writer指向了一个DocumentWriter对象,因为接下来要调用这个DocumentWriter对象的成员函数addData对下载回来的网页数据进行解析。

接下来,我们首先分析DocumentLoader类的成员函数ensureWriter的实现,接下来再分析DocumentWriter类的成员函数addData的实现。

DocumentLoader类的成员函数ensureWriter的实现如下所示:

void DocumentLoader::ensureWriter(const AtomicString& mimeType, const KURL& overridingURL)
    {
        if (m_writer)
            return;

        ......
        m_writer = createWriterFor(m_frame, 0, url(), mimeType, encoding, false, false);

        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/loader/DocumentLoader.cpp中。

DocumentLoader类的成员函数ensureWriter首先检查成员变量m_writer是否指向了一个DocumentWriter对象。如果已经指向,那么就什么也不用做就直接返回。否则的话,就会调用另外一个成员函数createWriterFor为当前正在加载的URL创建一个DocumentWriter对象,并且保存在成员变量m_writer中。

DocumentLoader类的成员函数createWriterFor的实现如下所示:

PassRefPtrWillBeRawPtr<DocumentWriter> DocumentLoader::createWriterFor(LocalFrame* frame, const Document* ownerDocument, const KURL& url, const AtomicString& mimeType, const AtomicString& encoding, bool userChosen, bool dispatch)
    {
        ......

        // In some rare cases, we'll re-used a LocalDOMWindow for a new Document. For example,
        // when a script calls window.open("..."), the browser gives JavaScript a window
        // synchronously but kicks off the load in the window asynchronously. Web sites
        // expect that modifications that they make to the window object synchronously
        // won't be blown away when the network load commits. To make that happen, we
        // "securely transition" the existing LocalDOMWindow to the Document that results from
        // the network load. See also SecurityContext::isSecureTransitionTo.
        bool shouldReuseDefaultView = frame->loader().stateMachine()->isDisplayingInitialEmptyDocument() && frame->document()->isSecureTransitionTo(url);
        ......

        if (!shouldReuseDefaultView)
            frame->setDOMWindow(LocalDOMWindow::create(*frame));

        RefPtrWillBeRawPtr<Document> document = frame->domWindow()->installNewDocument(mimeType, init);
        ......

        return DocumentWriter::create(document.get(), mimeType, encoding, userChosen);
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/loader/DocumentLoader.cpp中。

从前面的调用过程可以知道,参数frame描述的LocalFrame对象来自于DocumentLoader类的成员变量m_frame,这个LocalFrame对象描述的是一个在当前Render进程中进行加载的网页。

如果当前正在加载的网页是通过JavaScript接口window.open打开的,那么参数frame描述的LocalFrame对象已经关联有一个默认的DOM Window。在符合安全规则的情况下,这个默认的DOM Window将会被使用。如果不符合安全规则,或者当前加载的网页不是通过JavaScript接口window.open打开的,那么就需要为参数frame描述的LocalFrame对象创建一个新的DOM Window。这是通过调用LocalDOMWindow类的静态成员函数create创建的,如下所示:

namespace WebCore {
        ......

        class LocalDOMWindow FINAL : public RefCountedWillBeRefCountedGarbageCollected<LocalDOMWindow>, public ScriptWrappable, public EventTargetWithInlineData, public DOMWindowBase64, public FrameDestructionObserver, public WillBeHeapSupplementable<LocalDOMWindow>, public LifecycleContext<LocalDOMWindow> {
            ......

            static PassRefPtrWillBeRawPtr<LocalDOMWindow> create(LocalFrame& frame)
            {
                return adoptRefWillBeRefCountedGarbageCollected(new LocalDOMWindow(frame));
            }

            ......
        };

        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/frame/LocalDOMWindow.h中。

LocalDOMWindow类的静态成员函数create为参数frame指向的一个LocalFrame对象创建的是一个类型为LocalDOMWindow的DOM Window,这是因为参数frame指向的LocalFrame对象描述的是一个在当前Render进程加载的网页。

回到DocumentLoader类的成员函数createWriterFor中,它调用LocalDOMWindow类的静态成员函数create创建了一个LocalDOMWindow对象之后,会将这个LocalDOMWindow对象设置给参数frame描述的LocalFrame对象。这是通过调用LocalFrame类的成员函数setDOMWindow实现的。

LocalFrame类的成员函数setDOMWindow的实现如下所示:

void LocalFrame::setDOMWindow(PassRefPtrWillBeRawPtr<LocalDOMWindow> domWindow)
    {
        ......
        Frame::setDOMWindow(domWindow);
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/frame/LocalFrame.cpp中。

LocalFrame类的成员函数setDOMWindow会将参数domWindow描述的一个LocalDOMWindow对象交给父类Frame处理,这是通过调用父类Frame的成员函数setDOMWindow实现的。

Frame类的成员函数setDOMWindow的实现如下所示:

void Frame::setDOMWindow(PassRefPtrWillBeRawPtr<LocalDOMWindow> domWindow)
    {
        ......
        m_domWindow = domWindow;
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/frame/Frame.cpp中。

Frame类的成员函数setDOMWindow将参数domWindow描述的一个LocalDOMWindow对象保存在成员变量m_domWindow中。以后就可以通过调用Frame类的成员函数domWindow获得这个LocalDOMWindow对象,如下所示:

inline LocalDOMWindow* Frame::domWindow() const
    {
        return m_domWindow.get();
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/frame/Frame.h中。

这一步执行完成之后,WebKit就为一个类型为LocaFrame的Frame创建了一个类型为LocalDOMWindow的DOM Window,正如图1所示。回到DocumentLoader类的成员函数createWriterFor中,接下来它会继续为上面创建的类型为LocalDOMWindow的DOM Window创建一个Document。这是通过调用LocalDOMWindow类的成员函数installNewDocument实现的,如下所示:

PassRefPtrWillBeRawPtr<Document> LocalDOMWindow::installNewDocument(const String& mimeType, const DocumentInit& init, bool forceXHTML)
    {
        ......

        m_document = createDocument(mimeType, init, forceXHTML); 
        ......
        m_document->attach();

        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/frame/LocalDOMWindow.cpp中。

LocalDOMWindow类的成员函数installNewDocument首先调用另外一个成员函数createDocument创建一个HTMLDocument对象,并且保存在成员变量m_document中,接下来又调用这个HTMLDocument对象的成员函数attach为其创建一个Render View。这个Render View即为图2所示的Render Object Tree的根节点。

接下来我们首先分析LocalDOMWindow类的成员函数createDocument的实现,接着再分析HTMLDocument类的成员函数attach的实现。

LocalDOMWindow类的成员函数createDocument的实现如下所示:

PassRefPtrWillBeRawPtr<Document> LocalDOMWindow::createDocument(const String& mimeType, const DocumentInit& init, bool forceXHTML)
    {
        RefPtrWillBeRawPtr<Document> document = nullptr;
        if (forceXHTML) {
            // This is a hack for XSLTProcessor. See XSLTProcessor::createDocumentFromSource().
            document = Document::create(init);
        } else {
            document = DOMImplementation::createDocument(mimeType, init, init.frame() ? init.frame()->inViewSourceMode() : false);
            ......
        }

        return document.release();
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/frame/LocalDOMWindow.cpp中。

当参数forceXHTML的值等于true的时候,表示当前加载的网页的MIME Type为"text/plain",这时候LocalDOMWindow类的成员函数createDocument调用Document类的静态成员函数create为其创建一个类型Document的Document。 我们考虑当前加载的网页的MIME Type为"text/html",这时候LocalDOMWindow类的成员函数createDocument调用DOMImplementation类的成员函数createDocument为当前正在加载的网页创建一个类型为HTMLDocument的Document。

DOMImplementation类的成员函数createDocument的实现如下所示:

PassRefPtrWillBeRawPtr<Document> DOMImplementation::createDocument(const String& type, const DocumentInit& init, bool inViewSourceMode)
    {
        ......

        if (type == "text/html")
            return HTMLDocument::create(init);

        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/DOMImplementation.cpp中。

从这里可以看到,如果当前正在加载的网页的MIME Type为"text/html",那么DOMImplementation类的成员函数createDocument就会调用HTMLDocument类的静态成员函数create创建一个Document。

HTMLDocument类的静态成员函数create的实现如下所示:

class HTMLDocument : public Document, public ResourceClient {
    public:
        static PassRefPtrWillBeRawPtr<HTMLDocument> create(const DocumentInit& initializer = DocumentInit())
        {
            return adoptRefWillBeNoop(new HTMLDocument(initializer));
        }

        ......
    };

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLDocument.h中。

从这里可以看到,HTMLDocument类的静态成员函数create创建的Document的类型为HTMLDocument。

回到LocalDOMWindow类的成员函数installNewDocument中,它调用成员函数createDocument创建了一个HTMLDocument对象之后,接下来会调用这个HTMLDocument对象的成员函数attach为其创建一个Render View。

HTMLDocument类的成员函数attach是从父类Document继承下来的,它的实现如下所示:

void Document::attach(const AttachContext& context)
    {
        ......

        m_renderView = new RenderView(this);
        setRenderer(m_renderView);

        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/Document.cpp。

Document类的成员函数attach首先是创建了一个RenderView对象保存在成员变量m_renderView中。这个RenderView对象就是图2所示的Render Object Tree的根节点。接下来又调用另外一个成员函数setRenderer将上述RenderView对象作为与当前正在处理的Document对象对应的Render Object。以后我们分析网页的渲染过程时,再详细分析Render View的作用。

这一步执行完成之后,WebKit就为一个类型为LocalDOMWindow的DOM Window创建了一个类型为HTMLDocument的Document,正如图1所示。回到DocumentLoader类的成员函数createWriterFor中,它最后调用DocumentWriter类的静态成员函数create为前面创建的类型为HTMLDocument的Document创建一个DocumentWriter对象。这个DocumentWriter对象负责解析从Web服务器下载回来的网页数据。

DocumentWriter类的静态成员函数create的实现如下所示:

PassRefPtrWillBeRawPtr<DocumentWriter> DocumentWriter::create(Document* document, const AtomicString& mimeType, const AtomicString& encoding, bool encodingUserChoosen)
    {
        return adoptRefWillBeNoop(new DocumentWriter(document, mimeType, encoding, encodingUserChoosen));
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/loader/DocumentWriter.cpp中。

从这里可以看到,DocumentWriter类的静态成员函数create创建的是一个DocumentWriter对象。这个DocumentWriter对象的创建过程,即DocumentWriter类的构造函数的实现,如下所示:

DocumentWriter::DocumentWriter(Document* document, const AtomicString& mimeType, const AtomicString& encoding, bool encodingUserChoosen)
        : m_document(document)
        , ......
        , m_parser(m_document->implicitOpen())
    {
        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/loader/DocumentWriter.cpp中。

DocumentWriter类的构造函数首先将参数document描述的HTMLDocument对象保存在成员变量m_document中,接下来又调用这个HTMLDocument对象的成员函数implicitOpen创建了一个HTMLDocumentParser对象。这个HTMLDocumentParser对象就是用来解析从Web服务器下载回来网页数据的。

HTMLDocument类的成员函数implicitOpen是从父类Document继承下来的,它的实现如下所示:

PassRefPtrWillBeRawPtr<DocumentParser> Document::implicitOpen()
    {
        ......

        m_parser = createParser();
        ......

        return m_parser;
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/Document.cpp中。

Document类的成员函数implicitOpen调用另外一个成员函数createParser创建了一个HTMLDocumentParser对象保存在成员变量m_parser中,并且这个HTMLDocumentParser对象会返回给调用者。

Document类的成员函数createParser的实现如下所示:

PassRefPtrWillBeRawPtr<DocumentParser> Document::createParser()
    {
        if (isHTMLDocument()) {
            bool reportErrors = InspectorInstrumentation::collectingHTMLParseErrors(page());
            return HTMLDocumentParser::create(toHTMLDocument(*this), reportErrors);
        }
        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/Document.cpp中。

由于当前正在处理的实际上是一个HTMLDocument对象,因此Document类的成员函数createParser调用另外一个成员函数isHTMLDocument得到的返回值会为true,这时候Document类的成员函数就会调用HTMLDocumentParser类的静态成员函数create创建一个HTMLDocumentParser对象,如下所示:

class HTMLDocumentParser : public ScriptableDocumentParser, private HTMLScriptRunnerHost {
        WTF_MAKE_FAST_ALLOCATED_WILL_BE_REMOVED;
        WILL_BE_USING_GARBAGE_COLLECTED_MIXIN(HTMLDocumentParser);
    public:
        static PassRefPtrWillBeRawPtr<HTMLDocumentParser> create(HTMLDocument& document, bool reportErrors)
        {
            return adoptRefWillBeNoop(new HTMLDocumentParser(document, reportErrors));
        }

        ......
    };

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLDocumentParser.h中。

从这里可以看到,HTMLDocumentParser类的静态成员函数create创建的是一个HTMLDocumentParser对象,这个HTMLDocumentParser对象会返回给调用者。

这一步执行完成之后,回到DocumentLoader类的成员函数dataReceived中,它调用成员函数ensureWriter确定成员变量m_writer指向了一个DocumentWriter对象之后,接下来要调用这个DocumentWriter对象的成员函数addData对下载回来的网页数据进行解析。

DocumentWriter类的成员函数addData的实现如下所示:

void DocumentWriter::addData(const char* bytes, size_t length)
    {
        ......

        m_parser->appendBytes(bytes, length);
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/loader/DocumentWriter.cpp中。

从前面的分析可以知道,DocumentWriter类的成员变量m_parser指向的是一个HTMLDocumentParser对象,DocumentWriter类的成员函数addData调用这个HTMLDocumentParser对象的成员函数appendBytes对下载回来的网页数据进行解析。

HTMLDocumentParser类的成员函数appendBytes的实现如下所示:

void HTMLDocumentParser::appendBytes(const char* data, size_t length)
    {
        ......

        if (shouldUseThreading()) {
            ......

            OwnPtr<Vector<char> > buffer = adoptPtr(new Vector<char>(length));
            memcpy(buffer->data(), data, length);
            ......

            HTMLParserThread::shared()->postTask(bind(&BackgroundHTMLParser::appendRawBytesFromMainThread, m_backgroundParser, buffer.release()));
            return;
        }

        DecodedDataDocumentParser::appendBytes(data, length);
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLDocumentParser.cpp中。

HTMLDocumentParser类的成员函数appendBytes调用另外一个成员函数shouldUseThreading判断是否需要在一个专门的线程中对下载回来的网页数据进行解析。如果需要的话,那么就把下载回来的网页数据拷贝到一个新的缓冲区中去交给专门的线程进行解析。否则的话,就在当前线程中调用父类DecodedDataDocumentParser类的成员函数appendBytes对下载回来的网页数据进行解析。为了简单起见,我们分析后一种情况,也就是分析DecodedDataDocumentParser类的成员函数appendBytes的实现。

DecodedDataDocumentParser类的成员函数appendBytes的实现如下所示:

void DecodedDataDocumentParser::appendBytes(const char* data, size_t length)
    {
        ......

        String decoded = m_decoder->decode(data, length);
        updateDocument(decoded);
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/DecodedDataDocumentParser.cpp中。

DecodedDataDocumentParser类的成员变量m_decoder指向一个TextResourceDecoder对象。这个TextResourceDecoder对象负责对下载回来的网页数据进行解码。解码后得到网页数据的字符串表示。这个字符串将会交给由另外一个成员函数updateDocument进行处理。

DecodedDataDocumentParser类的成员函数updateDocument的实现如下所示:

void DecodedDataDocumentParser::updateDocument(String& decodedData)
    {
        ......

        if (!decodedData.isEmpty())
            append(decodedData.releaseImpl());
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/DecodedDataDocumentParser.cpp中。

DecodedDataDocumentParser类的成员函数updateDocument又将参数decodedData描述的网页内容交给由子类HTMLDocumentParser实现的成员函数append处理。

HTMLDocumentParser类的成员函数append的实现如下所示:

void HTMLDocumentParser::append(PassRefPtr<StringImpl> inputSource)
    {
        ......

        String source(inputSource);

        ......

        m_input.appendToEnd(source);

        ......

        if (m_isPinnedToMainThread)
            pumpTokenizerIfPossible(ForceSynchronous);
        else
            pumpTokenizerIfPossible(AllowYield);

        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLDocumentParser.cpp中。

HTMLDocumentParser类的成员函数append首先将网页内容附加在成员变量m_input描述的一个输入流中,接下来再调用成员函数pumpTokenizerIfPossible对该输入流中的网页内容进行解析。

在调用成员函数pumpTokenizerIfPossible的时候,根据成员变量m_isPinnedToMainThread的值的不同而传递不同的参数。当成员变量m_isPinnedToMainThread的值等于true的时候,传递的参数为ForceSynchronous,表示要以同步方式解析网页的内容。当成员变量m_isPinnedToMainThread的值等于false的时候,传递的参数为AllowYield,表示要以异步方式解析网页的内容。

在同步解析网页内容方式中,当前线程会一直运行到所有下载回来的网页内容都解析完为止,除非遇到有JavaScript需要运行。在异步解析网页内容方式中,在遇到有JavaScript需要运行,或者解析的网页内容超过一定量时,如果当前线程花在解析网页内容的时间超过预设的阀值,那么当前线程就会自动放弃CPU,通过一个定时器等待一小段时间后再继续解析剩下的网页内容。

接下来我们就继续分析HTMLDocumentParser类的成员函数pumpTokenizerIfPossible的实现,如下所示:

void HTMLDocumentParser::pumpTokenizerIfPossible(SynchronousMode mode)
    {
        ......

        // Once a resume is scheduled, HTMLParserScheduler controls when we next pump.
        if (isScheduledForResume()) {
            ASSERT(mode == AllowYield);
            return;
        }

        pumpTokenizer(mode);
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLDocumentParser.cpp中。

HTMLDocumentParser类的成员函数pumpTokenizerIfPossible首先调用成员函数isScheduledForResume判断当前正在处理的HTMLDocumentParser对象是否处于等待重启继续解析网页内容的状态中。如果是的话,等到定时器超时时,当前线程就会自动调用当前正在处理的HTMLDocumentParser对象的成员函数pumpTokenizer对剩下未解析的网页内容进行解析。这种情况必须要确保参数mode的值为AllowYield,也就是确保当前正在处理的HTMLDocumentParser对象使用异步方式解析网页内容。

如果当前正在处理的HTMLDocumentParser对象是以同步方式解析网页内容,那么HTMLDocumentParser类的成员函数pumpTokenizerIfPossible接下来就会马上调用成员函数pumpTokenizer对刚才下载回来的网页内容进行解析。

HTMLDocumentParser类的成员函数pumpTokenizer的实现如下所示:

void HTMLDocumentParser::pumpTokenizer(SynchronousMode mode)
    {
        ......

        PumpSession session(m_pumpSessionNestingLevel, contextForParsingSession());
        ......

        while (canTakeNextToken(mode, session) && !session.needsYield) {
            ......

            if (!m_tokenizer->nextToken(m_input.current(), token()))
                break;

            ......

            constructTreeFromHTMLToken(token());
            ......
        }

        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLDocumentParser.cpp中。

HTMLDocumentParser类的成员函数pumpTokenizer通过成员变量m_tokenizer描述的一个HTMLTokenizer对象的成员函数nextToken对网页内容进行字符串解析。网页内容被解析成一系列的Token。每一个Token描述的要么是一个标签,要么是一个标签的内容,也就是文本。有了这些Token之后,HTMLDocumentParser类的成员函数pumpTokenizer就可以构造DOM Tree了。这是通过调用另外一个成员函数constructTreeFromHTMLToken进行的。

注意,HTMLDocumentParser类的成员函数pumpTokenizer通过一个while循环依次提取网页内容的Token,并且每提取一个Token,都会调用一次HTMLDocumentParser类的成员函数constructTreeFromHTMLToken。这个while循环在三种情况下会结束。

第一种情况是所有的Token均已提取并且处理完毕。第二种情况是在解析的过程中遇到JavaScript脚本需要执行,这时候调用HTMLDocumentParser类的成员函数canTakeNextToken的返回值会等于false。第三种情况出现在异步方式解析网页内容时,这时候HTMLDocumentParser类的成员函数canTakeNextToken会将本地变量session描述的一个PumpSession对象的成员变量needsYield的值设置为true,表示当前线程持续解析的网页内容已经达到一定量并且持续的时间也超过了一定值,需要自动放弃使用CPU。

接下来我们继续分析HTMLDocumentParser类的成员函数constructTreeFromHTMLToken的实现,如下所示:

void HTMLDocumentParser::constructTreeFromHTMLToken(HTMLToken& rawToken)
    {
        AtomicHTMLToken token(rawToken);

        ......

        m_treeBuilder->constructTree(&token);

        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLDocumentParser.cpp中。

HTMLDocumentParser类的成员函数constructTreeFromHTMLToken所做的事情就是根据参数rawToken描述的一个Token来不断构造网页的DOM Tree。这个构造过程是通过调用成员变量m_treeBuilder描述的一个HTMLTreeBuilder对象的成员函数constructTree实现的。

HTMLTreeBuilder类的成员函数constructTree的实现如下所示:

void HTMLTreeBuilder::constructTree(AtomicHTMLToken* token)
    {
        if (shouldProcessTokenInForeignContent(token))
            processTokenInForeignContent(token);
        else
            processToken(token);

        ......

        m_tree.executeQueuedTasks();
        // We might be detached now.
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLTreeBuilder.cpp中。

HTMLTreeBuilder类的成员函数constructTree首先调用成员函数shouldProcessTokenInForeignContent判断参数token描述的Token是否为Foreign Content,即不是HTML标签相关的内容,而是MathML和SVG这种外部标签相关的内容。如果是的话,就调用成员函数processTokenInForeignContent对它进行处理。

如果参数token描述的是一个HTML标签相关的内容,那么HTMLTreeBuilder类的成员函数constructTree就会调用成员函数processToken对它进行处理。接下来我们只关注HTML标签相关内容的处理过程。

处理完成参数token描述的Token之后,HTMLTreeBuilder类的成员函数constructTree会调用成员变量m_tree描述的一个HTMLConstructionSite对象的成员函数executeQueuedTasks执行保存其内部的一个事件队列中的任务。这些任务是处理参数token描述的标签的过程中添加到事件队列中去的,主要是为了处理那些在网页中没有正确嵌套的格式化标签的。HTML标准规定了处理这些没有正确嵌套的格式化标签的算法,具体可以参考标准中的12.2.3.3小节:The list of active formatting elements。WebKit在实现这个算法的时候,就用到了上述的事件队列。

接下来我们继续分析HTMLTreeBuilder类的成员函数processToken的实现,如下所示:

void HTMLTreeBuilder::processToken(AtomicHTMLToken* token)
    {
        if (token->type() == HTMLToken::Character) {
            processCharacter(token);
            return;
        }

        // Any non-character token needs to cause us to flush any pending text immediately.
        // NOTE: flush() can cause any queued tasks to execute, possibly re-entering the parser.
        m_tree.flush();
        m_shouldSkipLeadingNewline = false;

        switch (token->type()) {
        case HTMLToken::Uninitialized:
        case HTMLToken::Character:
            ASSERT_NOT_REACHED();
            break;
        case HTMLToken::DOCTYPE:
            processDoctypeToken(token);
            break;
        case HTMLToken::StartTag:
            processStartTag(token);
            break;
        case HTMLToken::EndTag:
            processEndTag(token);
            break;
        case HTMLToken::Comment:
            processComment(token);
            break;
        case HTMLToken::EndOfFile:
            processEndOfFile(token);
            break;
        }
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLTreeBuilder.cpp中。

如果参数token描述的Token的类型是HTMLToken::Character,就表示该Token代表的是一个普通文本。这些普通文本不会马上进行处理,而是先保存在内部的一个Pending Text缓冲区中,这是通过调用HTMLTreeBuilder类的成员函数processCharacter实现的。等到遇到下一个Token的类型不是HTMLToken::Character时,才会对它们进行处理,这是通过调用成员变量m_tree描述的一个HTMLConstructionSite对象的成员函数flush实现的。

对于非HTMLToken::Character类型的Token,HTMLTreeBuilder类的成员函数processToken根据不同的类型调用不同的成员函数进行处理。在处理的过程中,就会使用图3所示的栈构造DOM Tree,并且会遵循HTML规范,具体可以参考这里:HTML Standard。例如,对于HTMLToken::StartTag类型的Token,就会调用成员函数processStartTag执行一个压栈操作,而对于HTMLToken::EndTag类型的Token,就会调用成员函数processEndTag执行一个出栈操作。

接下来我们主要分析HTMLTreeBuilder类的成员函数processStartTag的实现,主要是为了解WebKit在内部是如何描述一个HTML标签的。

HTMLTreeBuilder类的成员函数processStartTag的实现如下所示:

void HTMLTreeBuilder::processStartTag(AtomicHTMLToken* token)
    {
        ASSERT(token->type() == HTMLToken::StartTag);
        switch (insertionMode()) {
        ......
        case InBodyMode:
            ASSERT(insertionMode() == InBodyMode);
            processStartTagForInBody(token);
            break;
        ......
        }
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLTreeBuilder.cpp中。

HTMLTreeBuilder类在构造网页的DOM Tree时,根据当前所处理的网页内容而将内部状态设置为不同的Insertion Mode。这些Insertion Mode是由HTML规范定义的,具体可以参考12.2.3.1小节:The insertion mode。例如,当处理到网页的body标签里面的内容时,Insertion Mode就设置为InBodyMode,这时候HTMLTreeBuilder类的成员函数processStartTag就调用另外一个成员函数processStartTagForInBody按照InBodyMode的Insertion Mode来处理参数token描述的Token。

接下来我们继续分析HTMLTreeBuilder类的成员函数processStartTagForInBody的实现,如下所示:

void HTMLTreeBuilder::processStartTagForInBody(AtomicHTMLToken* token)
    {
        ......

        if (token->name() == addressTag
            || token->name() == articleTag
            || token->name() == asideTag
            || token->name() == blockquoteTag
            || token->name() == centerTag
            || token->name() == detailsTag
            || token->name() == dirTag
            || token->name() == divTag
            || token->name() == dlTag
            || token->name() == fieldsetTag
            || token->name() == figcaptionTag
            || token->name() == figureTag
            || token->name() == footerTag
            || token->name() == headerTag
            || token->name() == hgroupTag
            || token->name() == mainTag
            || token->name() == menuTag
            || token->name() == navTag
            || token->name() == olTag
            || token->name() == pTag
            || token->name() == sectionTag
            || token->name() == summaryTag
            || token->name() == ulTag) {
            ......
            m_tree.insertHTMLElement(token);
            return;
        }

        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLTreeBuilder.cpp中。

我们假设参数token描述的Token代表的是一个

标签,那么HTMLTreeBuilder类的成员函数processStartTagForInBody就会调用成员变量m_tree描述的一个HTMLConstructionSite对象的成员函数insertHTMLElement为其创建一个HTMLElement对象,并且将这个HTMLElement对象压入栈中去构造DOM Tree。

HTMLConstructionSite类的成员函数insertHTMLElement的实现如下所示:

void HTMLConstructionSite::insertHTMLElement(AtomicHTMLToken* token)
    {
        RefPtrWillBeRawPtr<Element> element = createHTMLElement(token);
        attachLater(currentNode(), element);
        m_openElements.push(HTMLStackItem::create(element.release(), token));
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLConstructionSite.cpp中。

HTMLConstructionSite类的成员函数insertHTMLElement首先调用成员函数createHTMLElement创建一个HTMLElement对象描述参数token代表的HTML标签,接着调用成员函数attachLater稍后将该HTMLElement对象设置为当前栈顶HTMLElement对象的子HTMLElement对象,最后又将该HTMLElement对象压入成员变量m_openElements描述的栈中去。

接下来我们主要分析HTMLConstructionSite类的成员函数createHTMLElement的实现,以便了解WebKit是如何描述一个HTML标签的。

HTMLConstructionSite类的成员函数createHTMLElement的实现如下所示:

PassRefPtrWillBeRawPtr<Element> HTMLConstructionSite::createHTMLElement(AtomicHTMLToken* token)
    {
        Document& document = ownerDocumentForCurrentNode();
        // Only associate the element with the current form if we're creating the new element
        // in a document with a browsing context (rather than in <template> contents).
        HTMLFormElement* form = document.frame() ? m_form.get() : 0;
        // FIXME: This can't use HTMLConstructionSite::createElement because we
        // have to pass the current form element.  We should rework form association
        // to occur after construction to allow better code sharing here.
        RefPtrWillBeRawPtr<Element> element = HTMLElementFactory::createHTMLElement(token->name(), document, form, true);
        setAttributes(element.get(), token, m_parserContentPolicy);
        ASSERT(element->isHTMLElement());
        return element.release();
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/parser/HTMLConstructionSite.cpp中。

从这里可以看到,HTMLConstructionSite类的成员函数createHTMLElement是调用HTMLElementFactory类的静态成员函数createHTMLElement为参数token描述的HTML标签创建一个HTMLElement对象的,并且接下来还会调用另外一个成员函数setAttributes根据Token的内容设置该HTMLElement对象的各个属性值。

HTMLElementFactory类的静态成员函数createHTMLElement的实现如下所示:

typedef HashMap<AtomicString, ConstructorFunction> FunctionMap;

    static FunctionMap* g_constructors = 0;

    ......

    static void createHTMLFunctionMap()
    {
        ASSERT(!g_constructors);
        g_constructors = new FunctionMap;
        // Empty array initializer lists are illegal [dcl.init.aggr] and will not
        // compile in MSVC. If tags list is empty, add check to skip this.
        static const CreateHTMLFunctionMapData data[] = {
            { abbrTag, abbrConstructor },
            ......
            { divTag, divConstructor },
            ......
            { wbrTag, wbrConstructor },
        };
        for (size_t i = 0; i < WTF_ARRAY_LENGTH(data); i++)
            g_constructors->set(data[i].tag.localName(), data[i].func);
    }

    PassRefPtrWillBeRawPtr<HTMLElement> HTMLElementFactory::createHTMLElement(
        const AtomicString& localName,
        Document& document,
        HTMLFormElement* formElement,
        bool createdByParser)
    {
        ......

        if (ConstructorFunction function = g_constructors->get(localName))
            return function(document, formElement, createdByParser);

        ......
    }

这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/blink/core/HTMLElementFactory.cpp中。

HTMLElementFactory类的静态成员函数createHTMLElement根据HTML标签的名称在全局变量g_constructors描述的一个Function Map中找到指定的函数为该HTML标签创建一个HTMLElement对象。例如,用来描述HTML标签

的HTMLElement对象是通过调用函数divConstructor进行创建的。

函数divConstructor的实现如下所示:

static PassRefPtrWillBeRawPtr<HTMLElement> divConstructor(
        Document& document,
        HTMLFormElement* formElement,
        bool createdByParser)
    {
        return HTMLDivElement::create(document);
    }

这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/blink/core/HTMLElementFactory.cpp中。

函数divConstructor调用HTMLDivElement类的静态成员函数create创建了一个HTMLDivElement对象,并且返回给调用者。这样以后我们需要了解

标签的更多细节时,就可以参考HTMLDivElement类的实现。

这样,我们就分析完成网页的DOM Tree的创建过程了。我们没有很详细地描述这个创建过程,因为这涉及到很多实现细节,以及极其繁琐的HTML规范。我们提供了一个DOM Tree创建的框架。有了这个框架之后,以后当我们需要了解某一个细节时,就可以方便地找到相关源码进行分析。

网页内容下载完成之后,DOM Tree的构造过程就结束。接下来WebKit就会根据DOM Tree创建Render Object Tree。在接下来一篇文章中,我们就详细分析Render Object Tree的创建过程,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:6月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:6月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:6月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:6月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:6月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:6月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:6月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:6月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:6月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:6月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:6月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:6月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:6月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:6月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:6月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:6月以前  |  398次阅读  |  详细内容 »
 目录