WebKit Inside: DOM树的构建
阅读原文时间:2022年02月22日阅读:1

当客户端App主进程创建WKWebView对象时,会创建另外两个子进程:渲染进程与网络进程。主进程WKWebView发起请求时,先将请求转发给渲染进程,渲染进程再转发给网络进程,网络进程请求服务器。如果请求的是一个网页,网络进程会将服务器的响应数据HTML文件字符流吐给渲染进程。渲染进程拿到HTML文件字符流,首先要进行解析,将HTML文件字符流转换成DOM树,然后在DOM树的基础上,进行渲染操作,也就是布局、绘制。最后渲染进程将渲染数据吐给主进程WKWebView,WKWebView根据渲染数据创建对应的View展现视图。整个流程如下图所示:

什么是DOM树

渲染进程获取到HTML文件字符流,会将HTML文件字符流转换成DOM树。下图中左侧是一个HTML文件,右边就是转换而成的DOM树。

可以看到DOM树的根节点是HTMLDocument,代表整个文档。根节点下面的子节点与HTML文件中的标签是一一对应的,比如HTML中的<head>标签就对应DOM树中的head节点。同时HTML文件中的文本,也成为DOM树中的一个节点,比如文本'Hello, World!',在DOM树中就成为div节点的子节点。

在DOM树中每一个节点都是具有一定方法与属性的对象,这些对象由对应的类创建出来。比如HTMLDocument节点,它对应的类是class HTMLDocument,下面是HTMLDocument的部分源码:

1 class HTMLDocument : public Document { // 继承自Document
2 …
3 WEBCORE_EXPORT int width();
4 WEBCORE_EXPORT int height();
5 …
6 }

从源码中可以看到,HTMLDocument继承自类Document,Document类的部分源码如下:

1 class Document
2 : public ContainerNode // Document继承自ContainerNode,ContainerNode继承自Node
3 , public TreeScope
4 , public ScriptExecutionContext
5 , public FontSelectorClient
6 , public FrameDestructionObserver
7 , public Supplementable
8 , public Logger::Observer
9 , public CanvasObserver {
10 WEBCORE_EXPORT ExceptionOr> createElementForBindings(const AtomString& tagName); // 创建Element的方法
11 WEBCORE_EXPORT Ref createTextNode(const String& data); // 创建文本节点的方法
12 WEBCORE_EXPORT Ref createComment(const String& data); // 创建注释的方法
13 WEBCORE_EXPORT Ref createElement(const QualifiedName&, bool createdByParser); // 创建Element方法
14 ….
15 }

上面源码可以看到Document继承自Node,而且还可以看到前端十分熟悉的createElement、createTextNode等方法,JavaScript对这些方法的调用,最后都转换为对应C++方法的调用。

类Document有这些方法,并不是没有原因的,而是W3C组织给出的标准规定的,这个标准就是DOM(Document Object Model,文档对象模型)。DOM定义了DOM树中每个节点需要实现的接口和属性,下面是HTMLDocument、Document、HTMLDivElment的部分IDL(Interactive Data Language,接口描述语言,与具体平台和语言无关)描述,完整的IDL可以参看W3C

1 interface HTMLDocument : Document { // HTMLDocument
2 getter (WindowProxy or Element or HTMLCollection) (DOMString name);
3 };
4
5
6 interface Document : Node { // Document
7 [NewObject, ImplementedAs=createElementForBindings] Element createElement(DOMString localName); // createElement
8 [NewObject] Text createTextNode(DOMString data); // createTextNode
9 …
10 }
11
12
13 interface HTMLDivElement : HTMLElement { // HTMLDivElement
14 [CEReactions=NotNeeded, Reflect] attribute DOMString align;
15 };

在DOM树中,每一个节点都继承自类Node,同时Node还有一个子类Element,有的节点直接继承自类Node,比如文本节点,而有的节点继承自类Element,比如div节点。因此针对上面图中的DOM树,执行下面的JavaScript语句返回的结果是不一样的:

1 document.childNodes; // 返回子Node集合,返回DocumentType与HTML节点,都继承自Node
2 document.children; // 返回子Element集合,只返回HTML节点,DocumentType不继承自Element

下图给出部分节点的继承关系图:

DOM树的构建

DOM树的构建流程可以分位4个步骤: 解码、分词、创建节点、添加节点。

1 解码

渲染进程从网络进程接收过来的是HTML字节流,而下一步分词是以字符为单位进行的。由于各种编码规范的存在,比如ISO-8859-1、UTF-8等,一个字符常常可能对应一个或者多个编码后的字节,解码的目的就是将HTML字节流转换成HTML字符流,或者换句话说,就是将原始的HTML字节流转换成字符串。

2 解码类图

从类图上看,类HTMLDocumentParser处于解码的核心位置,由这个类调用解码器将HTML字节流解码成字符流,存储到类HTMLInputStream中。

3 解码流程

整个解码流程当中,最关健的是如何找到正确的编码方式。只有找到了正确的编码方式,才能使用对应的解码器进行解码。解码发生的地方如下面源代码所示,这个方法在上图第3个栈帧被调用:

1 // HTMLDocumentParser是DecodedDataDocumentParser的子类
2 void DecodedDataDocumentParser::appendBytes(DocumentWriter& writer, const uint8_t* data, size_t length)
3 {
4 if (!length)
5 return;
6
7 String decoded = writer.decoder().decode(data, length); // 真正解码发生在这里
8 if (decoded.isEmpty())
9 return;
10
11 writer.reportDataReceived();
12 append(decoded.releaseImpl());
13 }

上面代码第7行writer.decoder()返回一个TextResourceDecoder对象,解码操作由TextResourceDecoder::decode方法完成。下面逐步查看TextResourceDecoder::decode方法的源码:

1 // 只保留了最重要的部分
2 2 String TextResourceDecoder::decode(const char* data, size_t length)
3 3 {
4 4 …
5 5
6 6 // 如果是HTML文件,就从head标签中寻找字符集
7 7 if ((m_contentType == HTML || m_contentType == XML) && !m_checkedForHeadCharset) // HTML and XML
8 8 if (!checkForHeadCharset(data, length, movedDataToBuffer))
9 9 return emptyString();
10 10
11 11 …
12 12
13 13 // m_encoding存储者从HTML文件中找到的编码名称
14 14 if (!m_codec)
15 15 m_codec = newTextCodec(m_encoding); // 创建具体的编码器
16 16
17 17 …
18 18
19 19 // 解码并返回
20 20 String result = m_codec->decode(m_buffer.data() + lengthOfBOM, m_buffer.size() - lengthOfBOM, false, m_contentType == XML && !m_useLenientXMLDecoding, m_sawError);
21 21 m_buffer.clear(); // 清空存储的原始未解码的HTML字节流
22 22 return result;
23 23 }

从源码中可以看到,TextResourceDecoder首先从HTML的标签中去找编码方式,因为标签可以包含标签,标签可以设置HTML文件的字符集:

1 2 3 DOM Tree 4 5

如果能找到对应的字符集,TextResourceDeocder将其存储在成员变量m_encoding当中,并且根据对应的编码创建真正的解码器存储在成员变量m_codec中,最终使用m_codec对字节流进行解码,并且返回解码后的字符串。如果带有字符集的标签没有找到,TextResourceDeocder的m_encoding有默认值windows-1252(等同于ISO-8859-1)。

下面看一下TextResourceDecoder寻找标签中字符集的流程,也就是上面源码中第8行对checkForHeadCharset函数的调用:

1 // 只保留了关健代码
2 bool TextResourceDecoder::checkForHeadCharset(const char* data, size_t len, bool& movedDataToBuffer)
3 {
4 …
5
6 // This is not completely efficient, since the function might go
7 // through the HTML head several times.
8
9 size_t oldSize = m_buffer.size();
10 m_buffer.grow(oldSize + len);
11 memcpy(m_buffer.data() + oldSize, data, len); // 将字节流数据拷贝到自己的缓存m_buffer里面
12
13 movedDataToBuffer = true;
14
15 // Continue with checking for an HTML meta tag if we were already doing so.
16 if (m_charsetParser)
17 return checkForMetaCharset(data, len); // 如果已经存在了meta标签解析器,直接开始解析
18
19 ….
20
21 m_charsetParser = makeUnique(); // 创建meta标签解析器
22 return checkForMetaCharset(data, len);
23 }

上面源代码中第11行,类TextResourceDecoder内部存储了需要解码的HTML字节流,这一步骤很重要,后面会讲到。先看第17行、21行、22行,这3行主要是使用标签解析器解析字符集,使用了懒加载的方式。下面看下checkForMetaCharset这个函数的实现:

1 bool TextResourceDecoder::checkForMetaCharset(const char* data, size_t length)
2 {
3 if (!m_charsetParser->checkForMetaCharset(data, length)) // 解析meta标签字符集
4 return false;
5
6 setEncoding(m_charsetParser->encoding(), EncodingFromMetaTag); // 找到后设置字符编码名称
7 m_charsetParser = nullptr;
8 m_checkedForHeadCharset = true;
9 return true;
10 }

上面源码第3行可以看到,整个解析标签的任务在类HTMLMetaCharsetParser::checkForMetaCharset中完成。

1 // 只保留了关健代码
2 bool HTMLMetaCharsetParser::checkForMetaCharset(const char* data, size_t length)
3 {
4 if (m_doneChecking) // 标志位,避免重复解析
5 return true;
6
7
8 // We still don't have an encoding, and are in the head.
9 // The following tags are allowed in : 10 // SCRIPT|STYLE|META|LINK|OBJECT|TITLE|BASE 11 // 12 // We stop scanning when a tag that is not permitted in 13 // is seen, rather when is seen, because that more closely
14 // matches behavior in other browsers; more details in
15 // http://bugs.webkit.org/show_bug.cgi?id=3590.
16 //
17 // Additionally, we ignore things that looks like tags in , <script> <br /> 18 // and <noscript>; see <a rel="nofollow noopener noreferrer" href="http://bugs.webkit.org/show_bug.cgi?id=4560">http://bugs.webkit.org/show_bug.cgi?id=4560</a>, <br /> 19 // <a rel="nofollow noopener noreferrer" href="http://bugs.webkit.org/show_bug.cgi?id=12165">http://bugs.webkit.org/show_bug.cgi?id=12165</a> and <br /> 20 // <a rel="nofollow noopener noreferrer" href="http://bugs.webkit.org/show_bug.cgi?id=12389">http://bugs.webkit.org/show_bug.cgi?id=12389</a>. <br /> 21 // <br /> 22 // Since many sites have charset declarations after <body> or other tags <br /> 23 // that are disallowed in <head>, we don't bail out until we've checked at <br /> 24 // least bytesToCheckUnconditionally bytes of input. <br /> 25 <br /> 26 constexpr int bytesToCheckUnconditionally = 1024; // 如果解析了1024个字符还未找到带有字符集的<meta>标签,整个解析也算完成,此时没有解析到正确的字符集,就使用默认编码windows-1252(等同于ISO-8859-1) <br /> 27 <br /> 28 bool ignoredSawErrorFlag; <br /> 29 m_input.append(m_codec->decode(data, length, false, false, ignoredSawErrorFlag)); // 对字节流进行解码 <br /> 30 <br /> 31 while (auto token = m_tokenizer.nextToken(m_input)) { // m_tokenizer进行分词操作,找meta标签也需要进行分词,分词操作后面讲 <br /> 32 bool isEnd = token->type() == HTMLToken::EndTag; <br /> 33 if (isEnd || token->type() == HTMLToken::StartTag) { <br /> 34 AtomString tagName(token->name()); <br /> 35 if (!isEnd) { <br /> 36 m_tokenizer.updateStateFor(tagName); <br /> 37 if (tagName == metaTag && processMeta(*token)) { // 找到meta标签进行处理 <br /> 38 m_doneChecking = true; <br /> 39 return true; // 如果找到了带有编码的meta标签,直接返回 <br /> 40 } <br /> 41 } <br /> 42 <br /> 43 if (tagName != scriptTag && tagName != noscriptTag <br /> 44 && tagName != styleTag && tagName != linkTag <br /> 45 && tagName != metaTag && tagName != objectTag <br /> 46 && tagName != titleTag && tagName != baseTag <br /> 47 && (isEnd || tagName != htmlTag) <br /> 48 && (isEnd || tagName != headTag)) { <br /> 49 m_inHeadSection = false; <br /> 50 } <br /> 51 } <br /> 52 <br /> 53 if (!m_inHeadSection && m_input.numberOfCharactersConsumed() >= bytesToCheckUnconditionally) { // 如果分词已经进入了<body>标签范围,同时分词数量已经超过了1024,也算成功 <br /> 54 m_doneChecking = true; <br /> 55 return true; <br /> 56 } <br /> 57 } <br /> 58 <br /> 59 return false; <br /> 60 }</p> <p>上面源码第29行,类HTMLMetaCharsetParser也有一个解码器m_codec,解码器是在HTMLMetaCharsetParser对象创建时生成,这个解码器的真实类型是TextCodecLatin1(Latin1编码也就是ISO-8859-1,等同于windows-1252编码)。之所以可以直接使用TextCodecLatin1解码器,是因为<meta>标签如果设置正确,都是英文字符,完全可以使用TextCodecLatin1进行解析出来。这样就避免了为了找到<meta>标签,需要对字节流进行解码,而要解码就必须要找到<meta>标签这种鸡生蛋、蛋生鸡的问题。</p> <p>代码第37行对找到的<meta>标签进行处理,这个函数比较简单,主要是解析<meta>标签当中的属性,然后查看这些属性名中有没有charset。</p> <p>1 bool HTMLMetaCharsetParser::processMeta(HTMLToken& token) <br /> 2 { <br /> 3 AttributeList attributes; <br /> 4 for (auto& attribute : token.attributes()) { // 获取meta标签属性 <br /> 5 String attributeName = StringImpl::create8BitIfPossible(attribute.name); <br /> 6 String attributeValue = StringImpl::create8BitIfPossible(attribute.value); <br /> 7 attributes.append(std::make_pair(attributeName, attributeValue)); <br /> 8 } <br /> 9 <br /> 10 m_encoding = encodingFromMetaAttributes(attributes); // 从属性中找字符集设置属性charset <br /> 11 return m_encoding.isValid(); <br /> 12 }</p> <p>上面分析TextResourceDecoder::checkForHeadCharset函数时,讲过第11行TextResourceDecoder类存储HTML字节流的操作很重要。原因是可能整个HTML字节流里面可能确实没有设置charset的<meta>标签,此时TextResourceDecoder::checkForHeadCharset函数就要返回false,导致TextResourceDecoder::decode函数返回空字符串,也就是不进行任何解码。是不是这样呢?真实的情况是,在接收HTML字节流整个过程中由于确实没有找到带有charset属性的<meta>标签,那么整个接收期间都不会解码。但是完整的HTML字节流会被存储在TextResourceDecoder的成员变量m_buffer里面,当整个HTML字节流接收结束的时,会有如下调用栈:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/ade3b671-1ecf-42b4-95e8-59882f442317.jpg" alt="" /></p> <p>从调用栈可以看到,当HTML字节流接收完成,最终会调用TextResourceDecoder::flush方法,这个方法会将TextResourceDecoder中有m_buffer存储的HTML字节流进行解码,由于在接收HTML字节流期间未成功找到编码方式,因此m_buffer里面存储的就是所有待解码的HTML字节流,然后在这里使用默认的编码windows-1252对全部字节流进行解码。因此,如果HTML字节流中包含汉字,那么如果不指定字符集,最终页面就会出现乱码。解码完成后,会将解码之后的字符流存储到HTMLDocumentParser中。</p> <p>1 void DecodedDataDocumentParser::flush(DocumentWriter& writer) <br /> 2 { <br /> 3 String remainingData = writer.decoder().flush(); <br /> 4 if (remainingData.isEmpty()) <br /> 5 return; <br /> 6 <br /> 7 writer.reportDataReceived(); <br /> 8 append(remainingData.releaseImpl()); // 解码后的字符流存储到HTMLDocumentParser <br /> 9 }</p> <p>4 解码总结</p> <p>整个解码过程可以分位两种情形: 第一种情形是HTML字节流可以解析出带有charset属性的<meta>标签,这样就可以获取相应的编码方式,那么每接收到一个HML字节流,都可以使用相应的编码方式进行解码,将解码后的字符流添加到HTMLInputStream当中;第二种是HTML字节流不能解析带有charset属性的<meta>标签,这样每接收到一个HTML字节流,都缓存到TextResourceDecoder的m_buffer缓存,等完整的HTML字节流接收完毕,就会使用默认的编码windows-1252进行解码。</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/b6dd5e5a-c48f-494c-8895-196dafb33d53.jpg" alt="" /></p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/b7818491-90b2-49bf-8dff-918aec2c3e30.jpg" alt="" /></p> <p><strong>分词</strong></p> <p>接收到的HTML字节流经过解码,成为存储在HTMLInputStream中的字符流。分词的过程就是从HTMLInputStream中依次取出每一个字符,然后判断字符是否是特殊的HTML字符'<'、'/'、'>'、'='等。根据这些特殊字符的分割,就能解析出HTML标签名以及属性列表,类HTMLToken就是存储分词出来的结果。</p> <p>1 分词类图</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/39ec0a66-7067-4e92-949a-b5c5684a5632.jpg" alt="" /></p> <p>从类图中可以看到,分词最重要的是类HTMLTokenizer和类HTMLToken。下面是类HTMLToken的主要信息:</p> <p>1 // 只保留了主要信息 <br /> 2 2 class HTMLToken { <br /> 3 3 public: <br /> 4 4 enum Type { // Token的类型 <br /> 5 5 Uninitialized, // Token初始化时的类型 <br /> 6 6 DOCTYPE, // 代表Token是DOCType标签 <br /> 7 7 StartTag, // 代表Token是一个开始标签 <br /> 8 8 EndTag, // 代表Token是一个结束标签 <br /> 9 9 Comment, // 代表Token是一个注释 <br /> 10 10 Character, // 代表Token是文本 <br /> 11 11 EndOfFile, // 代表Token是文件结尾 <br /> 12 12 }; <br /> 13 13 <br /> 14 14 struct Attribute { // 存储属性的数据结构 <br /> 15 15 Vector<UChar, 32> name; // 属性名 <br /> 16 16 Vector<UChar, 64> value; // 属性值 <br /> 17 17 <br /> 18 18 // Used by HTMLSourceTracker. <br /> 19 19 unsigned startOffset; <br /> 20 20 unsigned endOffset; <br /> 21 21 }; <br /> 22 22 <br /> 23 23 typedef Vector<Attribute, 10> AttributeList; // 属性列表 <br /> 24 24 typedef Vector<UChar, 256> DataVector; // 存储Token名 <br /> 25 25 <br /> 26 26 … <br /> 27 27 <br /> 28 28 private: <br /> 29 29 Type m_type; <br /> 30 30 DataVector m_data; <br /> 31 31 // For StartTag and EndTag <br /> 32 32 bool m_selfClosing; // Token是注入<img>一样自结束标签 <br /> 33 33 AttributeList m_attributes; <br /> 34 34 Attribute* m_currentAttribute; // 当前正在解析的属性 <br /> 35 35 };</p> <p>2 分词流程</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/6b978902-3093-4129-817e-ff76506237ef.jpg" alt="" /></p> <p>上面分词流程中HTMLDocumentParser::pumpTokenizerLoop方法是最重要的,从方法名字可以看出这个方法里面包含循环逻辑:</p> <p>1 // 只保留关健代码 <br /> 2 bool HTMLDocumentParser::pumpTokenizerLoop(SynchronousMode mode, bool parsingFragment, PumpSession& session) <br /> 3 { <br /> 4 do { // 分词循环体开始 <br /> 5 … <br /> 6 <br /> 7 if (UNLIKELY(mode == AllowYield && m_parserScheduler->shouldYieldBeforeToken(session))) // 避免长时间处于分词循环中,这里根据条件暂时退出循环 <br /> 8 return true; <br /> 9 <br /> 10 if (!parsingFragment) <br /> 11 m_sourceTracker.startToken(m_input.current(), m_tokenizer); <br /> 12 <br /> 13 auto token = m_tokenizer.nextToken(m_input.current()); // 进行分词操作,取出一个token <br /> 14 if (!token) <br /> 15 return false; // 分词没有产生token,就跳出循环 <br /> 16 <br /> 17 if (!parsingFragment) <br /> 18 m_sourceTracker.endToken(m_input.current(), m_tokenizer); <br /> 19 <br /> 20 constructTreeFromHTMLToken(token); // 根据token构建DOM树 <br /> 21 } while (!isStopped()); <br /> 22 <br /> 23 return false; <br /> 24 }</p> <p>上面代码中第7行会有一个yield退出操作,这是为了避免长时间处于分词循环,占用主线程。当退出条件为真时,会从分词循环中返回,返回值为true。下面是退出判断代码:</p> <p>1 // 只保留关健代码 <br /> 2 bool HTMLParserScheduler::shouldYieldBeforeToken(PumpSession& session) <br /> 3 { <br /> 4 … <br /> 5 <br /> 6 // numberOfTokensBeforeCheckingForYield是静态变量,定义为4096 <br /> 7 // session.processedTokensOnLastCheck表示从上一次退出为止,以及处理过的token个数 <br /> 8 // session.didSeeScript表示在分词过程中是否出现过script标签 <br /> 9 if (UNLIKELY(session.processedTokens > session.processedTokensOnLastCheck + numberOfTokensBeforeCheckingForYield || session.didSeeScript)) <br /> 10 return checkForYield(session); <br /> 11 <br /> 12 ++session.processedTokens; <br /> 13 return false; <br /> 14 } <br /> 15 <br /> 16 <br /> 17 bool HTMLParserScheduler::checkForYield(PumpSession& session) <br /> 18 { <br /> 19 session.processedTokensOnLastCheck = session.processedTokens; <br /> 20 session.didSeeScript = false; <br /> 21 <br /> 22 Seconds elapsedTime = MonotonicTime::now() - session.startTime; <br /> 23 return elapsedTime > m_parserTimeLimit; // m_parserTimeLimit的值默认是500ms,从分词开始超过500ms就要先yield <br /> 24 }</p> <p>如果命中了上面的yield退出条件,那么什么时候再次进入分词呢?下面的代码展示了再次进入分词的过程:</p> <p>1 // 保留关键代码 </p> <p>2 void HTMLDocumentParser::pumpTokenizer(SynchronousMode mode) <br /> 3 { <br /> 4 … <br /> 5 <br /> 6 if (shouldResume) // 从pumpTokenizerLoop中yield退出时返回值为true <br /> 7 m_parserScheduler->scheduleForResume(); <br /> 8 <br /> 9 } <br /> 10 <br /> 11 <br /> 12 <br /> 13 void HTMLParserScheduler::scheduleForResume() <br /> 14 { <br /> 15 ASSERT(!m_suspended); <br /> 16 m_continueNextChunkTimer.startOneShot(0_s); // 触发timer(0s后触发),触发后的响应函数为HTMLParserScheduler::continueNextChunkTimerFired <br /> 17 } <br /> 18 <br /> 19 <br /> 20 // 保留关健代码 <br /> 21 void HTMLParserScheduler::continueNextChunkTimerFired() <br /> 22 { <br /> 23 … <br /> 24 <br /> 25 m_parser.resumeParsingAfterYield(); // 重新Resume分词过程 <br /> 26 } <br /> 27 <br /> 28 <br /> 29 void HTMLDocumentParser::resumeParsingAfterYield() <br /> 30 { <br /> 31 // pumpTokenizer can cause this parser to be detached from the Document, <br /> 32 // but we need to ensure it isn't deleted yet. <br /> 33 Ref<HTMLDocumentParser> protectedThis(*this); <br /> 34 <br /> 35 // We should never be here unless we can pump immediately. <br /> 36 // Call pumpTokenizer() directly so that ASSERTS will fire if we're wrong. <br /> 37 pumpTokenizer(AllowYield); // 重新进入分词过程,该函数会调用pumpTokenizerLoop <br /> 38 endIfDelayed(); <br /> 39 }</p> <p>从上面代码可以看出,再次进入分词过程是通过触发一个Timer来实现的,虽然这个Timer在0s后触发,但是并不意味着Timer的响应函数会立刻执行。如果在此之前主线程已经有其他任务到达了执行时机,会有被执行的机会。</p> <p>继续看HTMLDocumentParser::pumpTokenizerLoop函数的第13行,这一行进行分词操作,从解码后的字符流中分出一个token。实现分词的代码位于HTMLTokenizer::processToken:</p> <p>1 // 只保留关键代码 <br /> 2 bool HTMLTokenizer::processToken(SegmentedString& source) <br /> 3 { <br /> 4 <br /> 5 … <br /> 6 <br /> 7 if (!m_preprocessor.peek(source, isNullCharacterSkippingState(m_state))) // 取出source内部指向的字符,赋给m_nextInputCharacter <br /> 8 return haveBufferedCharacterToken(); <br /> 9 UChar character = m_preprocessor.nextInputCharacter(); // 获取character <br /> 10 <br /> 11 // <a rel="nofollow noopener noreferrer" href="https://html.spec.whatwg.org/#tokenization">https://html.spec.whatwg.org/#tokenization</a> <br /> 12 switch (m_state) { // 进行状态转换,m_state初始值为DataState <br /> 13 … <br /> 14 } <br /> 15 <br /> 16 return false; <br /> 17 }</p> <p>这个方法由于内部要做很多状态转换,总共有1200多行,后面会有4个例子来解释状态转换的逻辑。</p> <p>首先来看InputStreamPreprocessor::peek方法:</p> <p>1 // Returns whether we succeeded in peeking at the next character. <br /> 2 // The only way we can fail to peek is if there are no more <br /> 3 // characters in |source| (after collapsing \r\n, etc). <br /> 4 ALWAYS_INLINE bool InputStreamPreprocessor::peek(SegmentedString& source, bool skipNullCharacters = false) <br /> 5 { <br /> 6 if (UNLIKELY(source.isEmpty())) <br /> 7 return false; <br /> 8 <br /> 9 m_nextInputCharacter = source.currentCharacter(); // 获取字符流source内部指向的当前字符 <br /> 10 <br /> 11 // Every branch in this function is expensive, so we have a <br /> 12 // fast-reject branch for characters that don't require special <br /> 13 // handling. Please run the parser benchmark whenever you touch <br /> 14 // this function. It's very hot. <br /> 15 constexpr UChar specialCharacterMask = '\n' | '\r' | '\0'; <br /> 16 if (LIKELY(m_nextInputCharacter & ~specialCharacterMask)) { <br /> 17 m_skipNextNewLine = false; <br /> 18 return true; <br /> 19 } <br /> 20 <br /> 21 return processNextInputCharacter(source, skipNullCharacters); // 跳过空字符,将\r\n换行符合并成\n <br /> 22 } <br /> 23 <br /> 24 <br /> 25 bool InputStreamPreprocessor::processNextInputCharacter(SegmentedString& source, bool skipNullCharacters) <br /> 26 { <br /> 27 ProcessAgain: <br /> 28 ASSERT(m_nextInputCharacter == source.currentCharacter()); <br /> 29 <br /> 30 // 针对\r\n换行符,下面if语句处理\r字符并且设置m_skipNextNewLine=true,后面处理\n就直接忽略 <br /> 31 if (m_nextInputCharacter == '\n' && m_skipNextNewLine) { <br /> 32 m_skipNextNewLine = false; <br /> 33 source.advancePastNewline(); // 向前移动字符 <br /> 34 if (source.isEmpty()) <br /> 35 return false; <br /> 36 m_nextInputCharacter = source.currentCharacter(); <br /> 37 } <br /> 38 <br /> 39 // 如果是\r\n连续的换行符,那么第一次遇到\r字符,将\r字符替换成\n字符,同时设置标志m_skipNextNewLine=true <br /> 40 if (m_nextInputCharacter == '\r') { <br /> 41 m_nextInputCharacter = '\n'; <br /> 42 m_skipNextNewLine = true; <br /> 43 return true; <br /> 44 } <br /> 45 m_skipNextNewLine = false; <br /> 46 if (m_nextInputCharacter || isAtEndOfFile(source)) <br /> 47 return true; <br /> 48 <br /> 49 // 跳过空字符 <br /> 50 if (skipNullCharacters && !m_tokenizer.neverSkipNullCharacters()) { <br /> 51 source.advancePastNonNewline(); <br /> 52 if (source.isEmpty()) <br /> 53 return false; <br /> 54 m_nextInputCharacter = source.currentCharacter(); <br /> 55 goto ProcessAgain; // 跳转到开头 <br /> 56 } <br /> 57 m_nextInputCharacter = replacementCharacter; <br /> 58 return true; <br /> 59 }</p> <p>由于peek方法会跳过空字符,同时合并\r\n字符为\n字符,所以一个字符流source如果包含了空格或者\r\n换行符,实际上处理起来如下图所示:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/d5e0ea62-258f-4346-82b7-9e2eaf11760d.jpg" alt="" /></p> <p>HTMLTokenizer::processToken内部定义了一个状态机,下面以四种情形来进行解释。</p> <p>第一种 <!DCOTYPE>标签</p> <p>1 BEGIN_STATE(DataState) // 刚开始解析是DataState状态 <br /> 2 if (character == '&') <br /> 3 ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInDataState); <br /> 4 if (character == '<') {// 整个字符流一开始是'<',那么表示是一个标签的开始 5 if (haveBufferedCharacterToken()) 6 RETURN_IN_CURRENT_STATE(true); 7 ADVANCE_PAST_NON_NEWLINE_TO(TagOpenState); // 跳转到TagOpenState状态,并取去下一个字符是'!" 8 } 9 if (character == kEndOfFileMarker) 10 return emitEndOfFile(source); 11 bufferCharacter(character); 12 ADVANCE_TO(DataState); 13 END_STATE() 14 15 // ADVANCE_PAST_NON_NEWLINE_TO定义 16 #define ADVANCE_PAST_NON_NEWLINE_TO(newState) \ 17 do { \ 18 if (!m_preprocessor.advancePastNonNewline(source, isNullCharacterSkippingState(newState))) { \ // 如果往下移动取不到下一个字符 19 m_state = newState; \ // 保存状态 20 return haveBufferedCharacterToken(); \ // 返回 21 } \ 22 character = m_preprocessor.nextInputCharacter(); \ // 先取出下一个字符 23 goto newState; \ // 跳转到指定状态 24 } while (false) 25 26 27 BEGIN_STATE(TagOpenState) 28 if (character == '!') // 满足此条件 29 ADVANCE_PAST_NON_NEWLINE_TO(MarkupDeclarationOpenState); // 同理,跳转到MarkupDeclarationOpenState状态,并且取出下一个字符'D' 30 if (character == '/') 31 ADVANCE_PAST_NON_NEWLINE_TO(EndTagOpenState); 32 if (isASCIIAlpha(character)) { 33 m_token.beginStartTag(convertASCIIAlphaToLower(character)); 34 ADVANCE_PAST_NON_NEWLINE_TO(TagNameState); 35 } 36 if (character == '?') { 37 parseError(); 38 // The spec consumes the current character before switching 39 // to the bogus comment state, but it's easier to implement 40 // if we reconsume the current character. 41 RECONSUME_IN(BogusCommentState); 42 } 43 parseError(); 44 bufferASCIICharacter('<'); 45 RECONSUME_IN(DataState); 46 END_STATE() 47 48 BEGIN_STATE(MarkupDeclarationOpenState) 49 if (character == '-') { 50 auto result = source.advancePast("--"); 51 if (result == SegmentedString::DidMatch) { 52 m_token.beginComment(); 53 SWITCH_TO(CommentStartState); 54 } 55 if (result == SegmentedString::NotEnoughCharacters) 56 RETURN_IN_CURRENT_STATE(haveBufferedCharacterToken()); 57 } else if (isASCIIAlphaCaselessEqual(character, 'd')) { // 由于character == 'D',满足此条件 58 auto result = source.advancePastLettersIgnoringASCIICase("doctype"); // 看解码后的字符流中是否有完整的"doctype" 59 if (result == SegmentedString::DidMatch) 60 SWITCH_TO(DOCTYPEState); // 如果匹配,则跳转到DOCTYPEState,同时取出当前指向的字符,由于上面source字符流已经移动了"doctype",因此此时取出的字符为'>' <br /> 61 if (result == SegmentedString::NotEnoughCharacters) // 如果不匹配 <br /> 62 RETURN_IN_CURRENT_STATE(haveBufferedCharacterToken()); // 保存状态,直接返回 <br /> 63 } else if (character == '[' && shouldAllowCDATA()) { <br /> 64 auto result = source.advancePast("[CDATA["); <br /> 65 if (result == SegmentedString::DidMatch) <br /> 66 SWITCH_TO(CDATASectionState); <br /> 67 if (result == SegmentedString::NotEnoughCharacters) <br /> 68 RETURN_IN_CURRENT_STATE(haveBufferedCharacterToken()); <br /> 69 } <br /> 70 parseError(); <br /> 71 RECONSUME_IN(BogusCommentState); <br /> 72 END_STATE() <br /> 73 <br /> 74 <br /> 75 #define SWITCH_TO(newState) \ <br /> 76 do { \ <br /> 77 if (!m_preprocessor.peek(source, isNullCharacterSkippingState(newState))) { \ <br /> 78 m_state = newState; \ <br /> 79 return haveBufferedCharacterToken(); \ <br /> 80 } \ <br /> 81 character = m_preprocessor.nextInputCharacter(); \ // 取出下一个字符 <br /> 82 goto newState; \ // 跳转到指定的state <br /> 83 } while (false) <br /> 84 <br /> 85 <br /> 86 #define RETURN_IN_CURRENT_STATE(expression) \ <br /> 87 do { \ <br /> 88 m_state = currentState; \ // 保存当前状态 <br /> 89 return expression; \ <br /> 90 } while (false) <br /> 91 <br /> 92 <br /> 93 BEGIN_STATE(DOCTYPEState) <br /> 94 if (isTokenizerWhitespace(character)) <br /> 95 ADVANCE_TO(BeforeDOCTYPENameState); <br /> 96 if (character == kEndOfFileMarker) { <br /> 97 parseError(); <br /> 98 m_token.beginDOCTYPE(); <br /> 99 m_token.setForceQuirks(); <br /> 100 return emitAndReconsumeInDataState(); <br /> 101 } <br /> 102 parseError(); <br /> 103 RECONSUME_IN(BeforeDOCTYPENameState); <br /> 104 END_STATE() <br /> 105 <br /> 106 <br /> 107 #define RECONSUME_IN(newState) \ <br /> 108 do { \ // 直接跳转到指定state <br /> 109 goto newState; \ <br /> 110 } while (false) <br /> 111 <br /> 112 <br /> 113 BEGIN_STATE(BeforeDOCTYPENameState) <br /> 114 if (isTokenizerWhitespace(character)) <br /> 115 ADVANCE_TO(BeforeDOCTYPENameState); <br /> 116 if (character == '>') { // character == '>',匹配此处,到此DOCTYPE标签匹配完毕 <br /> 117 parseError(); <br /> 118 m_token.beginDOCTYPE(); <br /> 119 m_token.setForceQuirks(); <br /> 120 return emitAndResumeInDataState(source); <br /> 121 } <br /> 122 if (character == kEndOfFileMarker) { <br /> 123 parseError(); <br /> 124 m_token.beginDOCTYPE(); <br /> 125 m_token.setForceQuirks(); <br /> 126 return emitAndReconsumeInDataState(); <br /> 127 } <br /> 128 m_token.beginDOCTYPE(toASCIILower(character)); <br /> 129 ADVANCE_PAST_NON_NEWLINE_TO(DOCTYPENameState); <br /> 130 END_STATE() <br /> 131 <br /> 132 <br /> 133 <br /> 134 <br /> 135 inline bool HTMLTokenizer::emitAndResumeInDataState(SegmentedString& source) <br /> 136 { <br /> 137 saveEndTagNameIfNeeded(); <br /> 138 m_state = DataState; // 重置状态为初始状态DataState <br /> 139 source.advancePastNonNewline(); // 移动到下一个字符 <br /> 140 return true; <br /> 141 }</p> <p>DOCTYPE Token经历了6个状态最终被解析出来,整个过程如下图所示:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/2ac0c8e2-e773-4655-8c00-793f4379e312.jpg" alt="" /></p> <p>当Token解析完毕之后,分词状态又被重置为DataState,同时需要注意的时,此时字符流source内部指向的是下一个字符'<'。</p> <p>上面代码第61行在用字符流source匹配字符串"doctype"时,可能出现匹配不上的情形。为什么会这样呢?这是因为整个DOM树的构建流程,并不是先要解码完成,解码完成之后获取到完整的字符流才进行分词。从前面解码可以知道,解码可能是一边接收字节流,一边进行解码的,因此分词也是这样,只要能解码出一段字符流,就会立即进行分词。整个流程会出现如下图所示:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/c9290d65-df82-471f-8664-8967bd54da86.jpg" alt="" /></p> <p>由于这个原因,用来分词的字符流可能是不完整的。对于出现不完整情形的DOCTYPE分词过程如下图所示:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/194c46fd-ef2b-4fae-9a35-401ccf7c846a.jpg" alt="" /></p> <p>上面介绍了解码、分词、解码、分词处理DOCTYPE标签的情形,可以看到从逻辑上这种情形与完整解码再分词是一样的。后续介绍的时都会只针对完整解码再分词的情形,对于一边解码一边分词的情形,只需要正确的认识source字符流内部指针的移动,并不难分析。</p> <p>第二种 html标签</p> <p>html标签的分词过程和DOCTYPE类似,其相关代码如下:</p> <p>1 BEGIN_STATE(TagOpenState) <br /> 2 if (character == '!') <br /> 3 ADVANCE_PAST_NON_NEWLINE_TO(MarkupDeclarationOpenState); <br /> 4 if (character == '/') <br /> 5 ADVANCE_PAST_NON_NEWLINE_TO(EndTagOpenState); <br /> 6 if (isASCIIAlpha(character)) { // 在开标签状态下,当前字符为'h' <br /> 7 m_token.beginStartTag(convertASCIIAlphaToLower(character)); // 将'h'添加到Token名中 <br /> 8 ADVANCE_PAST_NON_NEWLINE_TO(TagNameState); // 跳转到TagNameState,并移动到下一个字符't' <br /> 9 } <br /> 10 if (character == '?') { <br /> 11 parseError(); <br /> 12 // The spec consumes the current character before switching <br /> 13 // to the bogus comment state, but it's easier to implement <br /> 14 // if we reconsume the current character. <br /> 15 RECONSUME_IN(BogusCommentState); <br /> 16 } <br /> 17 parseError(); <br /> 18 bufferASCIICharacter('<'); 19 RECONSUME_IN(DataState); 20 END_STATE() 21 22 23 BEGIN_STATE(TagNameState) 24 if (isTokenizerWhitespace(character)) 25 ADVANCE_TO(BeforeAttributeNameState); 26 if (character == '/') 27 ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState); 28 if (character == '>') // 在这个状态下遇到起始标签终止字符 <br /> 29 return emitAndResumeInDataState(source); // 当前分词结束,重置分词状态为DataState <br /> 30 if (m_options.usePreHTML5ParserQuirks && character == '<') <br /> 31 return emitAndReconsumeInDataState(); <br /> 32 if (character == kEndOfFileMarker) { <br /> 33 parseError(); <br /> 34 RECONSUME_IN(DataState); <br /> 35 } <br /> 36 m_token.appendToName(toASCIILower(character)); // 将当前字符添加到Token名 <br /> 37 ADVANCE_PAST_NON_NEWLINE_TO(TagNameState); // 继续跳转到当前状态,并移动到下一个字符 <br /> 38 END_STATE()</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/3ec0e3da-74a2-4afc-b776-91329bd2fde6.jpg" alt="" /></p> <p>第三种 带有属性的标签div</p> <p>HTML标签可以带有属性,属性由属性名和属性值组成,属性之间以及属性与标签名之间用空格分隔:</p> <p>1 <!-- div标签有两个属性,属性名为class和align,它们的值都带有引号 --> <br /> 2 <div class="news" align="center">Hello,World!</div> <br /> 3 <br /> 4 <br /> 5 <!-- 属性值也可以不带引号 --> <br /> 6 <div class=news align=center>Hello,World!</div></p> <p>整个div标签的解析中,标签名div的解析流程和上面的html标签解析一样,当在解析标签名的过程中,碰到了空白字符,说明要开始解析属性了,下面是相关代码:</p> <p>1 BEGIN_STATE(TagNameState) <br /> 2 if (isTokenizerWhitespace(character)) // 在解析TagName时遇到空白字符,标志属性开始 <br /> 3 ADVANCE_TO(BeforeAttributeNameState); <br /> 4 if (character == '/') <br /> 5 ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState); <br /> 6 if (character == '>') <br /> 7 return emitAndResumeInDataState(source); <br /> 8 if (m_options.usePreHTML5ParserQuirks && character == '<') 9 return emitAndReconsumeInDataState(); 10 if (character == kEndOfFileMarker) { 11 parseError(); 12 RECONSUME_IN(DataState); 13 } 14 m_token.appendToName(toASCIILower(character)); 15 ADVANCE_PAST_NON_NEWLINE_TO(TagNameState); 16 END_STATE() 17 18 #define ADVANCE_TO(newState) \ 19 do { \ 20 if (!m_preprocessor.advance(source, isNullCharacterSkippingState(newState))) { \ // 移动到下一个字符 21 m_state = newState; \ 22 return haveBufferedCharacterToken(); \ 23 } \ 24 character = m_preprocessor.nextInputCharacter(); \ 25 goto newState; \ // 跳转到指定状态 26 } while (false) 27 28 29 BEGIN_STATE(BeforeAttributeNameState) 30 if (isTokenizerWhitespace(character)) // 如果标签名后有连续空格,那么就不停的跳过,在当前状态不停循环 31 ADVANCE_TO(BeforeAttributeNameState); 32 if (character == '/') 33 ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState); 34 if (character == '>') <br /> 35 return emitAndResumeInDataState(source); <br /> 36 if (m_options.usePreHTML5ParserQuirks && character == '<') 37 return emitAndReconsumeInDataState(); 38 if (character == kEndOfFileMarker) { 39 parseError(); 40 RECONSUME_IN(DataState); 41 } 42 if (character == '"' || character == '\'' || character == '<' || character == '=') 43 parseError(); 44 m_token.beginAttribute(source.numberOfCharactersConsumed()); // Token的属性列表增加一个,用来存放新的属性名与属性值 45 m_token.appendToAttributeName(toASCIILower(character)); // 添加属性名 46 ADVANCE_PAST_NON_NEWLINE_TO(AttributeNameState); // 跳转到AttributeNameState,并且移动到下一个字符 47 END_STATE() 48 49 50 BEGIN_STATE(AttributeNameState) 51 if (isTokenizerWhitespace(character)) 52 ADVANCE_TO(AfterAttributeNameState); 53 if (character == '/') 54 ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState); 55 if (character == '=') 56 ADVANCE_PAST_NON_NEWLINE_TO(BeforeAttributeValueState); // 在解析属性名的过程中如果碰到=,说明属性名结束,属性值就要开始 57 if (character == '>') <br /> 58 return emitAndResumeInDataState(source); <br /> 59 if (m_options.usePreHTML5ParserQuirks && character == '<') 60 return emitAndReconsumeInDataState(); 61 if (character == kEndOfFileMarker) { 62 parseError(); 63 RECONSUME_IN(DataState); 64 } 65 if (character == '"' || character == '\'' || character == '<' || character == '=') 66 parseError(); 67 m_token.appendToAttributeName(toASCIILower(character)); 68 ADVANCE_PAST_NON_NEWLINE_TO(AttributeNameState); 69 END_STATE() 70 71 72 BEGIN_STATE(BeforeAttributeValueState) 73 if (isTokenizerWhitespace(character)) 74 ADVANCE_TO(BeforeAttributeValueState); 75 if (character == '"') 76 ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueDoubleQuotedState); // 有的属性值有引号包围,这里跳转到AttributeValueDoubleQuotedState,并移动到下一个字符 77 if (character == '&') 78 RECONSUME_IN(AttributeValueUnquotedState); 79 if (character == '\'') 80 ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueSingleQuotedState); 81 if (character == '>') { <br /> 82 parseError(); <br /> 83 return emitAndResumeInDataState(source); <br /> 84 } <br /> 85 if (character == kEndOfFileMarker) { <br /> 86 parseError(); <br /> 87 RECONSUME_IN(DataState); <br /> 88 } <br /> 89 if (character == '<' || character == '=' || character == '`') 90 parseError(); 91 m_token.appendToAttributeValue(character); // 有的属性值没有引号包围,添加属性值字符到Token 92 ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueUnquotedState); // 跳转到AttributeValueUnquotedState,并移动到下一个字符 93 END_STATE() 94 95 BEGIN_STATE(AttributeValueDoubleQuotedState) 96 if (character == '"') { // 在当前状态下如果遇到引号,说明属性值结束 97 m_token.endAttribute(source.numberOfCharactersConsumed()); // 结束属性解析 98 ADVANCE_PAST_NON_NEWLINE_TO(AfterAttributeValueQuotedState); // 跳转到AfterAttributeValueQuotedState,并移动到下一个字符 99 } 100 if (character == '&') { 101 m_additionalAllowedCharacter = '"'; 102 ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInAttributeValueState); 103 } 104 if (character == kEndOfFileMarker) { 105 parseError(); 106 m_token.endAttribute(source.numberOfCharactersConsumed()); 107 RECONSUME_IN(DataState); 108 } 109 m_token.appendToAttributeValue(character); // 将属性值字符添加到Token 110 ADVANCE_TO(AttributeValueDoubleQuotedState); // 跳转到当前状态 111 END_STATE() 112 113 114 BEGIN_STATE(AfterAttributeValueQuotedState) 115 if (isTokenizerWhitespace(character)) 116 ADVANCE_TO(BeforeAttributeNameState); // 属性值解析完毕,如果后面继续跟着空白字符,说明后续还有属性要解析,调回到BeforeAttributeNameState 117 if (character == '/') 118 ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState); 119 if (character == '>') <br /> 120 return emitAndResumeInDataState(source); // 属性值解析完毕,如果遇到'>'字符,说明整个标签也要解析完毕了,此时结束当前标签解析,并且重置分词状态为DataState,并移动到下一个字符 <br /> 121 if (m_options.usePreHTML5ParserQuirks && character == '<') 122 return emitAndReconsumeInDataState(); 123 if (character == kEndOfFileMarker) { 124 parseError(); 125 RECONSUME_IN(DataState); 126 } 127 parseError(); 128 RECONSUME_IN(BeforeAttributeNameState); 129 END_STATE() 130 131 BEGIN_STATE(AttributeValueUnquotedState) 132 if (isTokenizerWhitespace(character)) { // 当解析不带引号的属性值时遇到空白字符(这与带引号的属性值不一样,带引号的属性值可以包含空白字符),说明当前属性解析完毕,后面还有其他属性,跳转到BeforeAttributeNameState,并且移动到下一个字符 133 m_token.endAttribute(source.numberOfCharactersConsumed()); 134 ADVANCE_TO(BeforeAttributeNameState); 135 } 136 if (character == '&') { 137 m_additionalAllowedCharacter = '>'; <br /> 138 ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInAttributeValueState); <br /> 139 } <br /> 140 if (character == '>') { // 解析过程中如果遇到'>'字符,说明整个标签也要解析完毕了,此时结束当前标签解析,并且重置分词状态为DataState,并移动到下一个字符 <br /> 141 m_token.endAttribute(source.numberOfCharactersConsumed()); <br /> 142 return emitAndResumeInDataState(source); <br /> 143 } <br /> 144 if (character == kEndOfFileMarker) { <br /> 145 parseError(); <br /> 146 m_token.endAttribute(source.numberOfCharactersConsumed()); <br /> 147 RECONSUME_IN(DataState); <br /> 148 } <br /> 149 if (character == '"' || character == '\'' || character == '<' || character == '=' || character == '`') <br /> 150 parseError(); <br /> 151 m_token.appendToAttributeValue(character); // 将遇到的属性值字符添加到Token <br /> 152 ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueUnquotedState); // 跳转到当前状态,并且移动到下一个字符 <br /> 153 END_STATE()</p> <p>从代码中可以看到,当属性值带引号和不带引号时,解析的逻辑是不一样的。当属性值带有引号时,属性值里面是可以包含空白字符的。如果属性值不带引号,那么一旦碰到空白字符,说明这个属性就解析结束了,会进入下一个属性的解析当中。</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/4457735e-3d08-4edb-817f-dcb32763cab2.jpg" alt="" /></p> <p>第四种 纯文本解析</p> <p>这里的纯文本指起始标签与结束标签之间的任何纯文字,包括脚本文、CSS文本等等,如下图所示:</p> <!-- div标签中的纯文本 Hello,Word! --> <div class=news align=center>Hello,World!</div> <!-- script标签中的纯文本 window.name = 'Lucy'; --> <script>window.name = 'Lucy';</script> <p>纯文本的解析过程比较简单,就是不停的在DataState状态上跳转,缓存遇到的字符,直到遇见一个结束标签的'<'字符,相关代码如下:</p> <p>1 BEGIN_STATE(DataState) <br /> 2 if (character == '&') <br /> 3 ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInDataState); <br /> 4 if (character == '<') { // 如果在解析文本的过程中遇到开标签,分两种情况 <br /> 5 if (haveBufferedCharacterToken()) // 第一种,如果缓存了文本字符就直接按当前DataState返回,并不移动字符,所以下次再进入分词操作时取到的字符仍为'<' <br /> 6 RETURN_IN_CURRENT_STATE(true); <br /> 7 ADVANCE_PAST_NON_NEWLINE_TO(TagOpenState); // 第二种,如果没有缓存任何文本字符,直接进入TagOpenState状态,进入到起始标签解析过程,并且移动下一个字符 <br /> 8 } <br /> 9 if (character == kEndOfFileMarker) <br /> 10 return emitEndOfFile(source); <br /> 11 bufferCharacter(character); // 缓存遇到的字符 <br /> 12 ADVANCE_TO(DataState); // 循环跳转到当前DataState状态,并且移动到下一个字符 <br /> 13 END_STATE()</p> <p>由于流程比较简单,下面只给出解析div标签中纯文本的结果:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/f2e8a072-6180-43fe-99d0-35eac915dcc5.jpg" alt="" /></p> <p><strong>创建节点与添加节点</strong></p> <p>1 相关类图</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/d4d3785f-d828-4b1b-9d1b-123f341f78d1.jpg" alt="" /></p> <p>2 创建、添加流程</p> <p>上面的分词循环中,每分出一个Token,就会根据Token创建对应的Node,然后将Node添加到DOM树上。(HTMLDocumentParser::pumpTokenizerLoop方法在上面分词中有介绍)。</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/42e68208-2658-4ffe-b34e-62fe96976f81.jpg" alt="" /></p> <p>上面方法中首先看HTMLTreeBuilder::constructTree,代码如下:</p> <p>1 // 只保留关健代码 <br /> 2 void HTMLTreeBuilder::constructTree(AtomHTMLToken&& token) <br /> 3 { <br /> 4 … <br /> 5 <br /> 6 if (shouldProcessTokenInForeignContent(token)) <br /> 7 processTokenInForeignContent(WTFMove(token)); <br /> 8 else <br /> 9 processToken(WTFMove(token)); // HTMLToken在这里被处理 <br /> 10 <br /> 11 … <br /> 12 <br /> 13 m_tree.executeQueuedTasks(); // HTMLContructionSiteTask在这里被执行,有时候也直接在创建的过程中直接执行,然后这个方法发现队列为空就会直接返回 <br /> 14 // The tree builder might have been destroyed as an indirect result of executing the queued tasks. <br /> 15 } <br /> 16 <br /> 17 <br /> 18 void HTMLConstructionSite::executeQueuedTasks() <br /> 19 { <br /> 20 if (m_taskQueue.isEmpty()) // 队列为空,就直接返回 <br /> 21 return; <br /> 22 <br /> 23 // Copy the task queue into a local variable in case executeTask <br /> 24 // re-enters the parser. <br /> 25 TaskQueue queue = WTFMove(m_taskQueue); <br /> 26 <br /> 27 for (auto& task : queue) // 这里的task就是HTMLContructionSiteTask <br /> 28 executeTask(task); // 执行task <br /> 29 <br /> 30 // We might be detached now. <br /> 31 }</p> <p>上面代码中HTMLTreeBuilder::processToken就是处理Token生成对应Node的地方,代码如下所示:</p> <p>1 void HTMLTreeBuilder::processToken(AtomHTMLToken&& token) <br /> 2 { <br /> 3 switch (token.type()) { <br /> 4 case HTMLToken::Uninitialized: <br /> 5 ASSERT_NOT_REACHED(); <br /> 6 break; <br /> 7 case HTMLToken::DOCTYPE: // HTML中的DOCType标签 <br /> 8 m_shouldSkipLeadingNewline = false; <br /> 9 processDoctypeToken(WTFMove(token)); <br /> 10 break; <br /> 11 case HTMLToken::StartTag: // 起始HTML标签 <br /> 12 m_shouldSkipLeadingNewline = false; <br /> 13 processStartTag(WTFMove(token)); <br /> 14 break; <br /> 15 case HTMLToken::EndTag: // 结束HTML标签 <br /> 16 m_shouldSkipLeadingNewline = false; <br /> 17 processEndTag(WTFMove(token)); <br /> 18 break; <br /> 19 case HTMLToken::Comment: // HTML中的注释 <br /> 20 m_shouldSkipLeadingNewline = false; <br /> 21 processComment(WTFMove(token)); <br /> 22 return; <br /> 23 case HTMLToken::Character: // HTML中的纯文本 <br /> 24 processCharacter(WTFMove(token)); <br /> 25 break; <br /> 26 case HTMLToken::EndOfFile: // HTML结束标志 <br /> 27 m_shouldSkipLeadingNewline = false; <br /> 28 processEndOfFile(WTFMove(token)); <br /> 29 break; <br /> 30 } <br /> 31 }</p> <p>可以看到上面代码对7类Token做了处理,由于处理的流程都是类似的,这里只给出3种HTML标签的创建添加过程,分别是DOCTYPE标签,html标签,title标签文本,剩下的过程都使用图表示。</p> <p>2.1 DOCTYPE标签</p> <p>1 // 只保留关健代码 <br /> 2 void HTMLTreeBuilder::processDoctypeToken(AtomHTMLToken&& token) <br /> 3 { <br /> 4 ASSERT(token.type() == HTMLToken::DOCTYPE); <br /> 5 if (m_insertionMode == InsertionMode::Initial) { // m_insertionMode的初始值就是InsertionMode::Initial <br /> 6 m_tree.insertDoctype(WTFMove(token)); // 插入DOCTYPE标签 <br /> 7 m_insertionMode = InsertionMode::BeforeHTML; // 插入DOCTYPE标签之后,m_insertionMode设置为InsertionMode::BeforeHTML,表示下面要开是HTML标签插入 <br /> 8 return; <br /> 9 } <br /> 10 <br /> 11 … <br /> 12 } <br /> 13 <br /> 14 // 只保留关健代码 <br /> 15 void HTMLConstructionSite::insertDoctype(AtomHTMLToken&& token) <br /> 16 { <br /> 17 … <br /> 18 <br /> 19 // m_attachmentRoot就是Document对象,文档根节点 <br /> 20 // DocumentType::create方法创建出DOCTYPE节点 <br /> 21 // attachLater方法内部创建出HTMLContructionSiteTask <br /> 22 attachLater(m_attachmentRoot, DocumentType::create(m_document, token.name(), publicId, systemId)); <br /> 23 <br /> 24 … <br /> 25 } <br /> 26 <br /> 27 // 只保留关健代码 <br /> 28 void HTMLConstructionSite::attachLater(ContainerNode& parent, Ref<Node>&& child, bool selfClosing) <br /> 29 { <br /> 30 … <br /> 31 <br /> 32 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert); // 创建HTMLConstructionSiteTask <br /> 33 task.parent = &parent; // task持有当前节点的父节点 <br /> 34 task.child = WTFMove(child); // task持有需要操作的节点 <br /> 35 task.selfClosing = selfClosing; // 是否自关闭节点 <br /> 36 <br /> 37 // Add as a sibling of the parent if we have reached the maximum depth allowed. <br /> 38 // m_openElements就是HTMLElementStack,在这里还看不到它的作用,后面会讲。这里可以看到这个stack里面加入的对象个数是有限制的,最大不超过512个。 <br /> 39 // 所以如果一个HTML标签嵌套过多的子标签,就会触发这里的操作 <br /> 40 if (m_openElements.stackDepth() > m_maximumDOMTreeDepth && task.parent->parentNode()) <br /> 41 task.parent = task.parent->parentNode(); // 满足条件,就会将当前节点添加到爷爷节点,而不是父节点 <br /> 42 <br /> 43 ASSERT(task.parent); <br /> 44 m_taskQueue.append(WTFMove(task)); // 将task添加到Queue当中 <br /> 45 }</p> <p>从代码可以看到,这里只是创建了DOCTYPE节点,还没有真正添加。真正执行添加的操作,需要执行HTMLContructionSite::executeQueuedTasks,这个方法在一开始有列出来。下面就来看下每个Task如何被执行。</p> <p>1 // 方法位于HTMLContructionSite.cpp <br /> 2 static inline void executeTask(HTMLConstructionSiteTask& task) <br /> 3 { <br /> 4 switch (task.operation) { // HTMLConstructionSiteTask存储了自己要做的操作,构建DOM树一般都是Insert操作 <br /> 5 case HTMLConstructionSiteTask::Insert: <br /> 6 executeInsertTask(task); // 这里执行insert操作 <br /> 7 return; <br /> 8 // All the cases below this point are only used by the adoption agency. <br /> 9 case HTMLConstructionSiteTask::InsertAlreadyParsedChild: <br /> 10 executeInsertAlreadyParsedChildTask(task); <br /> 11 return; <br /> 12 case HTMLConstructionSiteTask::Reparent: <br /> 13 executeReparentTask(task); <br /> 14 return; <br /> 15 case HTMLConstructionSiteTask::TakeAllChildrenAndReparent: <br /> 16 executeTakeAllChildrenAndReparentTask(task); <br /> 17 return; <br /> 18 } <br /> 19 ASSERT_NOT_REACHED(); <br /> 20 } <br /> 21 <br /> 22 // 只保留关健代码,方法位于HTMLContructionSite.cpp <br /> 23 static inline void executeInsertTask(HTMLConstructionSiteTask& task) <br /> 24 { <br /> 25 ASSERT(task.operation == HTMLConstructionSiteTask::Insert); <br /> 26 <br /> 27 insert(task); // 继续调用插入方法 <br /> 28 <br /> 29 … <br /> 30 } <br /> 31 <br /> 32 // 只保留关健代码,方法位于HTMLContructionSite.cpp <br /> 33 static inline void insert(HTMLConstructionSiteTask& task) <br /> 34 { <br /> 35 … <br /> 36 <br /> 37 ASSERT(!task.child->parentNode()); <br /> 38 if (task.nextChild) <br /> 39 task.parent->parserInsertBefore(*task.child, *task.nextChild); <br /> 40 else <br /> 41 task.parent->parserAppendChild(*task.child); // 调用父节点方法继续插入 <br /> 42 } <br /> 43 <br /> 44 // 只保留关健代码 <br /> 45 void ContainerNode::parserAppendChild(Node& newChild) <br /> 46 { <br /> 47 … <br /> 48 <br /> 49 executeNodeInsertionWithScriptAssertion(*this, newChild, ChildChange::Source::Parser, ReplacedAllChildren::No, [&] { <br /> 50 if (&document() != &newChild.document()) <br /> 51 document().adoptNode(newChild); <br /> 52 <br /> 53 appendChildCommon(newChild); // 在Block回调中调用此方法继续插入 <br /> 54 <br /> 55 … <br /> 56 }); <br /> 57 } <br /> 58 <br /> 59 // 最终调用的是这个方法进行插入 <br /> 60 void ContainerNode::appendChildCommon(Node& child) <br /> 61 { <br /> 62 ScriptDisallowedScope::InMainThread scriptDisallowedScope; <br /> 63 <br /> 64 child.setParentNode(this); <br /> 65 <br /> 66 if (m_lastChild) { // 父节点已经插入子节点,运行在这里 <br /> 67 child.setPreviousSibling(m_lastChild); <br /> 68 m_lastChild->setNextSibling(&child); <br /> 69 } else <br /> 70 m_firstChild = &child; // 如果父节点是首次插入子节点,运行在这里 <br /> 71 <br /> 72 m_lastChild = &child; // 更新m_lastChild <br /> 73 }</p> <p>经过执行上面方法之后,原来只有一个根节点的DOM树变成了下面的样子:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/32e0b135-97d0-4b6b-95e0-0e0c87128b58.jpg" alt="" /></p> <p>2.2 html标签</p> <p>1 // processStartTag内部有很多状态处理,这里只保留关健代码 <br /> 2 void HTMLTreeBuilder::processStartTag(AtomHTMLToken&& token) <br /> 3 { <br /> 4 ASSERT(token.type() == HTMLToken::StartTag); <br /> 5 switch (m_insertionMode) { <br /> 6 case InsertionMode::Initial: <br /> 7 defaultForInitial(); <br /> 8 ASSERT(m_insertionMode == InsertionMode::BeforeHTML); <br /> 9 FALLTHROUGH; <br /> 10 case InsertionMode::BeforeHTML: <br /> 11 if (token.name() == htmlTag) { // html标签在这里处理 <br /> 12 m_tree.insertHTMLHtmlStartTagBeforeHTML(WTFMove(token)); <br /> 13 m_insertionMode = InsertionMode::BeforeHead; // 插入完html标签,m_insertionMode = InsertionMode::BeforeHead,表明即将处理head标签 <br /> 14 return; <br /> 15 } <br /> 16 <br /> 17 … <br /> 18 } <br /> 19 } <br /> 20 <br /> 21 <br /> 22 // 只保留关健代码 <br /> 23 void HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML(AtomHTMLToken&& token) <br /> 24 { <br /> 25 auto element = HTMLHtmlElement::create(m_document); // 创建html节点 <br /> 26 setAttributes(element, token, m_parserContentPolicy); <br /> 27 attachLater(m_attachmentRoot, element.copyRef()); // 同样调用了attachLater方法,与DOCTYPE类似 <br /> 28 m_openElements.pushHTMLHtmlElement(HTMLStackItem::create(element.copyRef(), WTFMove(token))); // 注意这里,这里向HTMLElementStack中压入了正在插入的html起始标签 <br /> 29 <br /> 30 executeQueuedTasks(); // 这里在插入操作直接执行了task,外面HTMLTreeBuilder::constructTree方法调用的executeQueuedTasks方法就会直接返回 <br /> 31 <br /> 32 … <br /> 33 }</p> <p>执行上面代码之后,DOM树变成了如下图所示:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/ecca9e71-d6ca-4ffe-8afc-5bd423e5a269.jpg" alt="" /></p> <p>当要插入title起始标签之后,DOM树以及HTMLElementStack m_openElements如下图所示:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/8b279e15-37af-4b5f-9a2c-e179a3597118.jpg" alt="" /></p> <p>3.3 title标签文本,</p> <p>title标签的文本作为文本节点插入,生成文本节点的代码如下:</p> <p>1 // 只保留关健代码 <br /> 2 void HTMLConstructionSite::insertTextNode(const String& characters, WhitespaceMode whitespaceMode) <br /> 3 { <br /> 4 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert); <br /> 5 task.parent = &currentNode(); // 直接取HTMLElementStack m_openElements的栈顶节点,此时节点是title <br /> 6 <br /> 7 … <br /> 8 <br /> 9 unsigned currentPosition = 0; <br /> 10 unsigned lengthLimit = shouldUseLengthLimit(*task.parent) ? Text::defaultLengthLimit : std::numeric_limits<unsigned>::max(); // 限制文本节点最大包含的字符个数为65536 <br /> 11 <br /> 12 … <br /> 13 <br /> 14 <br /> 15 // 可以看到如果文本过长,会将分割成多个文本节点 <br /> 16 while (currentPosition < characters.length()) { 17 AtomString charactersAtom = m_whitespaceCache.lookup(characters, whitespaceMode); 18 auto textNode = Text::createWithLengthLimit(task.parent->document(), charactersAtom.isNull() ? characters : charactersAtom.string(), currentPosition, lengthLimit); <br /> 19 // If we have a whole string of unbreakable characters the above could lead to an infinite loop. Exceeding the length limit is the lesser evil. <br /> 20 if (!textNode->length()) { <br /> 21 String substring = characters.substring(currentPosition); <br /> 22 AtomString substringAtom = m_whitespaceCache.lookup(substring, whitespaceMode); <br /> 23 textNode = Text::create(task.parent->document(), substringAtom.isNull() ? substring : substringAtom.string()); // 生成文本节点 <br /> 24 } <br /> 25 <br /> 26 currentPosition += textNode->length(); // 下一个文本节点包含的字符起点 <br /> 27 ASSERT(currentPosition <= characters.length()); <br /> 28 task.child = WTFMove(textNode); <br /> 29 <br /> 30 executeTask(task); // 直接执行Task插入 <br /> 31 } <br /> 32 }</p> <p>从代码可以看到,如果一个节点后面跟的文本字符过多,会被分割成多个文本节点插入。下面的例子将title节点后面的文本字符个数设置成85248,使用Safari查看确实生成了2个文本节点:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/44aa9914-8f59-4383-b0e3-a899a700c37c.jpg" alt="" /></p> <p>当遇到title结束标签,代码处理如下:</p> <p>1 // 代码内部有很多状态处理,这里只保留关健代码 <br /> 2 void HTMLTreeBuilder::processEndTag(AtomHTMLToken&& token) <br /> 3 { <br /> 4 ASSERT(token.type() == HTMLToken::EndTag); <br /> 5 switch (m_insertionMode) { <br /> 6 … <br /> 7 <br /> 8 case InsertionMode::Text: // 由于遇到title结束标签之前插入了文本,因此此时的插入模式就是InsertionMode::Text <br /> 9 <br /> 10 m_tree.openElements().pop(); // 因为遇到了title结束标签,整个标签已经处理完毕,从HTMLElementStack栈中弹出栈顶元素title <br /> 11 m_insertionMode = m_originalInsertionMode; // 恢复之前的插入模式 <br /> 12 break; <br /> 13 <br /> 14 … <br /> 15 }</p> <p>每当遇到一个标签的结束标签,都会像上面一样将HTMLElementStack m_openElementsStack的栈顶元素弹出。执行上面代码之后,DOM树与HTMLElementStack如下图所示:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/5d0fac63-8fd8-49d3-b31c-bf7204ef5c14.jpg" alt="" /></p> <p>当整个DOM树构建完成之后,DOM树和HTMLElementStack m_openElements如下图所示:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/916a4db2-71f0-4a70-ab59-2f9974a1ac7b.jpg" alt="" /></p> <p>从上图可以看到,当构建完DOM,HTMLElementStack m_openElements并没有将栈完全清空,而是保留了2个节点:html节点与body节点。这可以从Xcode的控制台输出看到:</p> <p><img class='lazyload' data-src="https://article.cdnof.com/2202/92d017fb-3821-478d-9960-f389f97620e1.jpg" alt="" /></p> <p>同时可以看到,内存中的DOM树结构和文章开头画的逻辑上的DOM树结构是不一样的。逻辑上的DOM树父节点有多少子节点,就有多少指向子节点的指针,而内存中的DOM树,不管父节点有多少子节点,始终只有2个指针指向子节点:m_firstChild与m_lastChild。同时,内存中的DOM树兄弟节点之间也相互有指针引用,而逻辑上的DOM树结构是没有的。通过这样的数据结构,使得内存中的DOM结构所占用的空间大大减少,同时也能达到遍历整棵树的效果。试想一下,如果一个父节点有100个子节点,那么使用逻辑上的DOM树结构,父节点就需要100个指向子节点的指针,如果一个指针占用8字节,那么总共就要占用800字节。但是使用上面内存中DOM的表示方式,父节点只需要2个指针就可以了,总共占用16字节,内存消耗大大减少。虽然两者实现方式不一样,但是两者是等价的,都可以正确的表示HTML文档。</p></div></div><div class="MuiGrid-root jss8 MuiGrid-item MuiGrid-grid-xs-true MuiGrid-grid-md-3"><div class="MuiTypography-root jss26 MuiTypography-body1"><div class="MuiTypography-root jss27 MuiTypography-body1"><canvas style="height:108px;width:108px" height="108" width="108"></canvas><div class="MuiTypography-root jss28 MuiTypography-body1"><p class="MuiTypography-root jss29 MuiTypography-body1">手机扫一扫</p><p class="MuiTypography-root jss29 MuiTypography-body1">移动阅读更方便</p></div></div></div><div class="MuiTypography-root jss9 MuiTypography-body1"><div class="MuiTypography-root jss30 MuiTypography-body1" style="height:150px"><div class="swiper-container jss32"><div class="swiper-pagination"></div><div class="swiper-wrapper"><div class="swiper-slide jss32"><a class="MuiTypography-root MuiLink-root MuiLink-underlineHover jss32 MuiTypography-colorInherit" target="_blank" rel="nofollow noopener noreferrer" href="https://qd.rs/aliyun"><img alt="阿里云服务器" class="jss31" src="https://article.cdnof.com/promotion/aliyun.jpg"/></a></div><div class="swiper-slide jss32"><a class="MuiTypography-root MuiLink-root MuiLink-underlineHover jss32 MuiTypography-colorInherit" target="_blank" rel="nofollow noopener noreferrer" href="https://qd.rs/tencent"><img alt="腾讯云服务器" class="jss31" src="https://article.cdnof.com/promotion/tencent.jpg"/></a></div><div class="swiper-slide jss32"><a class="MuiTypography-root MuiLink-root MuiLink-underlineHover jss32 MuiTypography-colorInherit" target="_blank" rel="nofollow noopener noreferrer" href="https://qd.rs/qiniu"><img alt="七牛云服务器" class="jss31" src="https://article.cdnof.com/promotion/qiniu.png"/></a></div></div></div></div></div><div class="MuiTypography-root MuiTypography-body1"><div class="MuiTypography-root jss33 MuiTypography-body1"><p class="MuiTypography-root jss34 MuiTypography-body1">你可能感兴趣的文章</p><div class="MuiList-root MuiList-padding" aria-label="main mailbox folders"></div></div></div></div></div></div><footer style="margin-top:30px"><p class="MuiTypography-root MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-alignCenter">Copyright © <a class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorInherit" href="https://v2as.com" title="哇哦,有大量工具等你探索">V2AS | 问路</a> <!-- -->2024<!-- --> <!-- -->.</p><p class="MuiTypography-root MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-alignCenter"><a class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorInherit" rel="nofollow noopener noreferrer" href="https://beian.miit.gov.cn/">浙ICP备15029886号</a></p></footer></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"article":{"article_id":"9bc05d0e-4e32-4fff-9f3d-c5a652f463bd","title":"WebKit Inside: DOM树的构建","link":"","description":"当客户端App主进程创建WKWebView对象时,会创建另外两个子进程:渲染进程与网络进程。主进程WKWebView发起请求时,先将请求转发给渲染进程,渲染进程再转发给网络进程,网络进程请求服务器。如果请求的是一个网页,网络进程会将服务器的响应数据HTML文件字符流吐给渲染进程。渲染进程拿到HTML文件字符流,首先要进行解析,将HTML文件字符流转换成DOM树,然后在DOM树的基础上,进行渲染操作","image":"https://article.cdnof.com/2202/1d0c98f2-68a9-4ec7-ba28-73e5d2df5640.jpg","keywords":["character","标签","token","字符","HTML","return","DOM","节点","source","TO"],"created_at":"2022-02-21T23:20:33.625Z","html":"\u003cp\u003e当客户端App主进程创建WKWebView对象时,会创建另外两个子进程:渲染进程与网络进程。主进程WKWebView发起请求时,先将请求转发给渲染进程,渲染进程再转发给网络进程,网络进程请求服务器。如果请求的是一个网页,网络进程会将服务器的响应数据HTML文件字符流吐给渲染进程。渲染进程拿到HTML文件字符流,首先要进行解析,将HTML文件字符流转换成DOM树,然后在DOM树的基础上,进行渲染操作,也就是布局、绘制。最后渲染进程将渲染数据吐给主进程WKWebView,WKWebView根据渲染数据创建对应的View展现视图。整个流程如下图所示:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/1d0c98f2-68a9-4ec7-ba28-73e5d2df5640.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e什么是DOM树\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e渲染进程获取到HTML文件字符流,会将HTML文件字符流转换成DOM树。下图中左侧是一个HTML文件,右边就是转换而成的DOM树。\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/51472393-6829-41ef-81be-7e0eabbee17a.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e可以看到DOM树的根节点是HTMLDocument,代表整个文档。根节点下面的子节点与HTML文件中的标签是一一对应的,比如HTML中的\u003ccode\u003e\u0026lt;head\u0026gt;\u003c/code\u003e标签就对应DOM树中的head节点。同时HTML文件中的文本,也成为DOM树中的一个节点,比如文本'Hello, World!',在DOM树中就成为div节点的子节点。\u003c/p\u003e\n\u003cp\u003e在DOM树中每一个节点都是具有一定方法与属性的对象,这些对象由对应的类创建出来。比如HTMLDocument节点,它对应的类是class HTMLDocument,下面是HTMLDocument的部分源码:\u003c/p\u003e\n\u003cp\u003e1 class HTMLDocument : public Document { // 继承自Document \u003cbr /\u003e\n2 … \u003cbr /\u003e\n3 WEBCORE_EXPORT int width(); \u003cbr /\u003e\n4 WEBCORE_EXPORT int height(); \u003cbr /\u003e\n5 … \u003cbr /\u003e\n6 }\u003c/p\u003e\n\u003cp\u003e从源码中可以看到,HTMLDocument继承自类Document,Document类的部分源码如下:\u003c/p\u003e\n\u003cp\u003e1 class Document \u003cbr /\u003e\n 2 : public ContainerNode // Document继承自ContainerNode,ContainerNode继承自Node \u003cbr /\u003e\n 3 , public TreeScope \u003cbr /\u003e\n 4 , public ScriptExecutionContext \u003cbr /\u003e\n 5 , public FontSelectorClient \u003cbr /\u003e\n 6 , public FrameDestructionObserver \u003cbr /\u003e\n 7 , public Supplementable\u003cDocument\u003e \u003cbr /\u003e\n 8 , public Logger::Observer \u003cbr /\u003e\n 9 , public CanvasObserver { \u003cbr /\u003e\n10 WEBCORE_EXPORT ExceptionOr\u003cRef\u003cElement\u003e\u0026gt; createElementForBindings(const AtomString\u0026amp; tagName); // 创建Element的方法 \u003cbr /\u003e\n11 WEBCORE_EXPORT Ref\u003cText\u003e createTextNode(const String\u0026amp; data); // 创建文本节点的方法 \u003cbr /\u003e\n12 WEBCORE_EXPORT Ref\u003cComment\u003e createComment(const String\u0026amp; data); // 创建注释的方法 \u003cbr /\u003e\n13 WEBCORE_EXPORT Ref\u003cElement\u003e createElement(const QualifiedName\u0026amp;, bool createdByParser); // 创建Element方法 \u003cbr /\u003e\n14 …. \u003cbr /\u003e\n15 }\u003c/p\u003e\n\u003cp\u003e上面源码可以看到Document继承自Node,而且还可以看到前端十分熟悉的createElement、createTextNode等方法,JavaScript对这些方法的调用,最后都转换为对应C++方法的调用。\u003c/p\u003e\n\u003cp\u003e类Document有这些方法,并不是没有原因的,而是W3C组织给出的标准规定的,这个标准就是\u003ca rel=\"nofollow noopener noreferrer\" href=\"https://dom.spec.whatwg.org/#introduction-to-the-dom\"\u003eDOM(\u003cstrong\u003eD\u003c/strong\u003eocument \u003cstrong\u003eO\u003c/strong\u003ebject \u003cstrong\u003eM\u003c/strong\u003eodel,文档对象模型)\u003c/a\u003e。DOM定义了DOM树中每个节点需要实现的接口和属性,下面是HTMLDocument、Document、HTMLDivElment的部分IDL(\u003cstrong\u003eI\u003c/strong\u003enteractive \u003cstrong\u003eD\u003c/strong\u003eata \u003cstrong\u003eL\u003c/strong\u003eanguage,接口描述语言,与具体平台和语言无关)描述,完整的IDL可以参看\u003ca rel=\"nofollow noopener noreferrer\" href=\"https://dom.spec.whatwg.org/review-drafts/2020-06/#concept-tree\"\u003eW3C\u003c/a\u003e 。\u003c/p\u003e\n\u003cp\u003e1 interface HTMLDocument : Document { // HTMLDocument \u003cbr /\u003e\n 2 getter (WindowProxy or Element or HTMLCollection) (DOMString name); \u003cbr /\u003e\n 3 }; \u003cbr /\u003e\n 4 \u003cbr /\u003e\n 5 \u003cbr /\u003e\n 6 interface Document : Node { // Document \u003cbr /\u003e\n 7 [NewObject, ImplementedAs=createElementForBindings] Element createElement(DOMString localName); // createElement \u003cbr /\u003e\n 8 [NewObject] Text createTextNode(DOMString data); // createTextNode \u003cbr /\u003e\n 9 … \u003cbr /\u003e\n10 } \u003cbr /\u003e\n11 \u003cbr /\u003e\n12 \u003cbr /\u003e\n13 interface HTMLDivElement : HTMLElement { // HTMLDivElement \u003cbr /\u003e\n14 [CEReactions=NotNeeded, Reflect] attribute DOMString align; \u003cbr /\u003e\n15 };\u003c/p\u003e\n\u003cp\u003e在DOM树中,每一个节点都继承自类Node,同时Node还有一个子类Element,有的节点直接继承自类Node,比如文本节点,而有的节点继承自类Element,比如div节点。因此针对上面图中的DOM树,执行下面的JavaScript语句返回的结果是不一样的:\u003c/p\u003e\n\u003cp\u003e1 document.childNodes; // 返回子Node集合,返回DocumentType与HTML节点,都继承自Node \u003cbr /\u003e\n2 document.children; // 返回子Element集合,只返回HTML节点,DocumentType不继承自Element\u003c/p\u003e\n\u003cp\u003e下图给出部分节点的继承关系图:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/ba140af8-7601-4949-8d4d-61775513e786.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDOM树的构建\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eDOM树的构建流程可以分位4个步骤: 解码、分词、创建节点、添加节点。\u003c/p\u003e\n\u003cp\u003e1 解码\u003c/p\u003e\n\u003cp\u003e渲染进程从网络进程接收过来的是HTML字节流,而下一步分词是以字符为单位进行的。由于各种编码规范的存在,比如ISO-8859-1、UTF-8等,一个字符常常可能对应一个或者多个编码后的字节,解码的目的就是将HTML字节流转换成HTML字符流,或者换句话说,就是将原始的HTML字节流转换成字符串。\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/27b0ca6b-f8eb-41ee-a0c9-06b08e2cf410.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e2 解码类图\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/0c1d12d2-8a71-4adf-baef-36987b792aec.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e从类图上看,类HTMLDocumentParser处于解码的核心位置,由这个类调用解码器将HTML字节流解码成字符流,存储到类HTMLInputStream中。\u003c/p\u003e\n\u003cp\u003e3 解码流程\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/ad5391a2-dcac-40bb-9bd8-529589ef6958.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e整个解码流程当中,最关健的是如何找到正确的编码方式。只有找到了正确的编码方式,才能使用对应的解码器进行解码。解码发生的地方如下面源代码所示,这个方法在上图第3个栈帧被调用:\u003c/p\u003e\n\u003cp\u003e1 // HTMLDocumentParser是DecodedDataDocumentParser的子类 \u003cbr /\u003e\n 2 void DecodedDataDocumentParser::appendBytes(DocumentWriter\u0026amp; writer, const uint8_t* data, size_t length) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 if (!length) \u003cbr /\u003e\n 5 return; \u003cbr /\u003e\n 6 \u003cbr /\u003e\n 7 String decoded = writer.decoder().decode(data, length); // 真正解码发生在这里 \u003cbr /\u003e\n 8 if (decoded.isEmpty()) \u003cbr /\u003e\n 9 return; \u003cbr /\u003e\n10 \u003cbr /\u003e\n11 writer.reportDataReceived(); \u003cbr /\u003e\n12 append(decoded.releaseImpl()); \u003cbr /\u003e\n13 }\u003c/p\u003e\n\u003cp\u003e上面代码第7行writer.decoder()返回一个TextResourceDecoder对象,解码操作由TextResourceDecoder::decode方法完成。下面逐步查看TextResourceDecoder::decode方法的源码:\u003c/p\u003e\n\u003cp\u003e1 // 只保留了最重要的部分 \u003cbr /\u003e\n 2 2 String TextResourceDecoder::decode(const char* data, size_t length) \u003cbr /\u003e\n 3 3 { \u003cbr /\u003e\n 4 4 … \u003cbr /\u003e\n 5 5 \u003cbr /\u003e\n 6 6 // 如果是HTML文件,就从head标签中寻找字符集 \u003cbr /\u003e\n 7 7 if ((m_contentType == HTML || m_contentType == XML) \u0026amp;\u0026amp; !m_checkedForHeadCharset) // HTML and XML \u003cbr /\u003e\n 8 8 if (!checkForHeadCharset(data, length, movedDataToBuffer)) \u003cbr /\u003e\n 9 9 return emptyString(); \u003cbr /\u003e\n10 10 \u003cbr /\u003e\n11 11 … \u003cbr /\u003e\n12 12 \u003cbr /\u003e\n13 13 // m_encoding存储者从HTML文件中找到的编码名称 \u003cbr /\u003e\n14 14 if (!m_codec) \u003cbr /\u003e\n15 15 m_codec = newTextCodec(m_encoding); // 创建具体的编码器 \u003cbr /\u003e\n16 16 \u003cbr /\u003e\n17 17 … \u003cbr /\u003e\n18 18 \u003cbr /\u003e\n19 19 // 解码并返回 \u003cbr /\u003e\n20 20 String result = m_codec-\u0026gt;decode(m_buffer.data() + lengthOfBOM, m_buffer.size() - lengthOfBOM, false, m_contentType == XML \u0026amp;\u0026amp; !m_useLenientXMLDecoding, m_sawError); \u003cbr /\u003e\n21 21 m_buffer.clear(); // 清空存储的原始未解码的HTML字节流 \u003cbr /\u003e\n22 22 return result; \u003cbr /\u003e\n23 23 }\u003c/p\u003e\n\u003cp\u003e从源码中可以看到,TextResourceDecoder首先从HTML的\u003chead\u003e标签中去找编码方式,因为\u003chead\u003e标签可以包含\u003cmeta\u003e标签,\u003cmeta\u003e标签可以设置HTML文件的字符集:\u003c/p\u003e\n\u003cp\u003e1 \u003chead\u003e \n2 \u003cmeta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /\u003e \u003c!-- 字符集指定--\u003e \n3 \u003ctitle\u003eDOM Tree\u003c/title\u003e \n4 \u003cscript\u003ewindow.name = 'Lucy';\u003c/script\u003e \n5 \u003c/head\u003e\u003c/p\u003e\n\u003cp\u003e如果能找到对应的字符集,TextResourceDeocder将其存储在成员变量m_encoding当中,并且根据对应的编码创建真正的解码器存储在成员变量m_codec中,最终使用m_codec对字节流进行解码,并且返回解码后的字符串。如果带有字符集的\u003cmeta\u003e标签没有找到,TextResourceDeocder的m_encoding有默认值windows-1252(等同于ISO-8859-1)。\u003c/p\u003e\n\u003cp\u003e下面看一下TextResourceDecoder寻找\u003cmeta\u003e标签中字符集的流程,也就是上面源码中第8行对checkForHeadCharset函数的调用:\u003c/p\u003e\n\u003cp\u003e1 // 只保留了关健代码 \u003cbr /\u003e\n 2 bool TextResourceDecoder::checkForHeadCharset(const char* data, size_t len, bool\u0026amp; movedDataToBuffer) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 … \u003cbr /\u003e\n 5 \u003cbr /\u003e\n 6 // This is not completely efficient, since the function might go \u003cbr /\u003e\n 7 // through the HTML head several times. \u003cbr /\u003e\n 8 \u003cbr /\u003e\n 9 size_t oldSize = m_buffer.size(); \u003cbr /\u003e\n10 m_buffer.grow(oldSize + len); \u003cbr /\u003e\n11 memcpy(m_buffer.data() + oldSize, data, len); // 将字节流数据拷贝到自己的缓存m_buffer里面 \u003cbr /\u003e\n12 \u003cbr /\u003e\n13 movedDataToBuffer = true; \u003cbr /\u003e\n14 \u003cbr /\u003e\n15 // Continue with checking for an HTML meta tag if we were already doing so. \u003cbr /\u003e\n16 if (m_charsetParser) \u003cbr /\u003e\n17 return checkForMetaCharset(data, len); // 如果已经存在了meta标签解析器,直接开始解析 \u003cbr /\u003e\n18 \u003cbr /\u003e\n19 …. \u003cbr /\u003e\n20 \u003cbr /\u003e\n21 m_charsetParser = makeUnique\u003cHTMLMetaCharsetParser\u003e(); // 创建meta标签解析器 \u003cbr /\u003e\n22 return checkForMetaCharset(data, len); \u003cbr /\u003e\n23 }\u003c/p\u003e\n\u003cp\u003e上面源代码中第11行,类TextResourceDecoder内部存储了需要解码的HTML字节流,这一步骤很重要,后面会讲到。先看第17行、21行、22行,这3行主要是使用\u003cmeta\u003e标签解析器解析字符集,使用了懒加载的方式。下面看下checkForMetaCharset这个函数的实现:\u003c/p\u003e\n\u003cp\u003e1 bool TextResourceDecoder::checkForMetaCharset(const char* data, size_t length) \u003cbr /\u003e\n 2 { \u003cbr /\u003e\n 3 if (!m_charsetParser-\u0026gt;checkForMetaCharset(data, length)) // 解析meta标签字符集 \u003cbr /\u003e\n 4 return false; \u003cbr /\u003e\n 5 \u003cbr /\u003e\n 6 setEncoding(m_charsetParser-\u0026gt;encoding(), EncodingFromMetaTag); // 找到后设置字符编码名称 \u003cbr /\u003e\n 7 m_charsetParser = nullptr; \u003cbr /\u003e\n 8 m_checkedForHeadCharset = true; \u003cbr /\u003e\n 9 return true; \u003cbr /\u003e\n10 }\u003c/p\u003e\n\u003cp\u003e上面源码第3行可以看到,整个解析\u003cmeta\u003e标签的任务在类HTMLMetaCharsetParser::checkForMetaCharset中完成。\u003c/p\u003e\n\u003cp\u003e1 // 只保留了关健代码 \u003cbr /\u003e\n 2 bool HTMLMetaCharsetParser::checkForMetaCharset(const char* data, size_t length) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 if (m_doneChecking) // 标志位,避免重复解析 \u003cbr /\u003e\n 5 return true; \u003cbr /\u003e\n 6 \u003cbr /\u003e\n 7 \u003cbr /\u003e\n 8 // We still don't have an encoding, and are in the head. \u003cbr /\u003e\n 9 // The following tags are allowed in \u003chead\u003e: \n10 // SCRIPT|STYLE|META|LINK|OBJECT|TITLE|BASE \n11 // \n12 // We stop scanning when a tag that is not permitted in \u003chead\u003e \n13 // is seen, rather when \u003c/head\u003e is seen, because that more closely \u003cbr /\u003e\n14 // matches behavior in other browsers; more details in \u003cbr /\u003e\n15 // \u003ca rel=\"nofollow noopener noreferrer\" href=\"http://bugs.webkit.org/show_bug.cgi?id=3590\"\u003ehttp://bugs.webkit.org/show_bug.cgi?id=3590\u003c/a\u003e. \u003cbr /\u003e\n16 // \u003cbr /\u003e\n17 // Additionally, we ignore things that looks like tags in \u003ctitle\u003e, \u003cscript\u003e \u003cbr /\u003e\n18 // and \u003cnoscript\u003e; see \u003ca rel=\"nofollow noopener noreferrer\" href=\"http://bugs.webkit.org/show_bug.cgi?id=4560\"\u003ehttp://bugs.webkit.org/show_bug.cgi?id=4560\u003c/a\u003e, \u003cbr /\u003e\n19 // \u003ca rel=\"nofollow noopener noreferrer\" href=\"http://bugs.webkit.org/show_bug.cgi?id=12165\"\u003ehttp://bugs.webkit.org/show_bug.cgi?id=12165\u003c/a\u003e and \u003cbr /\u003e\n20 // \u003ca rel=\"nofollow noopener noreferrer\" href=\"http://bugs.webkit.org/show_bug.cgi?id=12389\"\u003ehttp://bugs.webkit.org/show_bug.cgi?id=12389\u003c/a\u003e. \u003cbr /\u003e\n21 // \u003cbr /\u003e\n22 // Since many sites have charset declarations after \u003cbody\u003e or other tags \u003cbr /\u003e\n23 // that are disallowed in \u003chead\u003e, we don't bail out until we've checked at \u003cbr /\u003e\n24 // least bytesToCheckUnconditionally bytes of input. \u003cbr /\u003e\n25 \u003cbr /\u003e\n26 constexpr int bytesToCheckUnconditionally = 1024; // 如果解析了1024个字符还未找到带有字符集的\u003cmeta\u003e标签,整个解析也算完成,此时没有解析到正确的字符集,就使用默认编码windows-1252(等同于ISO-8859-1) \u003cbr /\u003e\n27 \u003cbr /\u003e\n28 bool ignoredSawErrorFlag; \u003cbr /\u003e\n29 m_input.append(m_codec-\u0026gt;decode(data, length, false, false, ignoredSawErrorFlag)); // 对字节流进行解码 \u003cbr /\u003e\n30 \u003cbr /\u003e\n31 while (auto token = m_tokenizer.nextToken(m_input)) { // m_tokenizer进行分词操作,找meta标签也需要进行分词,分词操作后面讲 \u003cbr /\u003e\n32 bool isEnd = token-\u0026gt;type() == HTMLToken::EndTag; \u003cbr /\u003e\n33 if (isEnd || token-\u0026gt;type() == HTMLToken::StartTag) { \u003cbr /\u003e\n34 AtomString tagName(token-\u0026gt;name()); \u003cbr /\u003e\n35 if (!isEnd) { \u003cbr /\u003e\n36 m_tokenizer.updateStateFor(tagName); \u003cbr /\u003e\n37 if (tagName == metaTag \u0026amp;\u0026amp; processMeta(*token)) { // 找到meta标签进行处理 \u003cbr /\u003e\n38 m_doneChecking = true; \u003cbr /\u003e\n39 return true; // 如果找到了带有编码的meta标签,直接返回 \u003cbr /\u003e\n40 } \u003cbr /\u003e\n41 } \u003cbr /\u003e\n42 \u003cbr /\u003e\n43 if (tagName != scriptTag \u0026amp;\u0026amp; tagName != noscriptTag \u003cbr /\u003e\n44 \u0026amp;\u0026amp; tagName != styleTag \u0026amp;\u0026amp; tagName != linkTag \u003cbr /\u003e\n45 \u0026amp;\u0026amp; tagName != metaTag \u0026amp;\u0026amp; tagName != objectTag \u003cbr /\u003e\n46 \u0026amp;\u0026amp; tagName != titleTag \u0026amp;\u0026amp; tagName != baseTag \u003cbr /\u003e\n47 \u0026amp;\u0026amp; (isEnd || tagName != htmlTag) \u003cbr /\u003e\n48 \u0026amp;\u0026amp; (isEnd || tagName != headTag)) { \u003cbr /\u003e\n49 m_inHeadSection = false; \u003cbr /\u003e\n50 } \u003cbr /\u003e\n51 } \u003cbr /\u003e\n52 \u003cbr /\u003e\n53 if (!m_inHeadSection \u0026amp;\u0026amp; m_input.numberOfCharactersConsumed() \u0026gt;= bytesToCheckUnconditionally) { // 如果分词已经进入了\u003cbody\u003e标签范围,同时分词数量已经超过了1024,也算成功 \u003cbr /\u003e\n54 m_doneChecking = true; \u003cbr /\u003e\n55 return true; \u003cbr /\u003e\n56 } \u003cbr /\u003e\n57 } \u003cbr /\u003e\n58 \u003cbr /\u003e\n59 return false; \u003cbr /\u003e\n60 }\u003c/p\u003e\n\u003cp\u003e上面源码第29行,类HTMLMetaCharsetParser也有一个解码器m_codec,解码器是在HTMLMetaCharsetParser对象创建时生成,这个解码器的真实类型是TextCodecLatin1(Latin1编码也就是ISO-8859-1,等同于windows-1252编码)。之所以可以直接使用TextCodecLatin1解码器,是因为\u003cmeta\u003e标签如果设置正确,都是英文字符,完全可以使用TextCodecLatin1进行解析出来。这样就避免了为了找到\u003cmeta\u003e标签,需要对字节流进行解码,而要解码就必须要找到\u003cmeta\u003e标签这种鸡生蛋、蛋生鸡的问题。\u003c/p\u003e\n\u003cp\u003e代码第37行对找到的\u003cmeta\u003e标签进行处理,这个函数比较简单,主要是解析\u003cmeta\u003e标签当中的属性,然后查看这些属性名中有没有charset。\u003c/p\u003e\n\u003cp\u003e1 bool HTMLMetaCharsetParser::processMeta(HTMLToken\u0026amp; token) \u003cbr /\u003e\n 2 { \u003cbr /\u003e\n 3 AttributeList attributes; \u003cbr /\u003e\n 4 for (auto\u0026amp; attribute : token.attributes()) { // 获取meta标签属性 \u003cbr /\u003e\n 5 String attributeName = StringImpl::create8BitIfPossible(attribute.name); \u003cbr /\u003e\n 6 String attributeValue = StringImpl::create8BitIfPossible(attribute.value); \u003cbr /\u003e\n 7 attributes.append(std::make_pair(attributeName, attributeValue)); \u003cbr /\u003e\n 8 } \u003cbr /\u003e\n 9 \u003cbr /\u003e\n10 m_encoding = encodingFromMetaAttributes(attributes); // 从属性中找字符集设置属性charset \u003cbr /\u003e\n11 return m_encoding.isValid(); \u003cbr /\u003e\n12 }\u003c/p\u003e\n\u003cp\u003e上面分析TextResourceDecoder::checkForHeadCharset函数时,讲过第11行TextResourceDecoder类存储HTML字节流的操作很重要。原因是可能整个HTML字节流里面可能确实没有设置charset的\u003cmeta\u003e标签,此时TextResourceDecoder::checkForHeadCharset函数就要返回false,导致TextResourceDecoder::decode函数返回空字符串,也就是不进行任何解码。是不是这样呢?真实的情况是,在接收HTML字节流整个过程中由于确实没有找到带有charset属性的\u003cmeta\u003e标签,那么整个接收期间都不会解码。但是完整的HTML字节流会被存储在TextResourceDecoder的成员变量m_buffer里面,当整个HTML字节流接收结束的时,会有如下调用栈:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/ade3b671-1ecf-42b4-95e8-59882f442317.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e从调用栈可以看到,当HTML字节流接收完成,最终会调用TextResourceDecoder::flush方法,这个方法会将TextResourceDecoder中有m_buffer存储的HTML字节流进行解码,由于在接收HTML字节流期间未成功找到编码方式,因此m_buffer里面存储的就是所有待解码的HTML字节流,然后在这里使用默认的编码windows-1252对全部字节流进行解码。因此,如果HTML字节流中包含汉字,那么如果不指定字符集,最终页面就会出现乱码。解码完成后,会将解码之后的字符流存储到HTMLDocumentParser中。\u003c/p\u003e\n\u003cp\u003e1 void DecodedDataDocumentParser::flush(DocumentWriter\u0026amp; writer) \u003cbr /\u003e\n2 { \u003cbr /\u003e\n3 String remainingData = writer.decoder().flush(); \u003cbr /\u003e\n4 if (remainingData.isEmpty()) \u003cbr /\u003e\n5 return; \u003cbr /\u003e\n6 \u003cbr /\u003e\n7 writer.reportDataReceived(); \u003cbr /\u003e\n8 append(remainingData.releaseImpl()); // 解码后的字符流存储到HTMLDocumentParser \u003cbr /\u003e\n9 }\u003c/p\u003e\n\u003cp\u003e4 解码总结\u003c/p\u003e\n\u003cp\u003e整个解码过程可以分位两种情形: 第一种情形是HTML字节流可以解析出带有charset属性的\u003cmeta\u003e标签,这样就可以获取相应的编码方式,那么每接收到一个HML字节流,都可以使用相应的编码方式进行解码,将解码后的字符流添加到HTMLInputStream当中;第二种是HTML字节流不能解析带有charset属性的\u003cmeta\u003e标签,这样每接收到一个HTML字节流,都缓存到TextResourceDecoder的m_buffer缓存,等完整的HTML字节流接收完毕,就会使用默认的编码windows-1252进行解码。\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/b6dd5e5a-c48f-494c-8895-196dafb33d53.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/b7818491-90b2-49bf-8dff-918aec2c3e30.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e分词\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e接收到的HTML字节流经过解码,成为存储在HTMLInputStream中的字符流。分词的过程就是从HTMLInputStream中依次取出每一个字符,然后判断字符是否是特殊的HTML字符'\u003c'、'/'、'\u003e'、'='等。根据这些特殊字符的分割,就能解析出HTML标签名以及属性列表,类HTMLToken就是存储分词出来的结果。\u003c/p\u003e\n\u003cp\u003e1 分词类图\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/39ec0a66-7067-4e92-949a-b5c5684a5632.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e从类图中可以看到,分词最重要的是类HTMLTokenizer和类HTMLToken。下面是类HTMLToken的主要信息:\u003c/p\u003e\n\u003cp\u003e1 // 只保留了主要信息 \u003cbr /\u003e\n 2 2 class HTMLToken { \u003cbr /\u003e\n 3 3 public: \u003cbr /\u003e\n 4 4 enum Type { // Token的类型 \u003cbr /\u003e\n 5 5 Uninitialized, // Token初始化时的类型 \u003cbr /\u003e\n 6 6 DOCTYPE, // 代表Token是DOCType标签 \u003cbr /\u003e\n 7 7 StartTag, // 代表Token是一个开始标签 \u003cbr /\u003e\n 8 8 EndTag, // 代表Token是一个结束标签 \u003cbr /\u003e\n 9 9 Comment, // 代表Token是一个注释 \u003cbr /\u003e\n10 10 Character, // 代表Token是文本 \u003cbr /\u003e\n11 11 EndOfFile, // 代表Token是文件结尾 \u003cbr /\u003e\n12 12 }; \u003cbr /\u003e\n13 13 \u003cbr /\u003e\n14 14 struct Attribute { // 存储属性的数据结构 \u003cbr /\u003e\n15 15 Vector\u003cUChar, 32\u003e name; // 属性名 \u003cbr /\u003e\n16 16 Vector\u003cUChar, 64\u003e value; // 属性值 \u003cbr /\u003e\n17 17 \u003cbr /\u003e\n18 18 // Used by HTMLSourceTracker. \u003cbr /\u003e\n19 19 unsigned startOffset; \u003cbr /\u003e\n20 20 unsigned endOffset; \u003cbr /\u003e\n21 21 }; \u003cbr /\u003e\n22 22 \u003cbr /\u003e\n23 23 typedef Vector\u003cAttribute, 10\u003e AttributeList; // 属性列表 \u003cbr /\u003e\n24 24 typedef Vector\u003cUChar, 256\u003e DataVector; // 存储Token名 \u003cbr /\u003e\n25 25 \u003cbr /\u003e\n26 26 … \u003cbr /\u003e\n27 27 \u003cbr /\u003e\n28 28 private: \u003cbr /\u003e\n29 29 Type m_type; \u003cbr /\u003e\n30 30 DataVector m_data; \u003cbr /\u003e\n31 31 // For StartTag and EndTag \u003cbr /\u003e\n32 32 bool m_selfClosing; // Token是注入\u003cimg\u003e一样自结束标签 \u003cbr /\u003e\n33 33 AttributeList m_attributes; \u003cbr /\u003e\n34 34 Attribute* m_currentAttribute; // 当前正在解析的属性 \u003cbr /\u003e\n35 35 };\u003c/p\u003e\n\u003cp\u003e2 分词流程\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/6b978902-3093-4129-817e-ff76506237ef.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e上面分词流程中HTMLDocumentParser::pumpTokenizerLoop方法是最重要的,从方法名字可以看出这个方法里面包含循环逻辑:\u003c/p\u003e\n\u003cp\u003e1 // 只保留关健代码 \u003cbr /\u003e\n 2 bool HTMLDocumentParser::pumpTokenizerLoop(SynchronousMode mode, bool parsingFragment, PumpSession\u0026amp; session) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 do { // 分词循环体开始 \u003cbr /\u003e\n 5 … \u003cbr /\u003e\n 6 \u003cbr /\u003e\n 7 if (UNLIKELY(mode == AllowYield \u0026amp;\u0026amp; m_parserScheduler-\u0026gt;shouldYieldBeforeToken(session))) // 避免长时间处于分词循环中,这里根据条件暂时退出循环 \u003cbr /\u003e\n 8 return true; \u003cbr /\u003e\n 9 \u003cbr /\u003e\n10 if (!parsingFragment) \u003cbr /\u003e\n11 m_sourceTracker.startToken(m_input.current(), m_tokenizer); \u003cbr /\u003e\n12 \u003cbr /\u003e\n13 auto token = m_tokenizer.nextToken(m_input.current()); // 进行分词操作,取出一个token \u003cbr /\u003e\n14 if (!token) \u003cbr /\u003e\n15 return false; // 分词没有产生token,就跳出循环 \u003cbr /\u003e\n16 \u003cbr /\u003e\n17 if (!parsingFragment) \u003cbr /\u003e\n18 m_sourceTracker.endToken(m_input.current(), m_tokenizer); \u003cbr /\u003e\n19 \u003cbr /\u003e\n20 constructTreeFromHTMLToken(token); // 根据token构建DOM树 \u003cbr /\u003e\n21 } while (!isStopped()); \u003cbr /\u003e\n22 \u003cbr /\u003e\n23 return false; \u003cbr /\u003e\n24 }\u003c/p\u003e\n\u003cp\u003e上面代码中第7行会有一个yield退出操作,这是为了避免长时间处于分词循环,占用主线程。当退出条件为真时,会从分词循环中返回,返回值为true。下面是退出判断代码:\u003c/p\u003e\n\u003cp\u003e1 // 只保留关健代码 \u003cbr /\u003e\n 2 bool HTMLParserScheduler::shouldYieldBeforeToken(PumpSession\u0026amp; session) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 … \u003cbr /\u003e\n 5 \u003cbr /\u003e\n 6 // numberOfTokensBeforeCheckingForYield是静态变量,定义为4096 \u003cbr /\u003e\n 7 // session.processedTokensOnLastCheck表示从上一次退出为止,以及处理过的token个数 \u003cbr /\u003e\n 8 // session.didSeeScript表示在分词过程中是否出现过script标签 \u003cbr /\u003e\n 9 if (UNLIKELY(session.processedTokens \u0026gt; session.processedTokensOnLastCheck + numberOfTokensBeforeCheckingForYield || session.didSeeScript)) \u003cbr /\u003e\n10 return checkForYield(session); \u003cbr /\u003e\n11 \u003cbr /\u003e\n12 ++session.processedTokens; \u003cbr /\u003e\n13 return false; \u003cbr /\u003e\n14 } \u003cbr /\u003e\n15 \u003cbr /\u003e\n16 \u003cbr /\u003e\n17 bool HTMLParserScheduler::checkForYield(PumpSession\u0026amp; session) \u003cbr /\u003e\n18 { \u003cbr /\u003e\n19 session.processedTokensOnLastCheck = session.processedTokens; \u003cbr /\u003e\n20 session.didSeeScript = false; \u003cbr /\u003e\n21 \u003cbr /\u003e\n22 Seconds elapsedTime = MonotonicTime::now() - session.startTime; \u003cbr /\u003e\n23 return elapsedTime \u0026gt; m_parserTimeLimit; // m_parserTimeLimit的值默认是500ms,从分词开始超过500ms就要先yield \u003cbr /\u003e\n24 }\u003c/p\u003e\n\u003cp\u003e如果命中了上面的yield退出条件,那么什么时候再次进入分词呢?下面的代码展示了再次进入分词的过程:\u003c/p\u003e\n\u003cp\u003e1 // 保留关键代码 \u003c/p\u003e\n\u003cp\u003e2 void HTMLDocumentParser::pumpTokenizer(SynchronousMode mode) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 … \u003cbr /\u003e\n 5 \u003cbr /\u003e\n 6 if (shouldResume) // 从pumpTokenizerLoop中yield退出时返回值为true \u003cbr /\u003e\n 7 m_parserScheduler-\u0026gt;scheduleForResume(); \u003cbr /\u003e\n 8 \u003cbr /\u003e\n 9 } \u003cbr /\u003e\n10 \u003cbr /\u003e\n11 \u003cbr /\u003e\n12 \u003cbr /\u003e\n13 void HTMLParserScheduler::scheduleForResume() \u003cbr /\u003e\n14 { \u003cbr /\u003e\n15 ASSERT(!m_suspended); \u003cbr /\u003e\n16 m_continueNextChunkTimer.startOneShot(0_s); // 触发timer(0s后触发),触发后的响应函数为HTMLParserScheduler::continueNextChunkTimerFired \u003cbr /\u003e\n17 } \u003cbr /\u003e\n18 \u003cbr /\u003e\n19 \u003cbr /\u003e\n20 // 保留关健代码 \u003cbr /\u003e\n21 void HTMLParserScheduler::continueNextChunkTimerFired() \u003cbr /\u003e\n22 { \u003cbr /\u003e\n23 … \u003cbr /\u003e\n24 \u003cbr /\u003e\n25 m_parser.resumeParsingAfterYield(); // 重新Resume分词过程 \u003cbr /\u003e\n26 } \u003cbr /\u003e\n27 \u003cbr /\u003e\n28 \u003cbr /\u003e\n29 void HTMLDocumentParser::resumeParsingAfterYield() \u003cbr /\u003e\n30 { \u003cbr /\u003e\n31 // pumpTokenizer can cause this parser to be detached from the Document, \u003cbr /\u003e\n32 // but we need to ensure it isn't deleted yet. \u003cbr /\u003e\n33 Ref\u003cHTMLDocumentParser\u003e protectedThis(*this); \u003cbr /\u003e\n34 \u003cbr /\u003e\n35 // We should never be here unless we can pump immediately. \u003cbr /\u003e\n36 // Call pumpTokenizer() directly so that ASSERTS will fire if we're wrong. \u003cbr /\u003e\n37 pumpTokenizer(AllowYield); // 重新进入分词过程,该函数会调用pumpTokenizerLoop \u003cbr /\u003e\n38 endIfDelayed(); \u003cbr /\u003e\n39 }\u003c/p\u003e\n\u003cp\u003e从上面代码可以看出,再次进入分词过程是通过触发一个Timer来实现的,虽然这个Timer在0s后触发,但是并不意味着Timer的响应函数会立刻执行。如果在此之前主线程已经有其他任务到达了执行时机,会有被执行的机会。\u003c/p\u003e\n\u003cp\u003e继续看HTMLDocumentParser::pumpTokenizerLoop函数的第13行,这一行进行分词操作,从解码后的字符流中分出一个token。实现分词的代码位于HTMLTokenizer::processToken:\u003c/p\u003e\n\u003cp\u003e1 // 只保留关键代码 \u003cbr /\u003e\n 2 bool HTMLTokenizer::processToken(SegmentedString\u0026amp; source) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 \u003cbr /\u003e\n 5 … \u003cbr /\u003e\n 6 \u003cbr /\u003e\n 7 if (!m_preprocessor.peek(source, isNullCharacterSkippingState(m_state))) // 取出source内部指向的字符,赋给m_nextInputCharacter \u003cbr /\u003e\n 8 return haveBufferedCharacterToken(); \u003cbr /\u003e\n 9 UChar character = m_preprocessor.nextInputCharacter(); // 获取character \u003cbr /\u003e\n10 \u003cbr /\u003e\n11 // \u003ca rel=\"nofollow noopener noreferrer\" href=\"https://html.spec.whatwg.org/#tokenization\"\u003ehttps://html.spec.whatwg.org/#tokenization\u003c/a\u003e \u003cbr /\u003e\n12 switch (m_state) { // 进行状态转换,m_state初始值为DataState \u003cbr /\u003e\n13 … \u003cbr /\u003e\n14 } \u003cbr /\u003e\n15 \u003cbr /\u003e\n16 return false; \u003cbr /\u003e\n17 }\u003c/p\u003e\n\u003cp\u003e这个方法由于内部要做很多状态转换,总共有1200多行,后面会有4个例子来解释状态转换的逻辑。\u003c/p\u003e\n\u003cp\u003e首先来看InputStreamPreprocessor::peek方法:\u003c/p\u003e\n\u003cp\u003e1 // Returns whether we succeeded in peeking at the next character. \u003cbr /\u003e\n 2 // The only way we can fail to peek is if there are no more \u003cbr /\u003e\n 3 // characters in |source| (after collapsing \\r\\n, etc). \u003cbr /\u003e\n 4 ALWAYS_INLINE bool InputStreamPreprocessor::peek(SegmentedString\u0026amp; source, bool skipNullCharacters = false) \u003cbr /\u003e\n 5 { \u003cbr /\u003e\n 6 if (UNLIKELY(source.isEmpty())) \u003cbr /\u003e\n 7 return false; \u003cbr /\u003e\n 8 \u003cbr /\u003e\n 9 m_nextInputCharacter = source.currentCharacter(); // 获取字符流source内部指向的当前字符 \u003cbr /\u003e\n10 \u003cbr /\u003e\n11 // Every branch in this function is expensive, so we have a \u003cbr /\u003e\n12 // fast-reject branch for characters that don't require special \u003cbr /\u003e\n13 // handling. Please run the parser benchmark whenever you touch \u003cbr /\u003e\n14 // this function. It's very hot. \u003cbr /\u003e\n15 constexpr UChar specialCharacterMask = '\\n' | '\\r' | '\\0'; \u003cbr /\u003e\n16 if (LIKELY(m_nextInputCharacter \u0026amp; ~specialCharacterMask)) { \u003cbr /\u003e\n17 m_skipNextNewLine = false; \u003cbr /\u003e\n18 return true; \u003cbr /\u003e\n19 } \u003cbr /\u003e\n20 \u003cbr /\u003e\n21 return processNextInputCharacter(source, skipNullCharacters); // 跳过空字符,将\\r\\n换行符合并成\\n \u003cbr /\u003e\n22 } \u003cbr /\u003e\n23 \u003cbr /\u003e\n24 \u003cbr /\u003e\n25 bool InputStreamPreprocessor::processNextInputCharacter(SegmentedString\u0026amp; source, bool skipNullCharacters) \u003cbr /\u003e\n26 { \u003cbr /\u003e\n27 ProcessAgain: \u003cbr /\u003e\n28 ASSERT(m_nextInputCharacter == source.currentCharacter()); \u003cbr /\u003e\n29 \u003cbr /\u003e\n30 // 针对\\r\\n换行符,下面if语句处理\\r字符并且设置m_skipNextNewLine=true,后面处理\\n就直接忽略 \u003cbr /\u003e\n31 if (m_nextInputCharacter == '\\n' \u0026amp;\u0026amp; m_skipNextNewLine) { \u003cbr /\u003e\n32 m_skipNextNewLine = false; \u003cbr /\u003e\n33 source.advancePastNewline(); // 向前移动字符 \u003cbr /\u003e\n34 if (source.isEmpty()) \u003cbr /\u003e\n35 return false; \u003cbr /\u003e\n36 m_nextInputCharacter = source.currentCharacter(); \u003cbr /\u003e\n37 } \u003cbr /\u003e\n38 \u003cbr /\u003e\n39 // 如果是\\r\\n连续的换行符,那么第一次遇到\\r字符,将\\r字符替换成\\n字符,同时设置标志m_skipNextNewLine=true \u003cbr /\u003e\n40 if (m_nextInputCharacter == '\\r') { \u003cbr /\u003e\n41 m_nextInputCharacter = '\\n'; \u003cbr /\u003e\n42 m_skipNextNewLine = true; \u003cbr /\u003e\n43 return true; \u003cbr /\u003e\n44 } \u003cbr /\u003e\n45 m_skipNextNewLine = false; \u003cbr /\u003e\n46 if (m_nextInputCharacter || isAtEndOfFile(source)) \u003cbr /\u003e\n47 return true; \u003cbr /\u003e\n48 \u003cbr /\u003e\n49 // 跳过空字符 \u003cbr /\u003e\n50 if (skipNullCharacters \u0026amp;\u0026amp; !m_tokenizer.neverSkipNullCharacters()) { \u003cbr /\u003e\n51 source.advancePastNonNewline(); \u003cbr /\u003e\n52 if (source.isEmpty()) \u003cbr /\u003e\n53 return false; \u003cbr /\u003e\n54 m_nextInputCharacter = source.currentCharacter(); \u003cbr /\u003e\n55 goto ProcessAgain; // 跳转到开头 \u003cbr /\u003e\n56 } \u003cbr /\u003e\n57 m_nextInputCharacter = replacementCharacter; \u003cbr /\u003e\n58 return true; \u003cbr /\u003e\n59 }\u003c/p\u003e\n\u003cp\u003e由于peek方法会跳过空字符,同时合并\\r\\n字符为\\n字符,所以一个字符流source如果包含了空格或者\\r\\n换行符,实际上处理起来如下图所示:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/d5e0ea62-258f-4346-82b7-9e2eaf11760d.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003eHTMLTokenizer::processToken内部定义了一个状态机,下面以四种情形来进行解释。\u003c/p\u003e\n\u003cp\u003e第一种 \u003c!DCOTYPE\u003e标签\u003c/p\u003e\n\u003cp\u003e1 BEGIN_STATE(DataState) // 刚开始解析是DataState状态 \u003cbr /\u003e\n 2 if (character == '\u0026amp;') \u003cbr /\u003e\n 3 ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInDataState); \u003cbr /\u003e\n 4 if (character == '\u003c') {// 整个字符流一开始是'\u003c',那么表示是一个标签的开始 \n 5 if (haveBufferedCharacterToken()) \n 6 RETURN_IN_CURRENT_STATE(true); \n 7 ADVANCE_PAST_NON_NEWLINE_TO(TagOpenState); // 跳转到TagOpenState状态,并取去下一个字符是'!\" \n 8 } \n 9 if (character == kEndOfFileMarker) \n 10 return emitEndOfFile(source); \n 11 bufferCharacter(character); \n 12 ADVANCE_TO(DataState); \n 13 END_STATE() \n 14 \n 15 // ADVANCE_PAST_NON_NEWLINE_TO定义 \n 16 #define ADVANCE_PAST_NON_NEWLINE_TO(newState) \\ \n 17 do { \\ \n 18 if (!m_preprocessor.advancePastNonNewline(source, isNullCharacterSkippingState(newState))) { \\ // 如果往下移动取不到下一个字符 \n 19 m_state = newState; \\ // 保存状态 \n 20 return haveBufferedCharacterToken(); \\ // 返回 \n 21 } \\ \n 22 character = m_preprocessor.nextInputCharacter(); \\ // 先取出下一个字符 \n 23 goto newState; \\ // 跳转到指定状态 \n 24 } while (false) \n 25 \n 26 \n 27 BEGIN_STATE(TagOpenState) \n 28 if (character == '!') // 满足此条件 \n 29 ADVANCE_PAST_NON_NEWLINE_TO(MarkupDeclarationOpenState); // 同理,跳转到MarkupDeclarationOpenState状态,并且取出下一个字符'D' \n 30 if (character == '/') \n 31 ADVANCE_PAST_NON_NEWLINE_TO(EndTagOpenState); \n 32 if (isASCIIAlpha(character)) { \n 33 m_token.beginStartTag(convertASCIIAlphaToLower(character)); \n 34 ADVANCE_PAST_NON_NEWLINE_TO(TagNameState); \n 35 } \n 36 if (character == '?') { \n 37 parseError(); \n 38 // The spec consumes the current character before switching \n 39 // to the bogus comment state, but it's easier to implement \n 40 // if we reconsume the current character. \n 41 RECONSUME_IN(BogusCommentState); \n 42 } \n 43 parseError(); \n 44 bufferASCIICharacter('\u003c'); \n 45 RECONSUME_IN(DataState); \n 46 END_STATE() \n 47 \n 48 BEGIN_STATE(MarkupDeclarationOpenState) \n 49 if (character == '-') { \n 50 auto result = source.advancePast(\"--\"); \n 51 if (result == SegmentedString::DidMatch) { \n 52 m_token.beginComment(); \n 53 SWITCH_TO(CommentStartState); \n 54 } \n 55 if (result == SegmentedString::NotEnoughCharacters) \n 56 RETURN_IN_CURRENT_STATE(haveBufferedCharacterToken()); \n 57 } else if (isASCIIAlphaCaselessEqual(character, 'd')) { // 由于character == 'D',满足此条件 \n 58 auto result = source.advancePastLettersIgnoringASCIICase(\"doctype\"); // 看解码后的字符流中是否有完整的\"doctype\" \n 59 if (result == SegmentedString::DidMatch) \n 60 SWITCH_TO(DOCTYPEState); // 如果匹配,则跳转到DOCTYPEState,同时取出当前指向的字符,由于上面source字符流已经移动了\"doctype\",因此此时取出的字符为'\u003e' \u003cbr /\u003e\n 61 if (result == SegmentedString::NotEnoughCharacters) // 如果不匹配 \u003cbr /\u003e\n 62 RETURN_IN_CURRENT_STATE(haveBufferedCharacterToken()); // 保存状态,直接返回 \u003cbr /\u003e\n 63 } else if (character == '[' \u0026amp;\u0026amp; shouldAllowCDATA()) { \u003cbr /\u003e\n 64 auto result = source.advancePast(\"[CDATA[\"); \u003cbr /\u003e\n 65 if (result == SegmentedString::DidMatch) \u003cbr /\u003e\n 66 SWITCH_TO(CDATASectionState); \u003cbr /\u003e\n 67 if (result == SegmentedString::NotEnoughCharacters) \u003cbr /\u003e\n 68 RETURN_IN_CURRENT_STATE(haveBufferedCharacterToken()); \u003cbr /\u003e\n 69 } \u003cbr /\u003e\n 70 parseError(); \u003cbr /\u003e\n 71 RECONSUME_IN(BogusCommentState); \u003cbr /\u003e\n 72 END_STATE() \u003cbr /\u003e\n 73 \u003cbr /\u003e\n 74 \u003cbr /\u003e\n 75 #define SWITCH_TO(newState) \\ \u003cbr /\u003e\n 76 do { \\ \u003cbr /\u003e\n 77 if (!m_preprocessor.peek(source, isNullCharacterSkippingState(newState))) { \\ \u003cbr /\u003e\n 78 m_state = newState; \\ \u003cbr /\u003e\n 79 return haveBufferedCharacterToken(); \\ \u003cbr /\u003e\n 80 } \\ \u003cbr /\u003e\n 81 character = m_preprocessor.nextInputCharacter(); \\ // 取出下一个字符 \u003cbr /\u003e\n 82 goto newState; \\ // 跳转到指定的state \u003cbr /\u003e\n 83 } while (false) \u003cbr /\u003e\n 84 \u003cbr /\u003e\n 85 \u003cbr /\u003e\n 86 #define RETURN_IN_CURRENT_STATE(expression) \\ \u003cbr /\u003e\n 87 do { \\ \u003cbr /\u003e\n 88 m_state = currentState; \\ // 保存当前状态 \u003cbr /\u003e\n 89 return expression; \\ \u003cbr /\u003e\n 90 } while (false) \u003cbr /\u003e\n 91 \u003cbr /\u003e\n 92 \u003cbr /\u003e\n 93 BEGIN_STATE(DOCTYPEState) \u003cbr /\u003e\n 94 if (isTokenizerWhitespace(character)) \u003cbr /\u003e\n 95 ADVANCE_TO(BeforeDOCTYPENameState); \u003cbr /\u003e\n 96 if (character == kEndOfFileMarker) { \u003cbr /\u003e\n 97 parseError(); \u003cbr /\u003e\n 98 m_token.beginDOCTYPE(); \u003cbr /\u003e\n 99 m_token.setForceQuirks(); \u003cbr /\u003e\n100 return emitAndReconsumeInDataState(); \u003cbr /\u003e\n101 } \u003cbr /\u003e\n102 parseError(); \u003cbr /\u003e\n103 RECONSUME_IN(BeforeDOCTYPENameState); \u003cbr /\u003e\n104 END_STATE() \u003cbr /\u003e\n105 \u003cbr /\u003e\n106 \u003cbr /\u003e\n107 #define RECONSUME_IN(newState) \\ \u003cbr /\u003e\n108 do { \\ // 直接跳转到指定state \u003cbr /\u003e\n109 goto newState; \\ \u003cbr /\u003e\n110 } while (false) \u003cbr /\u003e\n111 \u003cbr /\u003e\n112 \u003cbr /\u003e\n113 BEGIN_STATE(BeforeDOCTYPENameState) \u003cbr /\u003e\n114 if (isTokenizerWhitespace(character)) \u003cbr /\u003e\n115 ADVANCE_TO(BeforeDOCTYPENameState); \u003cbr /\u003e\n116 if (character == '\u0026gt;') { // character == '\u0026gt;',匹配此处,到此DOCTYPE标签匹配完毕 \u003cbr /\u003e\n117 parseError(); \u003cbr /\u003e\n118 m_token.beginDOCTYPE(); \u003cbr /\u003e\n119 m_token.setForceQuirks(); \u003cbr /\u003e\n120 return emitAndResumeInDataState(source); \u003cbr /\u003e\n121 } \u003cbr /\u003e\n122 if (character == kEndOfFileMarker) { \u003cbr /\u003e\n123 parseError(); \u003cbr /\u003e\n124 m_token.beginDOCTYPE(); \u003cbr /\u003e\n125 m_token.setForceQuirks(); \u003cbr /\u003e\n126 return emitAndReconsumeInDataState(); \u003cbr /\u003e\n127 } \u003cbr /\u003e\n128 m_token.beginDOCTYPE(toASCIILower(character)); \u003cbr /\u003e\n129 ADVANCE_PAST_NON_NEWLINE_TO(DOCTYPENameState); \u003cbr /\u003e\n130 END_STATE() \u003cbr /\u003e\n131 \u003cbr /\u003e\n132 \u003cbr /\u003e\n133 \u003cbr /\u003e\n134 \u003cbr /\u003e\n135 inline bool HTMLTokenizer::emitAndResumeInDataState(SegmentedString\u0026amp; source) \u003cbr /\u003e\n136 { \u003cbr /\u003e\n137 saveEndTagNameIfNeeded(); \u003cbr /\u003e\n138 m_state = DataState; // 重置状态为初始状态DataState \u003cbr /\u003e\n139 source.advancePastNonNewline(); // 移动到下一个字符 \u003cbr /\u003e\n140 return true; \u003cbr /\u003e\n141 }\u003c/p\u003e\n\u003cp\u003eDOCTYPE Token经历了6个状态最终被解析出来,整个过程如下图所示:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/2ac0c8e2-e773-4655-8c00-793f4379e312.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e当Token解析完毕之后,分词状态又被重置为DataState,同时需要注意的时,此时字符流source内部指向的是下一个字符'\u0026lt;'。\u003c/p\u003e\n\u003cp\u003e上面代码第61行在用字符流source匹配字符串\"doctype\"时,可能出现匹配不上的情形。为什么会这样呢?这是因为整个DOM树的构建流程,并不是先要解码完成,解码完成之后获取到完整的字符流才进行分词。从前面解码可以知道,解码可能是一边接收字节流,一边进行解码的,因此分词也是这样,只要能解码出一段字符流,就会立即进行分词。整个流程会出现如下图所示:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/c9290d65-df82-471f-8664-8967bd54da86.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e由于这个原因,用来分词的字符流可能是不完整的。对于出现不完整情形的DOCTYPE分词过程如下图所示:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/194c46fd-ef2b-4fae-9a35-401ccf7c846a.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e上面介绍了解码、分词、解码、分词处理DOCTYPE标签的情形,可以看到从逻辑上这种情形与完整解码再分词是一样的。后续介绍的时都会只针对完整解码再分词的情形,对于一边解码一边分词的情形,只需要正确的认识source字符流内部指针的移动,并不难分析。\u003c/p\u003e\n\u003cp\u003e第二种 html标签\u003c/p\u003e\n\u003cp\u003ehtml标签的分词过程和DOCTYPE类似,其相关代码如下:\u003c/p\u003e\n\u003cp\u003e1 BEGIN_STATE(TagOpenState) \u003cbr /\u003e\n 2 if (character == '!') \u003cbr /\u003e\n 3 ADVANCE_PAST_NON_NEWLINE_TO(MarkupDeclarationOpenState); \u003cbr /\u003e\n 4 if (character == '/') \u003cbr /\u003e\n 5 ADVANCE_PAST_NON_NEWLINE_TO(EndTagOpenState); \u003cbr /\u003e\n 6 if (isASCIIAlpha(character)) { // 在开标签状态下,当前字符为'h' \u003cbr /\u003e\n 7 m_token.beginStartTag(convertASCIIAlphaToLower(character)); // 将'h'添加到Token名中 \u003cbr /\u003e\n 8 ADVANCE_PAST_NON_NEWLINE_TO(TagNameState); // 跳转到TagNameState,并移动到下一个字符't' \u003cbr /\u003e\n 9 } \u003cbr /\u003e\n10 if (character == '?') { \u003cbr /\u003e\n11 parseError(); \u003cbr /\u003e\n12 // The spec consumes the current character before switching \u003cbr /\u003e\n13 // to the bogus comment state, but it's easier to implement \u003cbr /\u003e\n14 // if we reconsume the current character. \u003cbr /\u003e\n15 RECONSUME_IN(BogusCommentState); \u003cbr /\u003e\n16 } \u003cbr /\u003e\n17 parseError(); \u003cbr /\u003e\n18 bufferASCIICharacter('\u003c'); \n19 RECONSUME_IN(DataState); \n20 END_STATE() \n21 \n22 \n23 BEGIN_STATE(TagNameState) \n24 if (isTokenizerWhitespace(character)) \n25 ADVANCE_TO(BeforeAttributeNameState); \n26 if (character == '/') \n27 ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState); \n28 if (character == '\u003e') // 在这个状态下遇到起始标签终止字符 \u003cbr /\u003e\n29 return emitAndResumeInDataState(source); // 当前分词结束,重置分词状态为DataState \u003cbr /\u003e\n30 if (m_options.usePreHTML5ParserQuirks \u0026amp;\u0026amp; character == '\u0026lt;') \u003cbr /\u003e\n31 return emitAndReconsumeInDataState(); \u003cbr /\u003e\n32 if (character == kEndOfFileMarker) { \u003cbr /\u003e\n33 parseError(); \u003cbr /\u003e\n34 RECONSUME_IN(DataState); \u003cbr /\u003e\n35 } \u003cbr /\u003e\n36 m_token.appendToName(toASCIILower(character)); // 将当前字符添加到Token名 \u003cbr /\u003e\n37 ADVANCE_PAST_NON_NEWLINE_TO(TagNameState); // 继续跳转到当前状态,并移动到下一个字符 \u003cbr /\u003e\n38 END_STATE()\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/3ec0e3da-74a2-4afc-b776-91329bd2fde6.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e第三种 带有属性的标签div\u003c/p\u003e\n\u003cp\u003eHTML标签可以带有属性,属性由属性名和属性值组成,属性之间以及属性与标签名之间用空格分隔:\u003c/p\u003e\n\u003cp\u003e1 \u003c!-- div标签有两个属性,属性名为class和align,它们的值都带有引号 --\u003e \u003cbr /\u003e\n2 \u003cdiv class=\"news\" align=\"center\"\u003eHello,World!\u003c/div\u003e \u003cbr /\u003e\n3 \u003cbr /\u003e\n4 \u003cbr /\u003e\n5 \u003c!-- 属性值也可以不带引号 --\u003e \u003cbr /\u003e\n6 \u003cdiv class=news align=center\u003eHello,World!\u003c/div\u003e\u003c/p\u003e\n\u003cp\u003e整个div标签的解析中,标签名div的解析流程和上面的html标签解析一样,当在解析标签名的过程中,碰到了空白字符,说明要开始解析属性了,下面是相关代码:\u003c/p\u003e\n\u003cp\u003e1 BEGIN_STATE(TagNameState) \u003cbr /\u003e\n 2 if (isTokenizerWhitespace(character)) // 在解析TagName时遇到空白字符,标志属性开始 \u003cbr /\u003e\n 3 ADVANCE_TO(BeforeAttributeNameState); \u003cbr /\u003e\n 4 if (character == '/') \u003cbr /\u003e\n 5 ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState); \u003cbr /\u003e\n 6 if (character == '\u0026gt;') \u003cbr /\u003e\n 7 return emitAndResumeInDataState(source); \u003cbr /\u003e\n 8 if (m_options.usePreHTML5ParserQuirks \u0026amp;\u0026amp; character == '\u003c') \n 9 return emitAndReconsumeInDataState(); \n 10 if (character == kEndOfFileMarker) { \n 11 parseError(); \n 12 RECONSUME_IN(DataState); \n 13 } \n 14 m_token.appendToName(toASCIILower(character)); \n 15 ADVANCE_PAST_NON_NEWLINE_TO(TagNameState); \n 16 END_STATE() \n 17 \n 18 #define ADVANCE_TO(newState) \\ \n 19 do { \\ \n 20 if (!m_preprocessor.advance(source, isNullCharacterSkippingState(newState))) { \\ // 移动到下一个字符 \n 21 m_state = newState; \\ \n 22 return haveBufferedCharacterToken(); \\ \n 23 } \\ \n 24 character = m_preprocessor.nextInputCharacter(); \\ \n 25 goto newState; \\ // 跳转到指定状态 \n 26 } while (false) \n 27 \n 28 \n 29 BEGIN_STATE(BeforeAttributeNameState) \n 30 if (isTokenizerWhitespace(character)) // 如果标签名后有连续空格,那么就不停的跳过,在当前状态不停循环 \n 31 ADVANCE_TO(BeforeAttributeNameState); \n 32 if (character == '/') \n 33 ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState); \n 34 if (character == '\u003e') \u003cbr /\u003e\n 35 return emitAndResumeInDataState(source); \u003cbr /\u003e\n 36 if (m_options.usePreHTML5ParserQuirks \u0026amp;\u0026amp; character == '\u003c') \n 37 return emitAndReconsumeInDataState(); \n 38 if (character == kEndOfFileMarker) { \n 39 parseError(); \n 40 RECONSUME_IN(DataState); \n 41 } \n 42 if (character == '\"' || character == '\\'' || character == '\u003c' || character == '=') \n 43 parseError(); \n 44 m_token.beginAttribute(source.numberOfCharactersConsumed()); // Token的属性列表增加一个,用来存放新的属性名与属性值 \n 45 m_token.appendToAttributeName(toASCIILower(character)); // 添加属性名 \n 46 ADVANCE_PAST_NON_NEWLINE_TO(AttributeNameState); // 跳转到AttributeNameState,并且移动到下一个字符 \n 47 END_STATE() \n 48 \n 49 \n 50 BEGIN_STATE(AttributeNameState) \n 51 if (isTokenizerWhitespace(character)) \n 52 ADVANCE_TO(AfterAttributeNameState); \n 53 if (character == '/') \n 54 ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState); \n 55 if (character == '=') \n 56 ADVANCE_PAST_NON_NEWLINE_TO(BeforeAttributeValueState); // 在解析属性名的过程中如果碰到=,说明属性名结束,属性值就要开始 \n 57 if (character == '\u003e') \u003cbr /\u003e\n 58 return emitAndResumeInDataState(source); \u003cbr /\u003e\n 59 if (m_options.usePreHTML5ParserQuirks \u0026amp;\u0026amp; character == '\u003c') \n 60 return emitAndReconsumeInDataState(); \n 61 if (character == kEndOfFileMarker) { \n 62 parseError(); \n 63 RECONSUME_IN(DataState); \n 64 } \n 65 if (character == '\"' || character == '\\'' || character == '\u003c' || character == '=') \n 66 parseError(); \n 67 m_token.appendToAttributeName(toASCIILower(character)); \n 68 ADVANCE_PAST_NON_NEWLINE_TO(AttributeNameState); \n 69 END_STATE() \n 70 \n 71 \n 72 BEGIN_STATE(BeforeAttributeValueState) \n 73 if (isTokenizerWhitespace(character)) \n 74 ADVANCE_TO(BeforeAttributeValueState); \n 75 if (character == '\"') \n 76 ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueDoubleQuotedState); // 有的属性值有引号包围,这里跳转到AttributeValueDoubleQuotedState,并移动到下一个字符 \n 77 if (character == '\u0026') \n 78 RECONSUME_IN(AttributeValueUnquotedState); \n 79 if (character == '\\'') \n 80 ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueSingleQuotedState); \n 81 if (character == '\u003e') { \u003cbr /\u003e\n 82 parseError(); \u003cbr /\u003e\n 83 return emitAndResumeInDataState(source); \u003cbr /\u003e\n 84 } \u003cbr /\u003e\n 85 if (character == kEndOfFileMarker) { \u003cbr /\u003e\n 86 parseError(); \u003cbr /\u003e\n 87 RECONSUME_IN(DataState); \u003cbr /\u003e\n 88 } \u003cbr /\u003e\n 89 if (character == '\u003c' || character == '=' || character == '`') \n 90 parseError(); \n 91 m_token.appendToAttributeValue(character); // 有的属性值没有引号包围,添加属性值字符到Token \n 92 ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueUnquotedState); // 跳转到AttributeValueUnquotedState,并移动到下一个字符 \n 93 END_STATE() \n 94 \n 95 BEGIN_STATE(AttributeValueDoubleQuotedState) \n 96 if (character == '\"') { // 在当前状态下如果遇到引号,说明属性值结束 \n 97 m_token.endAttribute(source.numberOfCharactersConsumed()); // 结束属性解析 \n 98 ADVANCE_PAST_NON_NEWLINE_TO(AfterAttributeValueQuotedState); // 跳转到AfterAttributeValueQuotedState,并移动到下一个字符 \n 99 } \n100 if (character == '\u0026') { \n101 m_additionalAllowedCharacter = '\"'; \n102 ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInAttributeValueState); \n103 } \n104 if (character == kEndOfFileMarker) { \n105 parseError(); \n106 m_token.endAttribute(source.numberOfCharactersConsumed()); \n107 RECONSUME_IN(DataState); \n108 } \n109 m_token.appendToAttributeValue(character); // 将属性值字符添加到Token \n110 ADVANCE_TO(AttributeValueDoubleQuotedState); // 跳转到当前状态 \n111 END_STATE() \n112 \n113 \n114 BEGIN_STATE(AfterAttributeValueQuotedState) \n115 if (isTokenizerWhitespace(character)) \n116 ADVANCE_TO(BeforeAttributeNameState); // 属性值解析完毕,如果后面继续跟着空白字符,说明后续还有属性要解析,调回到BeforeAttributeNameState \n117 if (character == '/') \n118 ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState); \n119 if (character == '\u003e') \u003cbr /\u003e\n120 return emitAndResumeInDataState(source); // 属性值解析完毕,如果遇到'\u0026gt;'字符,说明整个标签也要解析完毕了,此时结束当前标签解析,并且重置分词状态为DataState,并移动到下一个字符 \u003cbr /\u003e\n121 if (m_options.usePreHTML5ParserQuirks \u0026amp;\u0026amp; character == '\u003c') \n122 return emitAndReconsumeInDataState(); \n123 if (character == kEndOfFileMarker) { \n124 parseError(); \n125 RECONSUME_IN(DataState); \n126 } \n127 parseError(); \n128 RECONSUME_IN(BeforeAttributeNameState); \n129 END_STATE() \n130 \n131 BEGIN_STATE(AttributeValueUnquotedState) \n132 if (isTokenizerWhitespace(character)) { // 当解析不带引号的属性值时遇到空白字符(这与带引号的属性值不一样,带引号的属性值可以包含空白字符),说明当前属性解析完毕,后面还有其他属性,跳转到BeforeAttributeNameState,并且移动到下一个字符 \n133 m_token.endAttribute(source.numberOfCharactersConsumed()); \n134 ADVANCE_TO(BeforeAttributeNameState); \n135 } \n136 if (character == '\u0026') { \n137 m_additionalAllowedCharacter = '\u003e'; \u003cbr /\u003e\n138 ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInAttributeValueState); \u003cbr /\u003e\n139 } \u003cbr /\u003e\n140 if (character == '\u0026gt;') { // 解析过程中如果遇到'\u0026gt;'字符,说明整个标签也要解析完毕了,此时结束当前标签解析,并且重置分词状态为DataState,并移动到下一个字符 \u003cbr /\u003e\n141 m_token.endAttribute(source.numberOfCharactersConsumed()); \u003cbr /\u003e\n142 return emitAndResumeInDataState(source); \u003cbr /\u003e\n143 } \u003cbr /\u003e\n144 if (character == kEndOfFileMarker) { \u003cbr /\u003e\n145 parseError(); \u003cbr /\u003e\n146 m_token.endAttribute(source.numberOfCharactersConsumed()); \u003cbr /\u003e\n147 RECONSUME_IN(DataState); \u003cbr /\u003e\n148 } \u003cbr /\u003e\n149 if (character == '\"' || character == '\\'' || character == '\u0026lt;' || character == '=' || character == '`') \u003cbr /\u003e\n150 parseError(); \u003cbr /\u003e\n151 m_token.appendToAttributeValue(character); // 将遇到的属性值字符添加到Token \u003cbr /\u003e\n152 ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueUnquotedState); // 跳转到当前状态,并且移动到下一个字符 \u003cbr /\u003e\n153 END_STATE()\u003c/p\u003e\n\u003cp\u003e从代码中可以看到,当属性值带引号和不带引号时,解析的逻辑是不一样的。当属性值带有引号时,属性值里面是可以包含空白字符的。如果属性值不带引号,那么一旦碰到空白字符,说明这个属性就解析结束了,会进入下一个属性的解析当中。\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/4457735e-3d08-4edb-817f-dcb32763cab2.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e第四种 纯文本解析\u003c/p\u003e\n\u003cp\u003e这里的纯文本指起始标签与结束标签之间的任何纯文字,包括脚本文、CSS文本等等,如下图所示:\u003c/p\u003e\n\u003c!-- div标签中的纯文本 Hello,Word! --\u003e\n\u003cdiv class=news align=center\u003eHello,World!\u003c/div\u003e\n\u003c!-- script标签中的纯文本 window.name = 'Lucy'; --\u003e\n\u003cscript\u003ewindow.name = 'Lucy';\u003c/script\u003e\n\u003cp\u003e纯文本的解析过程比较简单,就是不停的在DataState状态上跳转,缓存遇到的字符,直到遇见一个结束标签的'\u0026lt;'字符,相关代码如下:\u003c/p\u003e\n\u003cp\u003e1 BEGIN_STATE(DataState) \u003cbr /\u003e\n 2 if (character == '\u0026amp;') \u003cbr /\u003e\n 3 ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInDataState); \u003cbr /\u003e\n 4 if (character == '\u0026lt;') { // 如果在解析文本的过程中遇到开标签,分两种情况 \u003cbr /\u003e\n 5 if (haveBufferedCharacterToken()) // 第一种,如果缓存了文本字符就直接按当前DataState返回,并不移动字符,所以下次再进入分词操作时取到的字符仍为'\u0026lt;' \u003cbr /\u003e\n 6 RETURN_IN_CURRENT_STATE(true); \u003cbr /\u003e\n 7 ADVANCE_PAST_NON_NEWLINE_TO(TagOpenState); // 第二种,如果没有缓存任何文本字符,直接进入TagOpenState状态,进入到起始标签解析过程,并且移动下一个字符 \u003cbr /\u003e\n 8 } \u003cbr /\u003e\n 9 if (character == kEndOfFileMarker) \u003cbr /\u003e\n10 return emitEndOfFile(source); \u003cbr /\u003e\n11 bufferCharacter(character); // 缓存遇到的字符 \u003cbr /\u003e\n12 ADVANCE_TO(DataState); // 循环跳转到当前DataState状态,并且移动到下一个字符 \u003cbr /\u003e\n13 END_STATE()\u003c/p\u003e\n\u003cp\u003e由于流程比较简单,下面只给出解析div标签中纯文本的结果:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/f2e8a072-6180-43fe-99d0-35eac915dcc5.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e创建节点与添加节点\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e1 相关类图\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/d4d3785f-d828-4b1b-9d1b-123f341f78d1.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e2 创建、添加流程\u003c/p\u003e\n\u003cp\u003e上面的分词循环中,每分出一个Token,就会根据Token创建对应的Node,然后将Node添加到DOM树上。(HTMLDocumentParser::pumpTokenizerLoop方法在上面分词中有介绍)。\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/42e68208-2658-4ffe-b34e-62fe96976f81.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e上面方法中首先看HTMLTreeBuilder::constructTree,代码如下:\u003c/p\u003e\n\u003cp\u003e1 // 只保留关健代码 \u003cbr /\u003e\n 2 void HTMLTreeBuilder::constructTree(AtomHTMLToken\u0026amp;\u0026amp; token) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 … \u003cbr /\u003e\n 5 \u003cbr /\u003e\n 6 if (shouldProcessTokenInForeignContent(token)) \u003cbr /\u003e\n 7 processTokenInForeignContent(WTFMove(token)); \u003cbr /\u003e\n 8 else \u003cbr /\u003e\n 9 processToken(WTFMove(token)); // HTMLToken在这里被处理 \u003cbr /\u003e\n10 \u003cbr /\u003e\n11 … \u003cbr /\u003e\n12 \u003cbr /\u003e\n13 m_tree.executeQueuedTasks(); // HTMLContructionSiteTask在这里被执行,有时候也直接在创建的过程中直接执行,然后这个方法发现队列为空就会直接返回 \u003cbr /\u003e\n14 // The tree builder might have been destroyed as an indirect result of executing the queued tasks. \u003cbr /\u003e\n15 } \u003cbr /\u003e\n16 \u003cbr /\u003e\n17 \u003cbr /\u003e\n18 void HTMLConstructionSite::executeQueuedTasks() \u003cbr /\u003e\n19 { \u003cbr /\u003e\n20 if (m_taskQueue.isEmpty()) // 队列为空,就直接返回 \u003cbr /\u003e\n21 return; \u003cbr /\u003e\n22 \u003cbr /\u003e\n23 // Copy the task queue into a local variable in case executeTask \u003cbr /\u003e\n24 // re-enters the parser. \u003cbr /\u003e\n25 TaskQueue queue = WTFMove(m_taskQueue); \u003cbr /\u003e\n26 \u003cbr /\u003e\n27 for (auto\u0026amp; task : queue) // 这里的task就是HTMLContructionSiteTask \u003cbr /\u003e\n28 executeTask(task); // 执行task \u003cbr /\u003e\n29 \u003cbr /\u003e\n30 // We might be detached now. \u003cbr /\u003e\n31 }\u003c/p\u003e\n\u003cp\u003e上面代码中HTMLTreeBuilder::processToken就是处理Token生成对应Node的地方,代码如下所示:\u003c/p\u003e\n\u003cp\u003e1 void HTMLTreeBuilder::processToken(AtomHTMLToken\u0026amp;\u0026amp; token) \u003cbr /\u003e\n 2 { \u003cbr /\u003e\n 3 switch (token.type()) { \u003cbr /\u003e\n 4 case HTMLToken::Uninitialized: \u003cbr /\u003e\n 5 ASSERT_NOT_REACHED(); \u003cbr /\u003e\n 6 break; \u003cbr /\u003e\n 7 case HTMLToken::DOCTYPE: // HTML中的DOCType标签 \u003cbr /\u003e\n 8 m_shouldSkipLeadingNewline = false; \u003cbr /\u003e\n 9 processDoctypeToken(WTFMove(token)); \u003cbr /\u003e\n10 break; \u003cbr /\u003e\n11 case HTMLToken::StartTag: // 起始HTML标签 \u003cbr /\u003e\n12 m_shouldSkipLeadingNewline = false; \u003cbr /\u003e\n13 processStartTag(WTFMove(token)); \u003cbr /\u003e\n14 break; \u003cbr /\u003e\n15 case HTMLToken::EndTag: // 结束HTML标签 \u003cbr /\u003e\n16 m_shouldSkipLeadingNewline = false; \u003cbr /\u003e\n17 processEndTag(WTFMove(token)); \u003cbr /\u003e\n18 break; \u003cbr /\u003e\n19 case HTMLToken::Comment: // HTML中的注释 \u003cbr /\u003e\n20 m_shouldSkipLeadingNewline = false; \u003cbr /\u003e\n21 processComment(WTFMove(token)); \u003cbr /\u003e\n22 return; \u003cbr /\u003e\n23 case HTMLToken::Character: // HTML中的纯文本 \u003cbr /\u003e\n24 processCharacter(WTFMove(token)); \u003cbr /\u003e\n25 break; \u003cbr /\u003e\n26 case HTMLToken::EndOfFile: // HTML结束标志 \u003cbr /\u003e\n27 m_shouldSkipLeadingNewline = false; \u003cbr /\u003e\n28 processEndOfFile(WTFMove(token)); \u003cbr /\u003e\n29 break; \u003cbr /\u003e\n30 } \u003cbr /\u003e\n31 }\u003c/p\u003e\n\u003cp\u003e可以看到上面代码对7类Token做了处理,由于处理的流程都是类似的,这里只给出3种HTML标签的创建添加过程,分别是DOCTYPE标签,html标签,title标签文本,剩下的过程都使用图表示。\u003c/p\u003e\n\u003cp\u003e2.1 DOCTYPE标签\u003c/p\u003e\n\u003cp\u003e1 // 只保留关健代码 \u003cbr /\u003e\n 2 void HTMLTreeBuilder::processDoctypeToken(AtomHTMLToken\u0026amp;\u0026amp; token) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 ASSERT(token.type() == HTMLToken::DOCTYPE); \u003cbr /\u003e\n 5 if (m_insertionMode == InsertionMode::Initial) { // m_insertionMode的初始值就是InsertionMode::Initial \u003cbr /\u003e\n 6 m_tree.insertDoctype(WTFMove(token)); // 插入DOCTYPE标签 \u003cbr /\u003e\n 7 m_insertionMode = InsertionMode::BeforeHTML; // 插入DOCTYPE标签之后,m_insertionMode设置为InsertionMode::BeforeHTML,表示下面要开是HTML标签插入 \u003cbr /\u003e\n 8 return; \u003cbr /\u003e\n 9 } \u003cbr /\u003e\n10 \u003cbr /\u003e\n11 … \u003cbr /\u003e\n12 } \u003cbr /\u003e\n13 \u003cbr /\u003e\n14 // 只保留关健代码 \u003cbr /\u003e\n15 void HTMLConstructionSite::insertDoctype(AtomHTMLToken\u0026amp;\u0026amp; token) \u003cbr /\u003e\n16 { \u003cbr /\u003e\n17 … \u003cbr /\u003e\n18 \u003cbr /\u003e\n19 // m_attachmentRoot就是Document对象,文档根节点 \u003cbr /\u003e\n20 // DocumentType::create方法创建出DOCTYPE节点 \u003cbr /\u003e\n21 // attachLater方法内部创建出HTMLContructionSiteTask \u003cbr /\u003e\n22 attachLater(m_attachmentRoot, DocumentType::create(m_document, token.name(), publicId, systemId)); \u003cbr /\u003e\n23 \u003cbr /\u003e\n24 … \u003cbr /\u003e\n25 } \u003cbr /\u003e\n26 \u003cbr /\u003e\n27 // 只保留关健代码 \u003cbr /\u003e\n28 void HTMLConstructionSite::attachLater(ContainerNode\u0026amp; parent, Ref\u003cNode\u003e\u0026amp;\u0026amp; child, bool selfClosing) \u003cbr /\u003e\n29 { \u003cbr /\u003e\n30 … \u003cbr /\u003e\n31 \u003cbr /\u003e\n32 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert); // 创建HTMLConstructionSiteTask \u003cbr /\u003e\n33 task.parent = \u0026parent; // task持有当前节点的父节点 \u003cbr /\u003e\n34 task.child = WTFMove(child); // task持有需要操作的节点 \u003cbr /\u003e\n35 task.selfClosing = selfClosing; // 是否自关闭节点 \u003cbr /\u003e\n36 \u003cbr /\u003e\n37 // Add as a sibling of the parent if we have reached the maximum depth allowed. \u003cbr /\u003e\n38 // m_openElements就是HTMLElementStack,在这里还看不到它的作用,后面会讲。这里可以看到这个stack里面加入的对象个数是有限制的,最大不超过512个。 \u003cbr /\u003e\n39 // 所以如果一个HTML标签嵌套过多的子标签,就会触发这里的操作 \u003cbr /\u003e\n40 if (m_openElements.stackDepth() \u0026gt; m_maximumDOMTreeDepth \u0026amp;\u0026amp; task.parent-\u0026gt;parentNode()) \u003cbr /\u003e\n41 task.parent = task.parent-\u0026gt;parentNode(); // 满足条件,就会将当前节点添加到爷爷节点,而不是父节点 \u003cbr /\u003e\n42 \u003cbr /\u003e\n43 ASSERT(task.parent); \u003cbr /\u003e\n44 m_taskQueue.append(WTFMove(task)); // 将task添加到Queue当中 \u003cbr /\u003e\n45 }\u003c/p\u003e\n\u003cp\u003e从代码可以看到,这里只是创建了DOCTYPE节点,还没有真正添加。真正执行添加的操作,需要执行HTMLContructionSite::executeQueuedTasks,这个方法在一开始有列出来。下面就来看下每个Task如何被执行。\u003c/p\u003e\n\u003cp\u003e1 // 方法位于HTMLContructionSite.cpp \u003cbr /\u003e\n 2 static inline void executeTask(HTMLConstructionSiteTask\u0026amp; task) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 switch (task.operation) { // HTMLConstructionSiteTask存储了自己要做的操作,构建DOM树一般都是Insert操作 \u003cbr /\u003e\n 5 case HTMLConstructionSiteTask::Insert: \u003cbr /\u003e\n 6 executeInsertTask(task); // 这里执行insert操作 \u003cbr /\u003e\n 7 return; \u003cbr /\u003e\n 8 // All the cases below this point are only used by the adoption agency. \u003cbr /\u003e\n 9 case HTMLConstructionSiteTask::InsertAlreadyParsedChild: \u003cbr /\u003e\n10 executeInsertAlreadyParsedChildTask(task); \u003cbr /\u003e\n11 return; \u003cbr /\u003e\n12 case HTMLConstructionSiteTask::Reparent: \u003cbr /\u003e\n13 executeReparentTask(task); \u003cbr /\u003e\n14 return; \u003cbr /\u003e\n15 case HTMLConstructionSiteTask::TakeAllChildrenAndReparent: \u003cbr /\u003e\n16 executeTakeAllChildrenAndReparentTask(task); \u003cbr /\u003e\n17 return; \u003cbr /\u003e\n18 } \u003cbr /\u003e\n19 ASSERT_NOT_REACHED(); \u003cbr /\u003e\n20 } \u003cbr /\u003e\n21 \u003cbr /\u003e\n22 // 只保留关健代码,方法位于HTMLContructionSite.cpp \u003cbr /\u003e\n23 static inline void executeInsertTask(HTMLConstructionSiteTask\u0026amp; task) \u003cbr /\u003e\n24 { \u003cbr /\u003e\n25 ASSERT(task.operation == HTMLConstructionSiteTask::Insert); \u003cbr /\u003e\n26 \u003cbr /\u003e\n27 insert(task); // 继续调用插入方法 \u003cbr /\u003e\n28 \u003cbr /\u003e\n29 … \u003cbr /\u003e\n30 } \u003cbr /\u003e\n31 \u003cbr /\u003e\n32 // 只保留关健代码,方法位于HTMLContructionSite.cpp \u003cbr /\u003e\n33 static inline void insert(HTMLConstructionSiteTask\u0026amp; task) \u003cbr /\u003e\n34 { \u003cbr /\u003e\n35 … \u003cbr /\u003e\n36 \u003cbr /\u003e\n37 ASSERT(!task.child-\u0026gt;parentNode()); \u003cbr /\u003e\n38 if (task.nextChild) \u003cbr /\u003e\n39 task.parent-\u0026gt;parserInsertBefore(*task.child, *task.nextChild); \u003cbr /\u003e\n40 else \u003cbr /\u003e\n41 task.parent-\u0026gt;parserAppendChild(*task.child); // 调用父节点方法继续插入 \u003cbr /\u003e\n42 } \u003cbr /\u003e\n43 \u003cbr /\u003e\n44 // 只保留关健代码 \u003cbr /\u003e\n45 void ContainerNode::parserAppendChild(Node\u0026amp; newChild) \u003cbr /\u003e\n46 { \u003cbr /\u003e\n47 … \u003cbr /\u003e\n48 \u003cbr /\u003e\n49 executeNodeInsertionWithScriptAssertion(*this, newChild, ChildChange::Source::Parser, ReplacedAllChildren::No, [\u0026amp;] { \u003cbr /\u003e\n50 if (\u0026amp;document() != \u0026amp;newChild.document()) \u003cbr /\u003e\n51 document().adoptNode(newChild); \u003cbr /\u003e\n52 \u003cbr /\u003e\n53 appendChildCommon(newChild); // 在Block回调中调用此方法继续插入 \u003cbr /\u003e\n54 \u003cbr /\u003e\n55 … \u003cbr /\u003e\n56 }); \u003cbr /\u003e\n57 } \u003cbr /\u003e\n58 \u003cbr /\u003e\n59 // 最终调用的是这个方法进行插入 \u003cbr /\u003e\n60 void ContainerNode::appendChildCommon(Node\u0026amp; child) \u003cbr /\u003e\n61 { \u003cbr /\u003e\n62 ScriptDisallowedScope::InMainThread scriptDisallowedScope; \u003cbr /\u003e\n63 \u003cbr /\u003e\n64 child.setParentNode(this); \u003cbr /\u003e\n65 \u003cbr /\u003e\n66 if (m_lastChild) { // 父节点已经插入子节点,运行在这里 \u003cbr /\u003e\n67 child.setPreviousSibling(m_lastChild); \u003cbr /\u003e\n68 m_lastChild-\u0026gt;setNextSibling(\u0026amp;child); \u003cbr /\u003e\n69 } else \u003cbr /\u003e\n70 m_firstChild = \u0026child; // 如果父节点是首次插入子节点,运行在这里 \u003cbr /\u003e\n71 \u003cbr /\u003e\n72 m_lastChild = \u0026child; // 更新m_lastChild \u003cbr /\u003e\n73 }\u003c/p\u003e\n\u003cp\u003e经过执行上面方法之后,原来只有一个根节点的DOM树变成了下面的样子:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/32e0b135-97d0-4b6b-95e0-0e0c87128b58.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e2.2 html标签\u003c/p\u003e\n\u003cp\u003e1 // processStartTag内部有很多状态处理,这里只保留关健代码 \u003cbr /\u003e\n 2 void HTMLTreeBuilder::processStartTag(AtomHTMLToken\u0026amp;\u0026amp; token) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 ASSERT(token.type() == HTMLToken::StartTag); \u003cbr /\u003e\n 5 switch (m_insertionMode) { \u003cbr /\u003e\n 6 case InsertionMode::Initial: \u003cbr /\u003e\n 7 defaultForInitial(); \u003cbr /\u003e\n 8 ASSERT(m_insertionMode == InsertionMode::BeforeHTML); \u003cbr /\u003e\n 9 FALLTHROUGH; \u003cbr /\u003e\n10 case InsertionMode::BeforeHTML: \u003cbr /\u003e\n11 if (token.name() == htmlTag) { // html标签在这里处理 \u003cbr /\u003e\n12 m_tree.insertHTMLHtmlStartTagBeforeHTML(WTFMove(token)); \u003cbr /\u003e\n13 m_insertionMode = InsertionMode::BeforeHead; // 插入完html标签,m_insertionMode = InsertionMode::BeforeHead,表明即将处理head标签 \u003cbr /\u003e\n14 return; \u003cbr /\u003e\n15 } \u003cbr /\u003e\n16 \u003cbr /\u003e\n17 … \u003cbr /\u003e\n18 } \u003cbr /\u003e\n19 } \u003cbr /\u003e\n20 \u003cbr /\u003e\n21 \u003cbr /\u003e\n22 // 只保留关健代码 \u003cbr /\u003e\n23 void HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML(AtomHTMLToken\u0026amp;\u0026amp; token) \u003cbr /\u003e\n24 { \u003cbr /\u003e\n25 auto element = HTMLHtmlElement::create(m_document); // 创建html节点 \u003cbr /\u003e\n26 setAttributes(element, token, m_parserContentPolicy); \u003cbr /\u003e\n27 attachLater(m_attachmentRoot, element.copyRef()); // 同样调用了attachLater方法,与DOCTYPE类似 \u003cbr /\u003e\n28 m_openElements.pushHTMLHtmlElement(HTMLStackItem::create(element.copyRef(), WTFMove(token))); // 注意这里,这里向HTMLElementStack中压入了正在插入的html起始标签 \u003cbr /\u003e\n29 \u003cbr /\u003e\n30 executeQueuedTasks(); // 这里在插入操作直接执行了task,外面HTMLTreeBuilder::constructTree方法调用的executeQueuedTasks方法就会直接返回 \u003cbr /\u003e\n31 \u003cbr /\u003e\n32 … \u003cbr /\u003e\n33 }\u003c/p\u003e\n\u003cp\u003e执行上面代码之后,DOM树变成了如下图所示:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/ecca9e71-d6ca-4ffe-8afc-5bd423e5a269.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e当要插入title起始标签之后,DOM树以及HTMLElementStack m_openElements如下图所示:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/8b279e15-37af-4b5f-9a2c-e179a3597118.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e3.3 title标签文本,\u003c/p\u003e\n\u003cp\u003etitle标签的文本作为文本节点插入,生成文本节点的代码如下:\u003c/p\u003e\n\u003cp\u003e1 // 只保留关健代码 \u003cbr /\u003e\n 2 void HTMLConstructionSite::insertTextNode(const String\u0026amp; characters, WhitespaceMode whitespaceMode) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert); \u003cbr /\u003e\n 5 task.parent = \u0026amp;currentNode(); // 直接取HTMLElementStack m_openElements的栈顶节点,此时节点是title \u003cbr /\u003e\n 6 \u003cbr /\u003e\n 7 … \u003cbr /\u003e\n 8 \u003cbr /\u003e\n 9 unsigned currentPosition = 0; \u003cbr /\u003e\n10 unsigned lengthLimit = shouldUseLengthLimit(*task.parent) ? Text::defaultLengthLimit : std::numeric_limits\u003cunsigned\u003e::max(); // 限制文本节点最大包含的字符个数为65536 \u003cbr /\u003e\n11 \u003cbr /\u003e\n12 … \u003cbr /\u003e\n13 \u003cbr /\u003e\n14 \u003cbr /\u003e\n15 // 可以看到如果文本过长,会将分割成多个文本节点 \u003cbr /\u003e\n16 while (currentPosition \u003c characters.length()) { \n17 AtomString charactersAtom = m_whitespaceCache.lookup(characters, whitespaceMode); \n18 auto textNode = Text::createWithLengthLimit(task.parent-\u003edocument(), charactersAtom.isNull() ? characters : charactersAtom.string(), currentPosition, lengthLimit); \u003cbr /\u003e\n19 // If we have a whole string of unbreakable characters the above could lead to an infinite loop. Exceeding the length limit is the lesser evil. \u003cbr /\u003e\n20 if (!textNode-\u0026gt;length()) { \u003cbr /\u003e\n21 String substring = characters.substring(currentPosition); \u003cbr /\u003e\n22 AtomString substringAtom = m_whitespaceCache.lookup(substring, whitespaceMode); \u003cbr /\u003e\n23 textNode = Text::create(task.parent-\u0026gt;document(), substringAtom.isNull() ? substring : substringAtom.string()); // 生成文本节点 \u003cbr /\u003e\n24 } \u003cbr /\u003e\n25 \u003cbr /\u003e\n26 currentPosition += textNode-\u0026gt;length(); // 下一个文本节点包含的字符起点 \u003cbr /\u003e\n27 ASSERT(currentPosition \u0026lt;= characters.length()); \u003cbr /\u003e\n28 task.child = WTFMove(textNode); \u003cbr /\u003e\n29 \u003cbr /\u003e\n30 executeTask(task); // 直接执行Task插入 \u003cbr /\u003e\n31 } \u003cbr /\u003e\n32 }\u003c/p\u003e\n\u003cp\u003e从代码可以看到,如果一个节点后面跟的文本字符过多,会被分割成多个文本节点插入。下面的例子将title节点后面的文本字符个数设置成85248,使用Safari查看确实生成了2个文本节点:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/44aa9914-8f59-4383-b0e3-a899a700c37c.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e当遇到title结束标签,代码处理如下:\u003c/p\u003e\n\u003cp\u003e1 // 代码内部有很多状态处理,这里只保留关健代码 \u003cbr /\u003e\n 2 void HTMLTreeBuilder::processEndTag(AtomHTMLToken\u0026amp;\u0026amp; token) \u003cbr /\u003e\n 3 { \u003cbr /\u003e\n 4 ASSERT(token.type() == HTMLToken::EndTag); \u003cbr /\u003e\n 5 switch (m_insertionMode) { \u003cbr /\u003e\n 6 … \u003cbr /\u003e\n 7 \u003cbr /\u003e\n 8 case InsertionMode::Text: // 由于遇到title结束标签之前插入了文本,因此此时的插入模式就是InsertionMode::Text \u003cbr /\u003e\n 9 \u003cbr /\u003e\n10 m_tree.openElements().pop(); // 因为遇到了title结束标签,整个标签已经处理完毕,从HTMLElementStack栈中弹出栈顶元素title \u003cbr /\u003e\n11 m_insertionMode = m_originalInsertionMode; // 恢复之前的插入模式 \u003cbr /\u003e\n12 break; \u003cbr /\u003e\n13 \u003cbr /\u003e\n14 … \u003cbr /\u003e\n15 }\u003c/p\u003e\n\u003cp\u003e每当遇到一个标签的结束标签,都会像上面一样将HTMLElementStack m_openElementsStack的栈顶元素弹出。执行上面代码之后,DOM树与HTMLElementStack如下图所示:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/5d0fac63-8fd8-49d3-b31c-bf7204ef5c14.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e当整个DOM树构建完成之后,DOM树和HTMLElementStack m_openElements如下图所示:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/916a4db2-71f0-4a70-ab59-2f9974a1ac7b.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e从上图可以看到,当构建完DOM,HTMLElementStack m_openElements并没有将栈完全清空,而是保留了2个节点:html节点与body节点。这可以从Xcode的控制台输出看到:\u003c/p\u003e\n\u003cp\u003e\u003cimg class='lazyload' data-src=\"https://article.cdnof.com/2202/92d017fb-3821-478d-9960-f389f97620e1.jpg\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e同时可以看到,内存中的DOM树结构和文章开头画的逻辑上的DOM树结构是不一样的。逻辑上的DOM树父节点有多少子节点,就有多少指向子节点的指针,而内存中的DOM树,不管父节点有多少子节点,始终只有2个指针指向子节点:m_firstChild与m_lastChild。同时,内存中的DOM树兄弟节点之间也相互有指针引用,而逻辑上的DOM树结构是没有的。通过这样的数据结构,使得内存中的DOM结构所占用的空间大大减少,同时也能达到遍历整棵树的效果。试想一下,如果一个父节点有100个子节点,那么使用逻辑上的DOM树结构,父节点就需要100个指向子节点的指针,如果一个指针占用8字节,那么总共就要占用800字节。但是使用上面内存中DOM的表示方式,父节点只需要2个指针就可以了,总共占用16字节,内存消耗大大减少。虽然两者实现方式不一样,但是两者是等价的,都可以正确的表示HTML文档。\u003c/p\u003e"},"seo":{"title":"WebKit Inside: DOM树的构建","description":"当客户端App主进程创建WKWebView对象时,会创建另外两个子进程:渲染进程与网络进程。主进程WKWebView发起请求时,先将请求转发给渲染进程,渲染进程再转发给网络进程,网络进程请求服务器。如果请求的是一个网页,网络进程会将服务器的响应数据HTML文件字符流吐给渲染进程。渲染进程拿到HTML文件字符流,首先要进行解析,将HTML文件字符流转换成DOM树,然后在DOM树的基础上,进行渲染操作","image":"https://article.cdnof.com/2202/1d0c98f2-68a9-4ec7-ba28-73e5d2df5640.jpg","url":"https://v2as.com/article/9bc05d0e-4e32-4fff-9f3d-c5a652f463bd","keywords":["character","标签","token","字符","HTML","return","DOM","节点","source","TO"]},"viewsCount":1,"promotionList":[{"title":"阿里云服务器","image":"https://article.cdnof.com/promotion/aliyun.jpg","link":"https://qd.rs/aliyun"},{"title":"腾讯云服务器","image":"https://article.cdnof.com/promotion/tencent.jpg","link":"https://qd.rs/tencent"},{"title":"七牛云服务器","image":"https://article.cdnof.com/promotion/qiniu.png","link":"https://qd.rs/qiniu"}],"similarKeywordsList":null},"__N_SSG":true},"page":"/article/[article_id]","query":{"article_id":"9bc05d0e-4e32-4fff-9f3d-c5a652f463bd"},"buildId":"7EtL49Y65E8zx1NwcIC_o","isFallback":false,"gsp":true,"scriptLoader":[]}</script></body></html>