在Chromium中,Render进程是通过Browser进程下载网页内容的,后者又是通过共享内存将下载回来的网页内容交给前者的。Render进程获得网页内容之后,会交给WebKit进行处理。WebKit所做的第一个处理就是对网页内容进行解析,解析的结果是得到一棵DOM Tree。DOM Tree是网页的一种结构化描述,也是网页渲染的基础。本文接下来就对网页DOM Tree的创建过程进行详细分析。
老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!
网页的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的后端存储实际上就是一个内存缓冲区。在硬件渲染模式中,Layerr的后端存储实际上就是一个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代表的是一个<div>标签,那么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标签<div>的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对象,并且返回给调用者。这样以后我们需要了解<div>标签的更多细节时,就可以参考HTMLDivElement类的实现。
这样,我们就分析完成网页的DOM Tree的创建过程了。我们没有很详细地描述这个创建过程,因为这涉及到很多实现细节,以及极其繁琐的HTML规范。我们提供了一个DOM Tree创建的框架。有了这个框架之后,以后当我们需要了解某一个细节时,就可以方便地找到相关源码进行分析。
网页内容下载完成之后,DOM Tree的构造过程就结束。接下来WebKit就会根据DOM Tree创建Render Object Tree。在接下来一篇文章中,我们就详细分析Render Object Tree的创建过程,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo。