Vue解析器

解析器本质上是一个状态机。但我们也曾提到,正则表达式其实也是一个状态机。因此在编写 parser 的时候,利用正则表达式能够让我们少写不少代码。本章我们将更多地利用正则表达式来实现 HTML 解析器。另外,一个完善的 HTML 解析器远比想象的要复杂。我们知道,浏览器会对 HTML 文本进行解析,那么它是如何做的呢?其实关于 HTML 文本的解析,是有规范可循的,即 WHATWG 关于 HTML 的解析规范,其中定义了完整的错误处理和状态机的状态迁移流程,还提及了一些特殊的状态,例如 DATA、CDATA、RCDATA、RAWTEXT 等。那么,这些状态有什么含义呢?它们对解析器有哪些影响呢?什么是 HTML 实体,以及 Vue.js 模板解析器需要如何处理HTML 实体呢?

1、文本模式及其对解析器的影响

文本模式指的是解析器在工作时所进入的一些特殊状态,在不同的特殊状态下,解析器对文本的解析行为会有所不同。具体来说,当解析器遇到一些特殊标签时,会切换模式,从而影响其对文本的解析行为。这些特殊标签是:

  • </code> 标签、<code><textarea></code> 标签,当解析器遇到这两个标签时,会切换到 RCDATA 模式;</li> <li><code><style>、<xmp>、<iframe>、<noembed>、<noframes>、<noscript></code> 等标签,当解析器遇到这些标签时,会切换到 RAWTEXT 模式;</li> <li>当解析器遇到 <![CDATA[ 字符串时,会进入 CDATA 模式。</li> </ul> <p>解析器的初始模式则是 DATA 模式。对于 Vue.js 的模板 DSL 来说,模板中不允许出现 <code><script></code> 标签,因此 Vue.js 模板解析器在遇到 <code><script></code> 标签时也会切换到 RAWTEXT 模式。</p> <p>解析器的行为会因工作模式的不同而不同。下图出了初始模式下解析器的工作流程:<br> <a href="http://img.e-com-net.com/image/info8/245fc9a943c641d28ae281c0bfe4e85b.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/245fc9a943c641d28ae281c0bfe4e85b.jpg" alt="Vue解析器_第1张图片" width="650" height="300" style="border:1px solid black;"></a><br> 我们对上图做一些必要的解释。在默认的 DATA 模式下,解析器在遇到字符 < 时,会切换到标签开始状态(tag open state)。换句话说,在该模式下,解析器能够解析标签元素。当解析器遇到字符 & 时,会切换到字符引用状态(character reference state),也称 HTML 字符实体状态。也就是说,在DATA 模式下,解析器能够处理 HTML 字符实体。</p> <p>我们再来看看当解析器处于 RCDATA 状态时,它的工作情况如何。下图给出了 WHATWG 规范第 13.2.5.2 节的内容:<br> <a href="http://img.e-com-net.com/image/info8/5b68419381874bcc9e02dafd82ed287d.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/5b68419381874bcc9e02dafd82ed287d.jpg" alt="Vue解析器_第2张图片" width="650" height="278" style="border:1px solid black;"></a><br> 由上图可知,当解析器遇到字符 < 时,不会再切换到标签开始状态,而会切换到 RCDATA less-than sign state 状态。下图给出了 RCDATA less-than sign state 状态下解析器的工作方式:<br> <a href="http://img.e-com-net.com/image/info8/7d5cedd459284b8697ea2b97d410f594.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/7d5cedd459284b8697ea2b97d410f594.jpg" alt="Vue解析器_第3张图片" width="650" height="194" style="border:1px solid black;"></a><br> 由下图可知,在 RCDATA less-than sign state 状态下,如果解析器遇到字符 /,则直接切换到 RCDATA 的结束标签状态,即 RCDATA end tag open state;否则会将当前字符 < 作为普通字符处理,然后继续处理后面的字符。由此可知,在RCDATA 状态下,解析器不能识别标签元素。这其实间接说明了在 <code><textarea></code> 内可以将字符 < 作为普通文本,解析器并不会认为字符 < 是标签开始的标志,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>textarea<span class="token operator">></span> <span class="token number">02</span> <span class="token operator"><</span>div<span class="token operator">></span>asdf<span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span>asdfasdf <span class="token number">03</span> <span class="token operator"><</span><span class="token operator">/</span>textarea<span class="token operator">></span> </code></pre> <p>在上面这段 HTML 代码中,<code><textarea></code> 标签内存在一个<code><div></code> 标签。但解析器并不会把 <code><div></code> 解析为标签元素,而是作为普通文本处理。但是,由上上图可知,在 RCDATA 模式下,解析器仍然支持 HTML 实体。因为当解析器遇到字符 &时,会切换到字符引用状态,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>textarea<span class="token operator">></span><span class="token operator">&</span>copy<span class="token punctuation">;</span><span class="token operator"><</span><span class="token operator">/</span>textarea<span class="token operator">></span> </code></pre> <p>浏览器在渲染这段 HTML 代码时,会在文本框内展示字符 ©。</p> <p>解析器在 RAWTEXT 模式下的工作方式与在 RCDATA 模式下类似。唯一不同的是,在 RAWTEXT 模式下,解析器将不再支持HTML 实体。下图给出了 WHATWG 规范第 13.2.5.3 节中所定义的 RAWTEXT 模式下状态机的工作方式:<br> <a href="http://img.e-com-net.com/image/info8/b9b577dd507842f5b61dfc7c00028f5d.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/b9b577dd507842f5b61dfc7c00028f5d.jpg" alt="Vue解析器_第4张图片" width="650" height="244" style="border:1px solid black;"></a><br> RAWTEXT 模式的确不支持HTML 实体。在该模式下,解析器会将 HTML 实体字符作为普通字符处理。Vue.js 的单文件组件的解析器在遇到 <code><script></code> 标签时就会进入 RAWTEXT 模式,这时它会把 <code><script></code> 标签内的内容全部作为普通文本处理。</p> <p>CDATA 模式在 RAWTEXT 模式的基础上更进一步。下图给出了 WHATWG 规范第 13.2.5.69 节中所定义的 CDATA 模式下状态机的工作方式:<br> <a href="http://img.e-com-net.com/image/info8/3dca17379d154a0c92a9a8eb0c628198.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/3dca17379d154a0c92a9a8eb0c628198.jpg" alt="Vue解析器_第5张图片" width="650" height="307" style="border:1px solid black;"></a><br> 在 CDATA 模式下,解析器将把任何字符都作为普通字符处理,直到遇到 CDATA 的结束标志为止。</p> <p>实际上,在 WHATWG 规范中还定义了 PLAINTEXT 模式,该模式与 RAWTEXT 模式类似。不同的是,解析器一旦进入PLAINTEXT 模式,将不会再退出。另外,Vue.js 的模板 DSL 解析器是用不到 PLAINTEXT 模式的,因此我们不会过多介绍它。</p> <p>下表汇总了不同的模式及各其特性:<br> <a href="http://img.e-com-net.com/image/info8/b193078e90a440bc9013c98fd9f20a26.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/b193078e90a440bc9013c98fd9f20a26.jpg" alt="Vue解析器_第6张图片" width="650" height="207" style="border:1px solid black;"></a><br> 除了上表列出的特性之外,不同的模式还会影响解析器对于终止解析的判断,后文会具体讨论。另外,后续编写解析器代码时,我们会将上述模式定义为状态表,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> TextModes <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token constant">DATA</span><span class="token operator">:</span> <span class="token string">'DATA'</span><span class="token punctuation">,</span> <span class="token number">03</span> <span class="token constant">RCDATA</span><span class="token operator">:</span> <span class="token string">'RCDATA'</span><span class="token punctuation">,</span> <span class="token number">04</span> <span class="token constant">RAWTEXT</span><span class="token operator">:</span> <span class="token string">'RAWTEXT'</span><span class="token punctuation">,</span> <span class="token number">05</span> <span class="token constant">CDATA</span><span class="token operator">:</span> <span class="token string">'CDATA'</span> <span class="token number">06</span> <span class="token punctuation">}</span> </code></pre> <h2>2、递归下降算法构造模板 AST</h2> <p>从本节开始,我们将着手实现一个更加完善的模板解析器。解析器的基本架构模型如下:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token comment">// 定义文本模式,作为一个状态表</span> <span class="token number">02</span> <span class="token keyword">const</span> TextModes <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token number">03</span> <span class="token constant">DATA</span><span class="token operator">:</span> <span class="token string">'DATA'</span><span class="token punctuation">,</span> <span class="token number">04</span> <span class="token constant">RCDATA</span><span class="token operator">:</span> <span class="token string">'RCDATA'</span><span class="token punctuation">,</span> <span class="token number">05</span> <span class="token constant">RAWTEXT</span><span class="token operator">:</span> <span class="token string">'RAWTEXT'</span><span class="token punctuation">,</span> <span class="token number">06</span> <span class="token constant">CDATA</span><span class="token operator">:</span> <span class="token string">'CDATA'</span> <span class="token number">07</span> <span class="token punctuation">}</span> <span class="token number">08</span> <span class="token number">09</span> <span class="token comment">// 解析器函数,接收模板作为参数</span> <span class="token number">10</span> <span class="token keyword">function</span> <span class="token function">parse</span><span class="token punctuation">(</span><span class="token parameter">str</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">11</span> <span class="token comment">// 定义上下文对象</span> <span class="token number">12</span> <span class="token keyword">const</span> context <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token number">13</span> <span class="token comment">// source 是模板内容,用于在解析过程中进行消费</span> <span class="token number">14</span> <span class="token literal-property property">source</span><span class="token operator">:</span> str<span class="token punctuation">,</span> <span class="token number">15</span> <span class="token comment">// 解析器当前处于文本模式,初始模式为 DATA</span> <span class="token number">16</span> <span class="token literal-property property">mode</span><span class="token operator">:</span> TextModes<span class="token punctuation">.</span><span class="token constant">DATA</span> <span class="token number">17</span> <span class="token punctuation">}</span> <span class="token number">18</span> <span class="token comment">// 调用 parseChildren 函数开始进行解析,它返回解析后得到的子节点</span> <span class="token number">19</span> <span class="token comment">// parseChildren 函数接收两个参数:</span> <span class="token number">20</span> <span class="token comment">// 第一个参数是上下文对象 context</span> <span class="token number">21</span> <span class="token comment">// 第二个参数是由父代节点构成的节点栈,初始时栈为空</span> <span class="token number">22</span> <span class="token keyword">const</span> nodes <span class="token operator">=</span> <span class="token function">parseChildren</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token number">23</span> <span class="token number">24</span> <span class="token comment">// 解析器返回 Root 根节点</span> <span class="token number">25</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> <span class="token number">26</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Root'</span><span class="token punctuation">,</span> <span class="token number">27</span> <span class="token comment">// 使用 nodes 作为根节点的 children</span> <span class="token number">28</span> <span class="token literal-property property">children</span><span class="token operator">:</span> nodes <span class="token number">29</span> <span class="token punctuation">}</span> <span class="token number">30</span> <span class="token punctuation">}</span> </code></pre> <p>在上面这段代码中,我们首先定义了一个状态表 TextModes,它用来描述预定义的文本模式。然后,我们定义了 parse 函数,即解析器函数,在其中定义了上下文对象 context,用来维护解析程序执行过程中程序的各种状态。接着,调用parseChildren 函数进行解析,该函数会返回解析后得到的子节点,并使用这些子节点作为 children 来创建 Root 根节点。最后,parse 函数返回根节点,完成模板 AST 的构建。</p> <p>在上面这段代码中,parseChildren 函数是整个解析器的核心。后续我们会递归地调用它来不断地消费模板内容。parseChildren 函数会返回解析后得到的子节点。举个例子,假设有如下模板:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>p<span class="token operator">></span><span class="token number">1</span><span class="token operator"><</span><span class="token operator">/</span>p<span class="token operator">></span> <span class="token number">02</span> <span class="token operator"><</span>p<span class="token operator">></span><span class="token number">2</span><span class="token operator"><</span><span class="token operator">/</span>p<span class="token operator">></span> </code></pre> <p>上面这段模板有两个根节点,即两个 <code><p></code> 标签。parseChildren 函数在解析这段模板后,会得到由这两个 <code><p></code>节点组成的数组:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token punctuation">[</span> <span class="token number">02</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Element'</span><span class="token punctuation">,</span> <span class="token literal-property property">tag</span><span class="token operator">:</span> <span class="token string">'p'</span><span class="token punctuation">,</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token comment">/*...*/</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">03</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Element'</span><span class="token punctuation">,</span> <span class="token literal-property property">tag</span><span class="token operator">:</span> <span class="token string">'p'</span><span class="token punctuation">,</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token comment">/*...*/</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">04</span> <span class="token punctuation">]</span> </code></pre> <p>之后,这个数组将作为 Root 根节点的 children。</p> <p>parseChildren 函数接收两个参数:</p> <ul> <li>第一个参数:上下文对象 context。</li> <li>第二个参数:由父代节点构成的栈,用于维护节点间的父子级关系。</li> </ul> <p>parseChildren 函数本质上也是一个状态机,该状态机有多少种状态取决于子节点的类型数量。在模板中,元素的子节点可以是以下几种:</p> <ul> <li>标签节点,例如 <code><div></code>。</li> <li>文本插值节点,例如 <code>{{ val }}</code>。</li> <li>普通文本节点,例如:text。</li> <li>注释节点,例如 <code><!----></code>。</li> <li>CDATA 节点,例如 <code><![CDATA[ xxx ]]></code>。</li> </ul> <p>在标准的 HTML 中,节点的类型将会更多,例如 DOCTYPE 节点等。为了降低复杂度,我们仅考虑上述类型的节点。</p> <p>上图给出了 parseChildren 函数在解析模板过程中的状态迁移过程:<br> <a href="http://img.e-com-net.com/image/info8/0ff720dc8dbb464581f977467fb061aa.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/0ff720dc8dbb464581f977467fb061aa.jpg" alt="Vue解析器_第7张图片" width="650" height="499" style="border:1px solid black;"></a><br> 我们可以把上图所展示的状态迁移过程总结如下:</p> <ul> <li>当遇到字符 < 时,进入临时状态。</li> <li>如果下一个字符匹配正则 /a-z/i,则认为这是一个标签节点,于是调用 parseElement 函数完成标签的解析。注意正则表达式 /a-z/i 中的 i,意思是忽略大小写(case-insensitive)。</li> <li>如果字符串以 <!-- 开头,则认为这是一个注释节点,于是调用 parseComment 函数完成注释节点的解析。</li> <li>如果字符串以 <![CDATA[ 开头,则认为这是一个 CDATA 节点,于是调用 parseCDATA 函数完成 CDATA 节点的解析。</li> <li>如果字符串以 {{ 开头,则认为这是一个插值节点,于是调用parseInterpolation 函数完成插值节点的解析。</li> <li>其他情况,都作为普通文本,调用 parseText 函数完成文本节点的解析。</li> </ul> <p>落实到代码时,我们还需要结合文本模式,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseChildren</span><span class="token punctuation">(</span><span class="token parameter">context<span class="token punctuation">,</span> ancestors</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 定义 nodes 数组存储子节点,它将作为最终的返回值</span> <span class="token number">03</span> <span class="token keyword">let</span> nodes <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token number">04</span> <span class="token comment">// 从上下文对象中取得当前状态,包括模式 mode 和模板内容 source</span> <span class="token number">05</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> mode<span class="token punctuation">,</span> source <span class="token punctuation">}</span> <span class="token operator">=</span> context <span class="token number">06</span> <span class="token number">07</span> <span class="token comment">// 开启 while 循环,只要满足条件就会一直对字符串进行解析</span> <span class="token number">08</span> <span class="token comment">// 关于 isEnd() 后文会详细讲解</span> <span class="token number">09</span> <span class="token keyword">while</span><span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">isEnd</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> ancestors<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">10</span> <span class="token keyword">let</span> node <span class="token number">11</span> <span class="token comment">// 只有 DATA 模式和 RCDATA 模式才支持插值节点的解析</span> <span class="token number">12</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>mode <span class="token operator">===</span> TextModes<span class="token punctuation">.</span><span class="token constant">DATA</span> <span class="token operator">||</span> mode <span class="token operator">===</span> TextModes<span class="token punctuation">.</span><span class="token constant">RCDATA</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">13</span> <span class="token comment">// 只有 DATA 模式才支持标签节点的解析</span> <span class="token number">14</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>mode <span class="token operator">===</span> TextModes<span class="token punctuation">.</span><span class="token constant">DATA</span> <span class="token operator">&&</span> source<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">'<'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">15</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>source<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">'!'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">16</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'<!--'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">17</span> <span class="token comment">// 注释</span> <span class="token number">18</span> node <span class="token operator">=</span> <span class="token function">parseComment</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span> <span class="token number">19</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'<![CDATA['</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">20</span> <span class="token comment">// CDATA</span> <span class="token number">21</span> node <span class="token operator">=</span> <span class="token function">parseCDATA</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> ancestors<span class="token punctuation">)</span> <span class="token number">22</span> <span class="token punctuation">}</span> <span class="token number">23</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>source<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">'/'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">24</span> <span class="token comment">// 结束标签,这里需要抛出错误,后文会详细解释原因</span> <span class="token number">25</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">[a-z]</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">.</span><span class="token function">test</span><span class="token punctuation">(</span>source<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">26</span> <span class="token comment">// 标签</span> <span class="token number">27</span> node <span class="token operator">=</span> <span class="token function">parseElement</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> ancestors<span class="token punctuation">)</span> <span class="token number">28</span> <span class="token punctuation">}</span> <span class="token number">29</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'{{'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">30</span> <span class="token comment">// 解析插值</span> <span class="token number">31</span> node <span class="token operator">=</span> <span class="token function">parseInterpolation</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span> <span class="token number">32</span> <span class="token punctuation">}</span> <span class="token number">33</span> <span class="token punctuation">}</span> <span class="token number">34</span> <span class="token number">35</span> <span class="token comment">// node 不存在,说明处于其他模式,即非 DATA 模式且非 RCDATA 模式</span> <span class="token number">36</span> <span class="token comment">// 这时一切内容都作为文本处理</span> <span class="token number">37</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>node<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">38</span> <span class="token comment">// 解析文本节点</span> <span class="token number">39</span> node <span class="token operator">=</span> <span class="token function">parseText</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span> <span class="token number">40</span> <span class="token punctuation">}</span> <span class="token number">41</span> <span class="token number">42</span> <span class="token comment">// 将节点添加到 nodes 数组中</span> <span class="token number">43</span> nodes<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>node<span class="token punctuation">)</span> <span class="token number">44</span> <span class="token punctuation">}</span> <span class="token number">45</span> <span class="token number">46</span> <span class="token comment">// 当 while 循环停止后,说明子节点解析完毕,返回子节点</span> <span class="token number">47</span> <span class="token keyword">return</span> nodes <span class="token number">48</span> <span class="token punctuation">}</span> </code></pre> <p>上面这段代码完整地描述了上图所示的状态迁移过程,这里有几点需要注意:</p> <ul> <li>parseChildren 函数的返回值是由子节点组成的数组,每次while 循环都会解析一个或多个节点,这些节点会被添加到nodes 数组中,并作为 parseChildren 函数的返回值返回。</li> <li>解析过程中需要判断当前的文本模式。根据上表可知,只有处于 DATA 模式或 RCDATA 模式时,解析器才支持插值节点的解析。并且,只有处于 DATA 模式时,解析器才支持标签节点、注释节点和 CDATA 节点的解析。</li> <li>当遇到特定标签时,解析器会切换模式。一旦解析器切换到 DATA 模式和 RCDATA 模式之外的模式时,一切字符都将作为文本节点被解析。当然,即使在 DATA 模式或 RCDATA 模式下,如果无法匹配标签节点、注释节点、CDATA 节点、插值节点,那么也会作为文本节点解析。</li> </ul> <p>除了上述三点内容外,你可能对这段代码仍然有疑问,其中之一是 while 循环何时停止?以及 isEnd() 函数的用途是什么?这里我们给出简单的解释,parseChildren 函数是用来解析子节点的,因此 while 循环一定要遇到父级节点的结束标签才会停止,这是正常的思路。但这个思路存在一些问题,不过我们这里暂时将其忽略,后文会详细讨论。</p> <p>我们可以通过一个例子来更加直观地了解 parseChildren 函数,以及其他解析函数在解析模板时的工作职责和工作流程。以下面的模板为例:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><div> 02 <p>Text1</p> 03 <p>Text2</p> 04 </div></span><span class="token template-punctuation string">`</span></span> </code></pre> <p>这里需要强调的是,在解析模板时,我们不能忽略空白字符。这些空白字符包括:换行符(<code>\n</code>)、回车符(<code>\r</code>)、空格(<code>''</code>)、制表符(<code>\t</code>)以及换页符(<code>\f</code>)。如果我们用加号(<code>+</code>)代表换行符,用减号(<code>-</code>)代表空格字符。那么上面的模板可以表示为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><div>+--<p>Text1</p>+--<p>Text2</p>+</div></span><span class="token template-punctuation string">`</span></span> </code></pre> <p>接下来,我们以这段模板作为输入来执行解析过程。</p> <p>解析器一开始处于 DATA 模式。开始执行解析后,解析器遇到的第一个字符为 <,并且第二个字符能够匹配正则表达式 /a-z/i,所以解析器会进入标签节点状态,并调用 parseElement 函数进行解析。</p> <p>parseElement 函数会做三件事:解析开始标签,解析子节点,解析结束标签。可以用下面的伪代码来表达 parseElement 函数所做的事情:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseElement</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 解析开始标签</span> <span class="token number">03</span> <span class="token keyword">const</span> element <span class="token operator">=</span> <span class="token function">parseTag</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token number">04</span> <span class="token comment">// 这里递归地调用 parseChildren 函数进行 <div> 标签子节点的解析</span> <span class="token number">05</span> element<span class="token punctuation">.</span>children <span class="token operator">=</span> <span class="token function">parseChildren</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token number">06</span> <span class="token comment">// 解析结束标签</span> <span class="token number">07</span> <span class="token function">parseEndTag</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token number">08</span> <span class="token number">09</span> <span class="token keyword">return</span> element <span class="token number">10</span> <span class="token punctuation">}</span> </code></pre> <p>如果一个标签不是自闭合标签,则可以认为,一个完整的标签元素是由开始标签、子节点和结束标签这三部分构成的。因此,在 parseElement 函数内,我们分别调用三个解析函数来处理这三部分内容。以上述模板为例。</p> <p>parseTag 解析开始标签。parseTag 函数用于解析开始标签,包括开始标签上的属性和指令。因此,在 parseTag 解析函数执行完毕后,会消费字符串中的内容 <code><div></code>,处理后的模板内容将变为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">+--<p>Text1</p>+--<p>Text2</p>+</div></span><span class="token template-punctuation string">`</span></span> </code></pre> <p>递归地调用 parseChildren 函数解析子节点。parseElement 函数在解析开始标签时,会产生一个标签节点 element。在parseElement 函数执行完毕后,剩下的模板内容应该作为element 的子节点被解析,即 element.children。因此,我们要递归地调用 parseChildren 函数。在这个过程中,parseChildren 函数会消费字符串的内容:<code>+--<p>Text1</p>+--<p>Text2</p>+</code>。处理后的模板内容将变为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"></div></span><span class="token template-punctuation string">`</span></span> </code></pre> <p>parseEndTag 处理结束标签。可以看到,在经过parseChildren 函数处理后,模板内容只剩下一个结束标签了。因此,只需要调用 parseEndTag 解析函数来消费它即可。</p> <p>经过上述三个步骤的处理后,这段模板就被解析完毕了,最终得到了模板 AST。但这里值得注意的是,为了解析标签的子节点,我们递归地调用了 parseChildren 函数。这意味着,一个新的状态机开始运行了,我们称其为“状态机 2”。“状态机2”所处理的模板内容为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">+--<p>Text1</p>+--<p>Text2</p>+</span><span class="token template-punctuation string">`</span></span> </code></pre> <p>接下来,我们继续分析“状态机 2”的状态迁移流程。在“状态机 2”开始运行时,模板的第一个字符是换行符(字符 + 代表换行符)。因此,解析器会进入文本节点状态,并调用parseText 函数完成文本节点的解析。parseText 函数会将下一个 < 字符之前的所有字符都视作文本节点的内容。换句话说,parseText 函数会消费模板内容 ±-,并产生一个文本节点。在parseText 解析函数执行完毕后,剩下的模板内容为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><p>Text1</p>+--<p>Text2</p>+</span><span class="token template-punctuation string">`</span></span> </code></pre> <p>接着,parseChildren 函数继续执行。此时模板的第一个字符为<,并且下一个字符能够匹配正则 /a-z/i。于是解析器再次进入parseElement 解析函数的执行阶段,这会消费模板内容<code><p>Text1</p></code>。在这一步过后,剩下的模板内容为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">+--<p>Text2</p>+</span><span class="token template-punctuation string">`</span></span> </code></pre> <p>可以看到,此时模板的第一个字符是换行符,于是调用parseText 函数消费模板内容 ±-。现在,模板中剩下的内容是:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><p>Text2</p>+</span><span class="token template-punctuation string">`</span></span> </code></pre> <p>解析器会再次调用 parseElement 函数处理标签节点。在这之后,剩下的模板内容为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">+</span><span class="token template-punctuation string">`</span></span> </code></pre> <p>可以看到,现在模板内容只剩下一个换行符了。parseChildren 函数会继续执行并调用 parseText 函数消费剩下的内容,并产生一个文本节点。最终,模板被解析完毕,“状态机 2”停止运行。</p> <p>在“状态机 2”运行期间,为了处理标签节点,我们又调用了两次 parseElement 函数。第一次调用用于处理内容<code><p>Text1</p></code>,第二次调用用于处理内容 <code><p>Text2</p></code>。我们知道,parseElement 函数会递归地调用 parseChildren 函数完成子节点的解析,这就意味着解析器会再开启了两个新的状态机。</p> <p>通过上述例子我们能够认识到,parseChildren 解析函数是整个状态机的核心,状态迁移操作都在该函数内完成。在parseChildren 函数运行过程中,为了处理标签节点,会调用parseElement 解析函数,这会间接地调用 parseChildren 函数,并产生一个新的状态机。随着标签嵌套层次的增加,新的状态机会随着 parseChildren 函数被递归地调用而不断创建,这就是“递归下降”中“递归”二字的含义。而上级parseChildren 函数的调用用于构造上级模板 AST 节点,被递归调用的下级 parseChildren 函数则用于构造下级模板 AST 节点。最终,会构造出一棵树型结构的模板 AST,这就是“递归下降”中“下降”二字的含义。</p> <h2>3、状态机的开启与停止</h2> <p>在上一节中,我们讨论了递归下降算法的含义。我们知道,parseChildren 函数本质上是一个状态机,它会开启一个 while 循环使得状态机自动运行,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseChildren</span><span class="token punctuation">(</span><span class="token parameter">context<span class="token punctuation">,</span> ancestors</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token keyword">let</span> nodes <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token number">03</span> <span class="token number">04</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> mode <span class="token punctuation">}</span> <span class="token operator">=</span> context <span class="token number">05</span> <span class="token comment">// 运行状态机</span> <span class="token number">06</span> <span class="token keyword">while</span><span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">isEnd</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> ancestors<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">07</span> <span class="token comment">// 省略部分代码</span> <span class="token number">08</span> <span class="token punctuation">}</span> <span class="token number">09</span> <span class="token number">10</span> <span class="token keyword">return</span> nodes <span class="token number">11</span> <span class="token punctuation">}</span> </code></pre> <p>这里的问题在于,状态机何时停止呢?换句话说,while 循环应该何时停止运行呢?这涉及 isEnd() 函数的判断逻辑。为了搞清楚这个问题,我们需要模拟状态机的运行过程。</p> <p>我们知道,在调用 parseElement 函数解析标签节点时,会递归地调用 parseChildren 函数,从而开启新的状态机,如下图所示:<br> <a href="http://img.e-com-net.com/image/info8/e4b2e844a057449e8fbb9c3e65b15b3a.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/e4b2e844a057449e8fbb9c3e65b15b3a.jpg" alt="Vue解析器_第8张图片" width="650" height="109" style="border:1px solid black;"></a><br> 为了便于描述,我们可以把上图中所示的新的状态机称为“状态机 1”。“状态机 1”开始运行,继续解析模板,直到遇到下一个 <code><p></code> 标签,如下图所示:<br> <a href="http://img.e-com-net.com/image/info8/747aaaf58bf547388127f7c72a408a38.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/747aaaf58bf547388127f7c72a408a38.jpg" alt="Vue解析器_第9张图片" width="650" height="320" style="border:1px solid black;"></a><br> 因为遇到了 <code><p></code> 标签,所以“状态机 1”也会调用parseElement 函数进行解析。于是又重复了上述过程,即把当前解析的标签节点压入父级节点栈,然后递归地调用parseChildren 函数开启新的状态机,即“状态机 2”。可以看到,此时有两个状态机在同时运行。</p> <p>此时“状态机 2”拥有程序的执行权,它持续解析模板直到遇到结束标签 <code></p></code>。因为这是一个结束标签,并且在父级节点栈中存在与该结束标签同名的标签节点,所以“状态机 2”会停止运行,并弹出父级节点栈中处于栈顶的节点,如下图所示:<br> <a href="http://img.e-com-net.com/image/info8/e62327ed04bd46c0b4c0a4a525e02bbf.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/e62327ed04bd46c0b4c0a4a525e02bbf.jpg" alt="Vue解析器_第10张图片" width="650" height="318" style="border:1px solid black;"></a><br> 此时“状态机 2”已经停止运行了,但“状态机 1”仍在运行中,于是会继续解析模板,直到遇到下一个 <code><p></code> 标签。这时“状态机 1”会再次调用 parseElement 函数解析标签节点,因此又会执行压栈并开启新的“状态机 3”,如下图 所示:<br> <a href="http://img.e-com-net.com/image/info8/debf65860a8d47c6b34d67e5d6b2ab64.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/debf65860a8d47c6b34d67e5d6b2ab64.jpg" alt="Vue解析器_第11张图片" width="650" height="372" style="border:1px solid black;"></a><br> 此时“状态机 3”拥有程序的执行权,它会继续解析模板,直到遇到结束标签 <code></p></code>。因为这是一个结束标签,并且在父级节点栈中存在与该结束标签同名的标签节点,所以“状态机 3”会停止运行,并弹出父级节点栈中处于栈顶的节点,如下图所示:<br> <a href="http://img.e-com-net.com/image/info8/ca8967eef14f41f6a92b47ddf8f67fe9.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/ca8967eef14f41f6a92b47ddf8f67fe9.jpg" alt="Vue解析器_第12张图片" width="650" height="373" style="border:1px solid black;"></a><br> 当“状态机 3”停止运行后,程序的执行权交还给“状态机1”。“状态机 1”会继续解析模板,直到遇到最后的 <code></div></code>结束标签。这时“状态机 1”发现父级节点栈中存在与结束标签同名的标签节点,于是将该节点弹出父级节点栈,并停止运行,如下图所示:<br> <a href="http://img.e-com-net.com/image/info8/387b0b61f4694b7a946de1f5d96a7705.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/387b0b61f4694b7a946de1f5d96a7705.jpg" alt="Vue解析器_第13张图片" width="650" height="372" style="border:1px solid black;"></a><br> 这时父级节点栈为空,状态机全部停止运行,模板解析完毕。</p> <p>通过上面的描述,我们能够清晰地认识到,解析器会在何时开启新的状态机,以及状态机会在何时停止。结论是:**当解析器遇到开始标签时,会将该标签压入父级节点栈,同时开启新的状态机。当解析器遇到结束标签,并且父级节点栈中存在与该标签同名的开始标签节点时,会停止当前正在运行的状态机。**根据上述规则,我们可以给出 isEnd 函数的逻辑,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">isEnd</span><span class="token punctuation">(</span><span class="token parameter">context<span class="token punctuation">,</span> ancestors</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 当模板内容解析完毕后,停止</span> <span class="token number">03</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>context<span class="token punctuation">.</span>source<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token boolean">true</span> <span class="token number">04</span> <span class="token comment">// 获取父级标签节点</span> <span class="token number">05</span> <span class="token keyword">const</span> parent <span class="token operator">=</span> ancestors<span class="token punctuation">[</span>ancestors<span class="token punctuation">.</span>length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span> <span class="token number">06</span> <span class="token comment">// 如果遇到结束标签,并且该标签与父级标签节点同名,则停止</span> <span class="token number">07</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>parent <span class="token operator">&&</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"></</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>parent<span class="token punctuation">.</span>tag<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">08</span> <span class="token keyword">return</span> <span class="token boolean">true</span> <span class="token number">09</span> <span class="token punctuation">}</span> <span class="token number">10</span> <span class="token punctuation">}</span> </code></pre> <p>上面这段代码展示了状态机的停止时机,具体如下:</p> <ul> <li>第一个停止时机是当模板内容被解析完毕时;</li> <li>第二个停止时机则是在遇到结束标签时,这时解析器会取得父级节点栈栈顶的节点作为父节点,检查该结束标签是否与父节点的标签同名,如果相同,则状态机停止运行。</li> </ul> <p>这里需要注意的是,在第二个停止时机中,我们直接比较结束标签的名称与栈顶节点的标签名称。这么做的确可行,但严格来讲是有瑕疵的。例如下面的模板所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>div<span class="token operator">></span><span class="token operator"><</span>span<span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>span<span class="token operator">></span> </code></pre> <p>观察上述模板,它存在一个明显的问题,你能发现吗?实际上,这段模板有两种解释方式,下图给出了第一种:<br> <a href="http://img.e-com-net.com/image/info8/b7a804d2896f43f98631921eab92d5bf.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/b7a804d2896f43f98631921eab92d5bf.jpg" alt="Vue解析器_第14张图片" width="650" height="419" style="border:1px solid black;"></a><br> 如上图所示,这种解释方式的流程如下:</p> <ul> <li>状态机 1”遇到 <code><div></code> 开始标签,调用 parseElement 解析函数,这会开启“状态机 2”来完成子节点的解析。</li> <li>“状态机 2”遇到 <code><span></code> 开始标签,调用 parseElement 解析函数,这会开启“状态机 3”来完成子节点的解析。</li> <li>“状态机 3”遇到 <code></div></code> 结束标签。由于此时父级节点栈栈顶的节点名称是 span,并不是 div,所以“状态机 3”不会停止运行。这时,“状态机 3”遭遇了不符合预期的状态,因为结束标签 <code></div></code> 缺少与之对应的开始标签,所以这时“状态机3”会抛出错误:“无效的结束标签”。</li> </ul> <p>上述流程的思路与我们当前的实现相符,状态机会遭遇不符合预期的状态。下面 parseChildren 函数的代码能够体现这一点:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseChildren</span><span class="token punctuation">(</span><span class="token parameter">context<span class="token punctuation">,</span> ancestors</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token keyword">let</span> nodes <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token number">03</span> <span class="token number">04</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> mode <span class="token punctuation">}</span> <span class="token operator">=</span> context <span class="token number">05</span> <span class="token number">06</span> <span class="token keyword">while</span><span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">isEnd</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> ancestors<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">07</span> <span class="token keyword">let</span> node <span class="token number">08</span> <span class="token number">09</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>mode <span class="token operator">===</span> TextModes<span class="token punctuation">.</span><span class="token constant">DATA</span> <span class="token operator">||</span> mode <span class="token operator">===</span> TextModes<span class="token punctuation">.</span><span class="token constant">RCDATA</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">10</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>mode <span class="token operator">===</span> TextModes<span class="token punctuation">.</span><span class="token constant">DATA</span> <span class="token operator">&&</span> context<span class="token punctuation">.</span>source<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">'<'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">11</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">'!'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">12</span> <span class="token comment">// 省略部分代码</span> <span class="token number">13</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">'/'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">14</span> <span class="token comment">// 状态机遭遇了闭合标签,此时应该抛出错误,因为它缺少与之对应的开始标签</span> <span class="token number">15</span> console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'无效的结束标签'</span><span class="token punctuation">)</span> <span class="token number">16</span> <span class="token keyword">continue</span> <span class="token number">17</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">[a-z]</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">.</span><span class="token function">test</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">18</span> <span class="token comment">// 省略部分代码</span> <span class="token number">19</span> <span class="token punctuation">}</span> <span class="token number">20</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'{{'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">21</span> <span class="token comment">// 省略部分代码</span> <span class="token number">22</span> <span class="token punctuation">}</span> <span class="token number">23</span> <span class="token punctuation">}</span> <span class="token number">24</span> <span class="token comment">// 省略部分代码</span> <span class="token number">25</span> <span class="token punctuation">}</span> <span class="token number">26</span> <span class="token number">27</span> <span class="token keyword">return</span> nodes <span class="token number">28</span> <span class="token punctuation">}</span> </code></pre> <p>换句话说,按照我们当前的实现思路来解析上述例子中的模板,最终得到的错误信息是:“无效的结束标签”。但其实还有另外一种更好的解析方式。观察上例中给出的模板,其中存在一段完整的内容,如下图所示:<br> <a href="http://img.e-com-net.com/image/info8/fbf402f76d52496e9dff8385bb17bded.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/fbf402f76d52496e9dff8385bb17bded.jpg" alt="Vue解析器_第15张图片" width="650" height="403" style="border:1px solid black;"></a><br> 从上图中可以看到,模板中存在一段完整的内容,我们希望解析器可以正常对其进行解析,这很可能也是符合用户意图的。但实际上,无论哪一种解释方式,对程序的影响都不大。两者的区别体现在错误处理上。对于第一种解释方式,我们得到的错误信息是:“无效的结束标签”。而对于第二种解释方式,在“完整的内容”部分被解析完毕后,解析器就会打印错误信息:“<code><span></code> 标签缺少闭合标签”。很显然,第二种解释方式更加合理。</p> <p>为了实现第二种解释方式,我们需要调整 isEnd 函数的逻辑。当判断状态机是否应该停止时,我们不应该总是与栈顶的父级节点做比较,而是应该与整个父级节点栈中的所有节点做比较。只要父级节点栈中存在与当前遇到的结束标签同名的节点,就停止状态机,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">isEnd</span><span class="token punctuation">(</span><span class="token parameter">context<span class="token punctuation">,</span> ancestors</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>context<span class="token punctuation">.</span>source<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token boolean">true</span> <span class="token number">03</span> <span class="token number">04</span> <span class="token comment">// 与父级节点栈内所有节点做比较</span> <span class="token number">05</span> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> ancestors<span class="token punctuation">.</span>length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">;</span> i <span class="token operator">>=</span> <span class="token number">0</span><span class="token punctuation">;</span> <span class="token operator">--</span>i<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">06</span> <span class="token comment">// 只要栈中存在与当前结束标签同名的节点,就停止状态机</span> <span class="token number">07</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"></</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ancestors<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>tag<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">08</span> <span class="token keyword">return</span> <span class="token boolean">true</span> <span class="token number">09</span> <span class="token punctuation">}</span> <span class="token number">10</span> <span class="token punctuation">}</span> <span class="token number">11</span> <span class="token punctuation">}</span> </code></pre> <p>按照新的思路再次对如下模板执行解析:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>div<span class="token operator">></span><span class="token operator"><</span>span<span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>span<span class="token operator">></span> </code></pre> <p>其流程如下:</p> <ul> <li>“状态机 1”遇到 <code><div></code> 开始标签,调用 parseElement 解析函数,并开启“状态机 2”解析子节点。</li> <li>“状态机 2”遇到 <code><span></code> 开始标签,调用 parseElement 解析函数,并开启“状态机 3”解析子节点。</li> <li>“状态机 3”遇到 <code></div></code> 结束标签,由于节点栈中存在名为div 的标签节点,于是“状态机 3”停止了。</li> </ul> <p>在这个过程中,“状态机 2”在调用 parseElement 解析函数时,parseElement 函数能够发现 <code><span></code> 缺少闭合标签,于是会打印错误信息“<code><span></code> 标签缺少闭合标签”,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseElement</span><span class="token punctuation">(</span><span class="token parameter">context<span class="token punctuation">,</span> ancestors</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token keyword">const</span> element <span class="token operator">=</span> <span class="token function">parseTag</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span> <span class="token number">03</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>element<span class="token punctuation">.</span>isSelfClosing<span class="token punctuation">)</span> <span class="token keyword">return</span> element <span class="token number">04</span> <span class="token number">05</span> ancestors<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>element<span class="token punctuation">)</span> <span class="token number">06</span> element<span class="token punctuation">.</span>children <span class="token operator">=</span> <span class="token function">parseChildren</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> ancestors<span class="token punctuation">)</span> <span class="token number">07</span> ancestors<span class="token punctuation">.</span><span class="token function">pop</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token number">08</span> <span class="token number">09</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"></</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>element<span class="token punctuation">.</span>tag<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">10</span> <span class="token function">parseTag</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> <span class="token string">'end'</span><span class="token punctuation">)</span> <span class="token number">11</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token number">12</span> <span class="token comment">// 缺少闭合标签</span> <span class="token number">13</span> console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>element<span class="token punctuation">.</span>tag<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> 标签缺少闭合标签</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span> <span class="token number">14</span> <span class="token punctuation">}</span> <span class="token number">15</span> <span class="token number">16</span> <span class="token keyword">return</span> element <span class="token number">17</span> <span class="token punctuation">}</span> </code></pre> <h2>4、解析标签节点</h2> <p>在上一节给出的 parseElement 函数的实现中,无论是解析开始标签还是闭合标签,我们都调用了 parseTag 函数。同时,我们使用 parseChildren 函数来解析开始标签与闭合标签中间的部分,如下面的代码及注释所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseElement</span><span class="token punctuation">(</span><span class="token parameter">context<span class="token punctuation">,</span> ancestors</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 调用 parseTag 函数解析开始标签</span> <span class="token number">03</span> <span class="token keyword">const</span> element <span class="token operator">=</span> <span class="token function">parseTag</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span> <span class="token number">04</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>element<span class="token punctuation">.</span>isSelfClosing<span class="token punctuation">)</span> <span class="token keyword">return</span> element <span class="token number">05</span> <span class="token number">06</span> ancestors<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>element<span class="token punctuation">)</span> <span class="token number">07</span> element<span class="token punctuation">.</span>children <span class="token operator">=</span> <span class="token function">parseChildren</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> ancestors<span class="token punctuation">)</span> <span class="token number">08</span> ancestors<span class="token punctuation">.</span><span class="token function">pop</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token number">09</span> <span class="token number">10</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"></</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>element<span class="token punctuation">.</span>tag<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">11</span> <span class="token comment">// 再次调用 parseTag 函数解析结束标签,传递了第二个参数:'end'</span> <span class="token number">12</span> <span class="token function">parseTag</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> <span class="token string">'end'</span><span class="token punctuation">)</span> <span class="token number">13</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token number">14</span> console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>element<span class="token punctuation">.</span>tag<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> 标签缺少闭合标签</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span> <span class="token number">15</span> <span class="token punctuation">}</span> <span class="token number">16</span> <span class="token number">17</span> <span class="token keyword">return</span> element <span class="token number">18</span> <span class="token punctuation">}</span> </code></pre> <p>标签节点的整个解析过程如下图所示:<br> <a href="http://img.e-com-net.com/image/info8/16d89877ccc2415aa0651faae47adae4.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/16d89877ccc2415aa0651faae47adae4.jpg" alt="Vue解析器_第16张图片" width="650" height="117" style="border:1px solid black;"></a><br> 这里需要注意的是,由于开始标签与结束标签的格式非常类似,所以我们统一使用 parseTag 函数处理,并通过该函数的第二个参数来指定具体的处理类型。当第二个参数值为字符串’end’ 时,意味着解析的是结束标签。另外,无论处理的是开始标签还是结束标签,parseTag 函数都会消费对应的内容。为了实现对模板内容的消费,我们需要在上下文对象中新增两个工具函数,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parse</span><span class="token punctuation">(</span><span class="token parameter">str</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 上下文对象</span> <span class="token number">03</span> <span class="token keyword">const</span> context <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token number">04</span> <span class="token comment">// 模板内容</span> <span class="token number">05</span> <span class="token literal-property property">source</span><span class="token operator">:</span> str<span class="token punctuation">,</span> <span class="token number">06</span> <span class="token literal-property property">mode</span><span class="token operator">:</span> TextModes<span class="token punctuation">.</span><span class="token constant">DATA</span><span class="token punctuation">,</span> <span class="token number">07</span> <span class="token comment">// advanceBy 函数用来消费指定数量的字符,它接收一个数字作为参数</span> <span class="token number">08</span> <span class="token function">advanceBy</span><span class="token punctuation">(</span><span class="token parameter">num</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">09</span> <span class="token comment">// 根据给定字符数 num,截取位置 num 后的模板内容,并替换当前模板内容</span> <span class="token number">10</span> context<span class="token punctuation">.</span>source <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span>num<span class="token punctuation">)</span> <span class="token number">11</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">12</span> <span class="token comment">// 无论是开始标签还是结束标签,都可能存在无用的空白字符,例如 <div ></span> <span class="token number">13</span> <span class="token function">advanceSpaces</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">14</span> <span class="token comment">// 匹配空白字符</span> <span class="token number">15</span> <span class="token keyword">const</span> match <span class="token operator">=</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^[\t\r\n\f ]+</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">)</span> <span class="token number">16</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>match<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">17</span> <span class="token comment">// 调用 advanceBy 函数消费空白字符</span> <span class="token number">18</span> context<span class="token punctuation">.</span><span class="token function">advanceBy</span><span class="token punctuation">(</span>match<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">19</span> <span class="token punctuation">}</span> <span class="token number">20</span> <span class="token punctuation">}</span> <span class="token number">21</span> <span class="token punctuation">}</span> <span class="token number">22</span> <span class="token number">23</span> <span class="token keyword">const</span> nodes <span class="token operator">=</span> <span class="token function">parseChildren</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token number">24</span> <span class="token number">25</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> <span class="token number">26</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Root'</span><span class="token punctuation">,</span> <span class="token number">27</span> <span class="token literal-property property">children</span><span class="token operator">:</span> nodes <span class="token number">28</span> <span class="token punctuation">}</span> <span class="token number">29</span> <span class="token punctuation">}</span> </code></pre> <p>在上面这段代码中,我们为上下文对象增加了 advanceBy 函数和 advanceSpaces 函数。其中 advanceBy 函数用来消费指定数量的字符。其实现原理很简单,即调用字符串的 slice 函数,根据指定位置截取剩余字符串,并使用截取后的结果作为新的模板内容。advanceSpaces 函数则用来消费无用的空白字符,因为标签中可能存在空白字符,例如在模板 <code><div----></code> 中减号(-)代表空白字符。</p> <p>有了 advanceBy 和 advanceSpaces 函数后,我们就可以给出parseTag 函数的实现了,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token comment">// 由于 parseTag 既用来处理开始标签,也用来处理结束标签,因此我们设计第二个参数 type,</span> <span class="token number">02</span> <span class="token comment">// 用来代表当前处理的是开始标签还是结束标签,type 的默认值为 'start',即默认作为开始标签处理</span> <span class="token number">03</span> <span class="token keyword">function</span> <span class="token function">parseTag</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> type <span class="token operator">=</span> <span class="token string">'start'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">04</span> <span class="token comment">// 从上下文对象中拿到 advanceBy 函数</span> <span class="token number">05</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> advanceBy<span class="token punctuation">,</span> advanceSpaces <span class="token punctuation">}</span> <span class="token operator">=</span> context <span class="token number">06</span> <span class="token number">07</span> <span class="token comment">// 处理开始标签和结束标签的正则表达式不同</span> <span class="token number">08</span> <span class="token keyword">const</span> match <span class="token operator">=</span> type <span class="token operator">===</span> <span class="token string">'start'</span> <span class="token number">09</span> <span class="token comment">// 匹配开始标签</span> <span class="token number">10</span> <span class="token operator">?</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^<([a-z][^\t\r\n\f />]*)</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">)</span> <span class="token number">11</span> <span class="token comment">// 匹配结束标签</span> <span class="token number">12</span> <span class="token operator">:</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^<\/([a-z][^\t\r\n\f />]*)</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">)</span> <span class="token number">13</span> <span class="token comment">// 匹配成功后,正则表达式的第一个捕获组的值就是标签名称</span> <span class="token number">14</span> <span class="token keyword">const</span> tag <span class="token operator">=</span> match<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token number">15</span> <span class="token comment">// 消费正则表达式匹配的全部内容,例如 '<div' 这段内容</span> <span class="token number">16</span> <span class="token function">advanceBy</span><span class="token punctuation">(</span>match<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">17</span> <span class="token comment">// 消费标签中无用的空白字符</span> <span class="token number">18</span> <span class="token function">advanceSpaces</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token number">19</span> <span class="token number">20</span> <span class="token comment">// 在消费匹配的内容后,如果字符串以 '/>' 开头,则说明这是一个自闭合标签</span> <span class="token number">21</span> <span class="token keyword">const</span> isSelfClosing <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'/>'</span><span class="token punctuation">)</span> <span class="token number">22</span> <span class="token comment">// 如果是自闭合标签,则消费 '/>', 否则消费 '>'</span> <span class="token number">23</span> <span class="token function">advanceBy</span><span class="token punctuation">(</span>isSelfClosing <span class="token operator">?</span> <span class="token number">2</span> <span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token number">24</span> <span class="token number">25</span> <span class="token comment">// 返回标签节点</span> <span class="token number">26</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> <span class="token number">27</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Element'</span><span class="token punctuation">,</span> <span class="token number">28</span> <span class="token comment">// 标签名称</span> <span class="token number">29</span> tag<span class="token punctuation">,</span> <span class="token number">30</span> <span class="token comment">// 标签的属性暂时留空</span> <span class="token number">31</span> <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">32</span> <span class="token comment">// 子节点留空</span> <span class="token number">33</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">34</span> <span class="token comment">// 是否自闭合</span> <span class="token number">35</span> isSelfClosing <span class="token number">36</span> <span class="token punctuation">}</span> <span class="token number">37</span> <span class="token punctuation">}</span> </code></pre> <p>上面这段代码有两个关键点:</p> <ul> <li>由于 parseTag 函数既用于解析开始标签,又用于解析结束标签,因此需要用一个参数来标识当前处理的标签类型,即type。</li> <li>对于开始标签和结束标签,用于匹配它们的正则表达式只有一点不同:结束标签是以字符串 </ 开头的。下图给出了用于匹配开始标签的正则表达式的含义。</li> </ul> <p><a href="http://img.e-com-net.com/image/info8/7821b721cf4b4aafb323ff7922681015.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/7821b721cf4b4aafb323ff7922681015.jpg" alt="Vue解析器_第17张图片" width="650" height="324" style="border:1px solid black;"></a><br> 下面给出了几个使用上图所示的正则来匹配开始标签的例子:</p> <ul> <li>对于字符串 <code>'<div></code>',会匹配出字符串 ‘<code><div',剩余 '></code>’。</li> <li>对于字符串 <code>'<div/>'</code>,会匹配出字符串 <code>'<div',剩余 '/>'</code>。</li> <li>对于字符串 <code>'<div---->'</code>,其中减号(-)代表空白符,会匹配出字符串 <code>'<div',剩余 '---->'</code>。</li> </ul> <p>另外,上图中所示的正则拥有一个捕获组,它用来捕获标签名称。<br> 除了正则表达式外,parseTag 函数的另外几个关键点如下:</p> <ul> <li>在完成正则匹配后,需要调用 advanceBy 函数消费由正则匹配的全部内容。</li> <li>根据上面给出的第三个正则匹配例子可知,由于标签中可能存在无用的空白字符,例如 <code><div----></code>,因此我们需要调用advanceSpaces 函数消费空白字符。</li> <li>在消费由正则匹配的内容后,需要检查剩余模板内容是否以字符串 <code>/></code> 开头。如果是,则说明当前解析的是一个自闭合标签,这时需要将标签节点的 isSelfClosing 属性设置为 true。</li> <li>最后,判断标签是否自闭合。如果是,则调用 advnaceBy 函数消费内容 <code>/></code>,否则只需要消费内容 > 即可。</li> </ul> <p>在经过上述处理后,parseTag 函数会返回一个标签节点。parseElement 函数在得到由 parseTag 函数产生的标签节点后,需要根据节点的类型完成文本模式的切换,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseElement</span><span class="token punctuation">(</span><span class="token parameter">context<span class="token punctuation">,</span> ancestors</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token keyword">const</span> element <span class="token operator">=</span> <span class="token function">parseTag</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span> <span class="token number">03</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>element<span class="token punctuation">.</span>isSelfClosing<span class="token punctuation">)</span> <span class="token keyword">return</span> element <span class="token number">04</span> <span class="token number">05</span> <span class="token comment">// 切换到正确的文本模式</span> <span class="token number">06</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>element<span class="token punctuation">.</span>tag <span class="token operator">===</span> <span class="token string">'textarea'</span> <span class="token operator">||</span> element<span class="token punctuation">.</span>tag <span class="token operator">===</span> <span class="token string">'title'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">07</span> <span class="token comment">// 如果由 parseTag 解析得到的标签是 <textarea> 或 <title>,则切换到 RCDATA 模式</span> <span class="token number">08</span> context<span class="token punctuation">.</span>mode <span class="token operator">=</span> TextModes<span class="token punctuation">.</span><span class="token constant">RCDATA</span> <span class="token number">09</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">style|xmp|iframe|noembed|noframes|noscript</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">.</span><span class="token function">test</span><span class="token punctuation">(</span>element<span class="token punctuation">.</span>tag<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">10</span> <span class="token comment">// 如果由 parseTag 解析得到的标签是:</span> <span class="token number">11</span> <span class="token comment">// <style>、<xmp>、<iframe>、<noembed>、<noframes>、<noscript></span> <span class="token number">12</span> <span class="token comment">// 则切换到 RAWTEXT 模式</span> <span class="token number">13</span> context<span class="token punctuation">.</span>mode <span class="token operator">=</span> TextModes<span class="token punctuation">.</span><span class="token constant">RAWTEXT</span> <span class="token number">14</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token number">15</span> <span class="token comment">// 否则切换到 DATA 模式</span> <span class="token number">16</span> context<span class="token punctuation">.</span>mode <span class="token operator">=</span> TextModes<span class="token punctuation">.</span><span class="token constant">DATA</span> <span class="token number">17</span> <span class="token punctuation">}</span> <span class="token number">18</span> <span class="token number">19</span> ancestors<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>element<span class="token punctuation">)</span> <span class="token number">20</span> element<span class="token punctuation">.</span>children <span class="token operator">=</span> <span class="token function">parseChildren</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> ancestors<span class="token punctuation">)</span> <span class="token number">21</span> ancestors<span class="token punctuation">.</span><span class="token function">pop</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token number">22</span> <span class="token number">23</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"></</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>element<span class="token punctuation">.</span>tag<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">24</span> <span class="token function">parseTag</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> <span class="token string">'end'</span><span class="token punctuation">)</span> <span class="token number">25</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token number">26</span> console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>element<span class="token punctuation">.</span>tag<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> 标签缺少闭合标签</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span> <span class="token number">27</span> <span class="token punctuation">}</span> <span class="token number">28</span> <span class="token number">29</span> <span class="token keyword">return</span> element <span class="token number">30</span> <span class="token punctuation">}</span> </code></pre> <p>至此,我们就实现了对标签节点的解析。但是目前的实现忽略了节点中的属性和指令,下一节将会讲解。</p> <h2>5、解析属性</h2> <p>上一节中介绍的 parseTag 解析函数会消费整个开始标签,这意味着该函数需要有能力处理开始标签中存在的属性与指令,例如:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>div id<span class="token operator">=</span><span class="token string">"foo"</span> v<span class="token operator">-</span>show<span class="token operator">=</span><span class="token string">"display"</span><span class="token operator">/</span><span class="token operator">></span> </code></pre> <p>上面这段模板中的 div 标签存在一个 id 属性和一个 v-show 指令。为了处理属性和指令,我们需要在 parseTag 函数中增加parseAttributes 解析函数,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseTag</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> type <span class="token operator">=</span> <span class="token string">'start'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> advanceBy<span class="token punctuation">,</span> advanceSpaces <span class="token punctuation">}</span> <span class="token operator">=</span> context <span class="token number">03</span> <span class="token number">04</span> <span class="token keyword">const</span> match <span class="token operator">=</span> type <span class="token operator">===</span> <span class="token string">'start'</span> <span class="token number">05</span> <span class="token operator">?</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^<([a-z][^\t\r\n\f />]*)</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">)</span> <span class="token number">06</span> <span class="token operator">:</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^<\/([a-z][^\t\r\n\f />]*)</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">)</span> <span class="token number">07</span> <span class="token keyword">const</span> tag <span class="token operator">=</span> match<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token number">08</span> <span class="token number">09</span> <span class="token function">advanceBy</span><span class="token punctuation">(</span>match<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">10</span> <span class="token function">advanceSpaces</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token number">11</span> <span class="token comment">// 调用 parseAttributes 函数完成属性与指令的解析,并得到 props 数组,</span> <span class="token number">12</span> <span class="token comment">// props 数组是由指令节点与属性节点共同组成的数组</span> <span class="token number">13</span> <span class="token keyword">const</span> props <span class="token operator">=</span> <span class="token function">parseAttributes</span><span class="token punctuation">(</span>context<span class="token punctuation">)</span> <span class="token number">14</span> <span class="token number">15</span> <span class="token keyword">const</span> isSelfClosing <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'/>'</span><span class="token punctuation">)</span> <span class="token number">16</span> <span class="token function">advanceBy</span><span class="token punctuation">(</span>isSelfClosing <span class="token operator">?</span> <span class="token number">2</span> <span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token number">17</span> <span class="token number">18</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> <span class="token number">19</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Element'</span><span class="token punctuation">,</span> <span class="token number">20</span> tag<span class="token punctuation">,</span> <span class="token number">21</span> props<span class="token punctuation">,</span> <span class="token comment">// 将 props 数组添加到标签节点上</span> <span class="token number">22</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">23</span> isSelfClosing <span class="token number">24</span> <span class="token punctuation">}</span> <span class="token number">25</span> <span class="token punctuation">}</span> </code></pre> <p>上面这段代码的关键点之一是,我们需要在消费标签的“开始部分”和无用的空白字符之后,再调用 parseAttribute 函数。举个例子,假设标签的内容如下:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>div id<span class="token operator">=</span><span class="token string">"foo"</span> v<span class="token operator">-</span>show<span class="token operator">=</span><span class="token string">"display"</span> <span class="token operator">></span> </code></pre> <p>标签的“开始部分”指的是字符串 <code><div,所以当消耗标签的“开始部分”</code>以及无用空白字符后,剩下的内容为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> id<span class="token operator">=</span><span class="token string">"foo"</span> v<span class="token operator">-</span>show<span class="token operator">=</span><span class="token string">"display"</span> <span class="token operator">></span> </code></pre> <p>上面这段内容才是 parseAttributes 函数要处理的内容。由于该函数只用来解析属性和指令,因此它会不断地消费上面这段模板内容,直到遇到标签的“结束部分”为止。其中,结束部分指的是字符 > 或者字符串 />。据此我们可以给出parseAttributes 函数的整体框架,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseAttributes</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 用来存储解析过程中产生的属性节点和指令节点</span> <span class="token number">03</span> <span class="token keyword">const</span> props <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token number">04</span> <span class="token number">05</span> <span class="token comment">// 开启 while 循环,不断地消费模板内容,直至遇到标签的“结束部分”为止</span> <span class="token number">06</span> <span class="token keyword">while</span> <span class="token punctuation">(</span> <span class="token number">07</span> <span class="token operator">!</span>context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'>'</span><span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token number">08</span> <span class="token operator">!</span>context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'/>'</span><span class="token punctuation">)</span> <span class="token number">09</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">10</span> <span class="token comment">// 解析属性或指令</span> <span class="token number">11</span> <span class="token punctuation">}</span> <span class="token number">12</span> <span class="token comment">// 将解析结果返回</span> <span class="token number">13</span> <span class="token keyword">return</span> props <span class="token number">14</span> <span class="token punctuation">}</span> </code></pre> <p>实际上,parseAttributes 函数消费模板内容的过程,就是不断地解析属性名称、等于号、属性值的过程,如下图所示:<br> <a href="http://img.e-com-net.com/image/info8/4955b1f36c964669898793e304a4954e.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/4955b1f36c964669898793e304a4954e.jpg" alt="Vue解析器_第18张图片" width="650" height="188" style="border:1px solid black;"></a><br> parseAttributes 函数会按照从左到右的顺序不断地消费字符串。以上图为例,该函数的解析过程如下:<br> 首先,解析出第一个属性的名称 id,并消费字符串 ‘id’。此时剩余模板内容为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator">=</span><span class="token string">"foo"</span> v<span class="token operator">-</span>show<span class="token operator">=</span><span class="token string">"display"</span> <span class="token operator">></span> </code></pre> <p>在解析属性名称时,除了要消费属性名称之外,还要消费属性名称后面可能存在的空白字符。如下面这段模板中,属性名称和等于号之间存在空白字符:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> id <span class="token operator">=</span> <span class="token string">"foo"</span> v<span class="token operator">-</span>show<span class="token operator">=</span><span class="token string">"display"</span> <span class="token operator">></span> </code></pre> <p>但无论如何,在属性名称解析完毕之后,模板剩余内容一定是以等于号开头的,即:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator">=</span> <span class="token string">"foo"</span> v<span class="token operator">-</span>show<span class="token operator">=</span><span class="token string">"display"</span> <span class="token operator">></span> </code></pre> <p>如果消费属性名称之后,模板内容不以等于号开头,则说明模板内容不合法,我们可以选择性地抛出错误。</p> <p>接着,我们需要消费等于号字符。由于等于号和属性值之间也可能存在空白字符,所以我们也需要消费对应的空白字符。在这一步操作过后,模板的剩余内容如下:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token string">"foo"</span> v<span class="token operator">-</span>show<span class="token operator">=</span><span class="token string">"display"</span> <span class="token operator">></span> </code></pre> <p>接下来,到了处理属性值的环节。模板中的属性值存在三种情况:</p> <ul> <li>属性值被双引号包裹:id=“foo”。</li> <li>属性值被单引号包裹:id=‘foo’。</li> <li>属性值没有引号包裹:id=foo。</li> </ul> <p>按照上述例子,此时模板的内容一定以双引号(")开头。因此我们可以通过检查当前模板内容是否以引号开头来确定属性值是否被引用。如果属性值被引号引用,则消费引号。此时模板的剩余内容为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> foo<span class="token string">" v-show="</span>display" <span class="token operator">></span> </code></pre> <p>既然属性值被引号引用了,就意味着在剩余模板内容中,下一个引号之前的内容都应该被解析为属性值。在这个例子中,属性值的内容是字符串 foo。于是,我们消费属性值及其后面的引号。当然,如果属性值没有被引号引用,那么在剩余模板内容中,下一个空白字符之前的所有字符都应该作为属性值。</p> <p>当属性值和引号被消费之后,由于属性值与下一个属性名称之间可能存在空白字符,所以我们还要消费对应的空白字符。在这一步处理过后,剩余模板内容为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> v<span class="token operator">-</span>show<span class="token operator">=</span><span class="token string">"display"</span> <span class="token operator">></span> </code></pre> <p>可以看到,经过上述操作之后,第一个属性就处理完毕了。</p> <p>此时模板中还剩下一个指令,我们只需重新执行上述步骤,即可完成 v-show 指令的解析。当 v-show 指令解析完毕后,将会遇到标签的“结束部分”,即字符 >。这时,parseAttributes 函数中的 while 循环将会停止,完成属性和指令的解析。</p> <p>下面的 parseAttributes 函数给出了上述逻辑的具体实现:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseAttributes</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> advanceBy<span class="token punctuation">,</span> advanceSpaces <span class="token punctuation">}</span> <span class="token operator">=</span> context <span class="token number">03</span> <span class="token keyword">const</span> props <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token number">04</span> <span class="token number">05</span> <span class="token keyword">while</span> <span class="token punctuation">(</span> <span class="token number">06</span> <span class="token operator">!</span>context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'>'</span><span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token number">07</span> <span class="token operator">!</span>context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'/>'</span><span class="token punctuation">)</span> <span class="token number">08</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">09</span> <span class="token comment">// 该正则用于匹配属性名称</span> <span class="token number">10</span> <span class="token keyword">const</span> match <span class="token operator">=</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^[^\t\r\n\f />][^\t\r\n\f />=]*</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">)</span> <span class="token number">11</span> <span class="token comment">// 得到属性名称</span> <span class="token number">12</span> <span class="token keyword">const</span> name <span class="token operator">=</span> match<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token number">13</span> <span class="token number">14</span> <span class="token comment">// 消费属性名称</span> <span class="token number">15</span> <span class="token function">advanceBy</span><span class="token punctuation">(</span>name<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">16</span> <span class="token comment">// 消费属性名称与等于号之间的空白字符</span> <span class="token number">17</span> <span class="token function">advanceSpaces</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token number">18</span> <span class="token comment">// 消费等于号</span> <span class="token number">19</span> <span class="token function">advanceBy</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token number">20</span> <span class="token comment">// 消费等于号与属性值之间的空白字符</span> <span class="token number">21</span> <span class="token function">advanceSpaces</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token number">22</span> <span class="token number">23</span> <span class="token comment">// 属性值</span> <span class="token number">24</span> <span class="token keyword">let</span> value <span class="token operator">=</span> <span class="token string">''</span> <span class="token number">25</span> <span class="token number">26</span> <span class="token comment">// 获取当前模板内容的第一个字符</span> <span class="token number">27</span> <span class="token keyword">const</span> quote <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token number">28</span> <span class="token comment">// 判断属性值是否被引号引用</span> <span class="token number">29</span> <span class="token keyword">const</span> isQuoted <span class="token operator">=</span> quote <span class="token operator">===</span> <span class="token string">'"'</span> <span class="token operator">||</span> quote <span class="token operator">===</span> <span class="token string">"'"</span> <span class="token number">30</span> <span class="token number">31</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>isQuoted<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">32</span> <span class="token comment">// 属性值被引号引用,消费引号</span> <span class="token number">33</span> <span class="token function">advanceBy</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token number">34</span> <span class="token comment">// 获取下一个引号的索引</span> <span class="token number">35</span> <span class="token keyword">const</span> endQuoteIndex <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span>quote<span class="token punctuation">)</span> <span class="token number">36</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>endQuoteIndex <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">37</span> <span class="token comment">// 获取下一个引号之前的内容作为属性值</span> <span class="token number">38</span> value <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> endQuoteIndex<span class="token punctuation">)</span> <span class="token number">39</span> <span class="token comment">// 消费属性值</span> <span class="token number">40</span> <span class="token function">advanceBy</span><span class="token punctuation">(</span>value<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">41</span> <span class="token comment">// 消费引号</span> <span class="token number">42</span> <span class="token function">advanceBy</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token number">43</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token number">44</span> <span class="token comment">// 缺少引号错误</span> <span class="token number">45</span> console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'缺少引号'</span><span class="token punctuation">)</span> <span class="token number">46</span> <span class="token punctuation">}</span> <span class="token number">47</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token number">48</span> <span class="token comment">// 代码运行到这里,说明属性值没有被引号引用</span> <span class="token number">49</span> <span class="token comment">// 下一个空白字符之前的内容全部作为属性值</span> <span class="token number">50</span> <span class="token keyword">const</span> match <span class="token operator">=</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^[^\t\r\n\f >]+</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span>source<span class="token punctuation">)</span> <span class="token number">51</span> <span class="token comment">// 获取属性值</span> <span class="token number">52</span> value <span class="token operator">=</span> match<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token number">53</span> <span class="token comment">// 消费属性值</span> <span class="token number">54</span> <span class="token function">advanceBy</span><span class="token punctuation">(</span>value<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">55</span> <span class="token punctuation">}</span> <span class="token number">56</span> <span class="token comment">// 消费属性值后面的空白字符</span> <span class="token number">57</span> <span class="token function">advanceSpaces</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token number">58</span> <span class="token number">59</span> <span class="token comment">// 使用属性名称 + 属性值创建一个属性节点,添加到 props 数组中</span> <span class="token number">60</span> props<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token number">61</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Attribute'</span><span class="token punctuation">,</span> <span class="token number">62</span> name<span class="token punctuation">,</span> <span class="token number">63</span> value <span class="token number">64</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token number">65</span> <span class="token number">66</span> <span class="token punctuation">}</span> <span class="token number">67</span> <span class="token comment">// 返回</span> <span class="token number">68</span> <span class="token keyword">return</span> props <span class="token number">69</span> <span class="token punctuation">}</span> </code></pre> <p>在上面这段代码中,有两个重要的正则表达式:</p> <ul> <li><code>/^[^\t\r\n\f />][^\t\r\n\f />=]*/</code>,用来匹配属性名称;</li> <li><code>/^[^\t\r\n\f >]+/</code>,用来匹配没有使用引号引用的属性值。</li> </ul> <p>我们分别来看看这两个正则表达式是如何工作的。下图给出了用于匹配属性名称的正则表达式的匹配原理:<br> <a href="http://img.e-com-net.com/image/info8/f84cd05cfee248d090111053189652fe.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/f84cd05cfee248d090111053189652fe.jpg" alt="Vue解析器_第19张图片" width="650" height="227" style="border:1px solid black;"></a><br> 如上图所示,我们可以将这个正则表达式分为 A、B 两个部分来看:</p> <ul> <li>部分 A 用于匹配一个位置,这个位置不能是空白字符,也不能是字符 / 或字符 >,并且字符串要以该位置开头。</li> <li>部分 B 则用于匹配 0 个或多个位置,这些位置不能是空白字符,也不能是字符 /、>、=。注意,这些位置不允许出现等于号(=)字符,这就实现了只匹配等于号之前的内容,即属性名称。</li> </ul> <p>下图给出了第二个正则表达式的匹配原理:</p> <p><a href="http://img.e-com-net.com/image/info8/21ca21a4361b4021a10e2c09fcb9d284.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/21ca21a4361b4021a10e2c09fcb9d284.jpg" alt="Vue解析器_第20张图片" width="650" height="212" style="border:1px solid black;"></a><br> 该正则表达式从字符串的开始位置进行匹配,并且会匹配一个或多个非空白字符、非字符 >。换句话说,该正则表达式会一直对字符串进行匹配,直到遇到空白字符或字符 > 为止,这就实现了属性值的提取。</p> <p>配合 parseAttributes 函数,假设给出如下模板:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>div id<span class="token operator">=</span><span class="token string">"foo"</span> v<span class="token operator">-</span>show<span class="token operator">=</span><span class="token string">"display"</span><span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> </code></pre> <p>解析上面这段模板,将会得到如下 AST:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> ast <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Root'</span><span class="token punctuation">,</span> <span class="token number">03</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token number">04</span> <span class="token punctuation">{</span> <span class="token number">05</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Element'</span> <span class="token number">06</span> <span class="token literal-property property">tag</span><span class="token operator">:</span> <span class="token string">'div'</span><span class="token punctuation">,</span> <span class="token number">07</span> <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token number">08</span> <span class="token comment">// 属性</span> <span class="token number">09</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Attribute'</span><span class="token punctuation">,</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'id'</span><span class="token punctuation">,</span> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">'foo'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">10</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Attribute'</span><span class="token punctuation">,</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'v-show'</span><span class="token punctuation">,</span> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">'display'</span> <span class="token punctuation">}</span> <span class="token number">11</span> <span class="token punctuation">]</span> <span class="token number">12</span> <span class="token punctuation">}</span> <span class="token number">13</span> <span class="token punctuation">]</span> <span class="token number">14</span> <span class="token punctuation">}</span> </code></pre> <p>可以看到,在 div 标签节点的 props 属性中,包含两个类型为Attribute 的节点,这两个节点就是 parseAttributes 函数的解析结果。</p> <p>我们可以增加更多在 Vue.js 中常见的属性和指令进行测试,如以下模板所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>div <span class="token operator">:</span>id<span class="token operator">=</span><span class="token string">"dynamicId"</span> @click<span class="token operator">=</span><span class="token string">"handler"</span> v<span class="token operator">-</span>on<span class="token operator">:</span>mousedown<span class="token operator">=</span><span class="token string">"onMouseDown"</span> <span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> </code></pre> <p>上面这段模板经过解析后,得到如下 AST:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> ast <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Root'</span><span class="token punctuation">,</span> <span class="token number">03</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token number">04</span> <span class="token punctuation">{</span> <span class="token number">05</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Element'</span> <span class="token number">06</span> <span class="token literal-property property">tag</span><span class="token operator">:</span> <span class="token string">'div'</span><span class="token punctuation">,</span> <span class="token number">07</span> <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token number">08</span> <span class="token comment">// 属性</span> <span class="token number">09</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Attribute'</span><span class="token punctuation">,</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">':id'</span><span class="token punctuation">,</span> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">'dynamicId'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">10</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Attribute'</span><span class="token punctuation">,</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'@click'</span><span class="token punctuation">,</span> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">'handler'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">11</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Attribute'</span><span class="token punctuation">,</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'v-on:mousedown'</span><span class="token punctuation">,</span> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">'onMouseDown'</span> <span class="token punctuation">}</span> <span class="token number">12</span> <span class="token punctuation">]</span> <span class="token number">13</span> <span class="token punctuation">}</span> <span class="token number">14</span> <span class="token punctuation">]</span> <span class="token number">15</span> <span class="token punctuation">}</span> </code></pre> <p>可以看到,在类型为 Attribute 的属性节点中,其 name 字段完整地保留着模板中编写的属性名称。我们可以对属性名称做进一步的分析,从而得到更具体的信息。例如,属性名称以字符 @ 开头,则认为它是一个 v-on 指令绑定。我们甚至可以把以 v- 开头的属性看作指令绑定,从而为它赋予不同的节点类型,例如:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token comment">// 指令,类型为 Directive</span> <span class="token number">02</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Directive'</span><span class="token punctuation">,</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'v-on:mousedown'</span><span class="token punctuation">,</span> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">'onMouseDown'</span> <span class="token punctuation">}</span> <span class="token number">03</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Directive'</span><span class="token punctuation">,</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'@click'</span><span class="token punctuation">,</span> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">'handler'</span> <span class="token punctuation">}</span> <span class="token number">04</span> <span class="token comment">// 普通属性</span> <span class="token number">05</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Attribute'</span><span class="token punctuation">,</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'id'</span><span class="token punctuation">,</span> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token string">'foo'</span> <span class="token punctuation">}</span> </code></pre> <p>不仅如此,为了得到更加具体的信息,我们甚至可以进一步分析指令节点的数据,也可以设计更多语法规则,这完全取决于框架设计者在语法层面的设计,以及为框架赋予的能力。</p> <h2>6、解析文本与解码 HTML 实体</h2> <h3>6.1、解析文本</h3> <p>本节我们将讨论文本节点的解析。给出如下模板:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token string">'<div>Text</div>'</span> </code></pre> <p>解析器在解析上面这段模板时,会先经过 parseTag 函数的处理,这会消费标签的开始部分 ‘</p> <div> ’。处理完毕后,剩余模板内容为: </div> <p></p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token string">'Text</div>'</span> </code></pre> <p>紧接着,解析器会调用 parseChildren 函数,开启一个新的状态机来处理这段模板。我们来回顾一下状态机的状态迁移过程,如下图所示:<br> <a href="http://img.e-com-net.com/image/info8/397e3791e9854495abd17daa260c3b85.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/397e3791e9854495abd17daa260c3b85.jpg" alt="Vue解析器_第21张图片" width="650" height="489" style="border:1px solid black;"></a><br> 状态机始于“状态 1”。在“状态 1”下,读取模板的第一个字符 T,由于该字符既不是字符 <,也不是插值定界符 {{,因此状态机会进入“状态 7”,即调用 parseText 函数处理文本内容。此时解析器会在模板中寻找下一个 < 字符或插值定界符 {{的位置索引,记为索引 I。然后,解析器会从模板的头部到索引I 的位置截取内容,这段截取出来的字符串将作为文本节点的内容。以下面的模板内容为例:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token string">'Text</div>'</span> </code></pre> <p>parseText 函数会尝试在这段模板内容中找到第一个出现的字符< 的位置索引。在这个例子中,字符 < 的索引值为 4。然后,parseText 函数会截取介于索引 [0, 4) 的内容作为文本内容。在这个例子中,文本内容就是字符串 ‘Text’。</p> <p>假设模板中存在插值,如下面的模板所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> template <span class="token operator">=</span> <span class="token string">'Text-{{ val }}</div>'</span> </code></pre> <p>在处理这段模板时,parseText 函数会找到第一个插值定界符 {{出现的位置索引。在这个例子中,定界符的索引为 5。于是,parseText 函数会截取介于索引 [0, 5) 的内容作为文本内容。在这个例子中,文本内容就是字符串 ‘Text-’。</p> <p>下面的 parseText 函数给出了具体实现:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseText</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// endIndex 为文本内容的结尾索引,默认将整个模板剩余内容都作为文本内容</span> <span class="token number">03</span> <span class="token keyword">let</span> endIndex <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span>length <span class="token number">04</span> <span class="token comment">// 寻找字符 < 的位置索引</span> <span class="token number">05</span> <span class="token keyword">const</span> ltIndex <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'<'</span><span class="token punctuation">)</span> <span class="token number">06</span> <span class="token comment">// 寻找定界符 {{ 的位置索引</span> <span class="token number">07</span> <span class="token keyword">const</span> delimiterIndex <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'{{'</span><span class="token punctuation">)</span> <span class="token number">08</span> <span class="token number">09</span> <span class="token comment">// 取 ltIndex 和当前 endIndex 中较小的一个作为新的结尾索引</span> <span class="token number">10</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>ltIndex <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span> <span class="token operator">&&</span> ltIndex <span class="token operator"><</span> endIndex<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">11</span> endIndex <span class="token operator">=</span> ltIndex <span class="token number">12</span> <span class="token punctuation">}</span> <span class="token number">13</span> <span class="token comment">// 取 delimiterIndex 和当前 endIndex 中较小的一个作为新的结尾索引</span> <span class="token number">14</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>delimiterIndex <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span> <span class="token operator">&&</span> delimiterIndex <span class="token operator"><</span> endIndex<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">15</span> endIndex <span class="token operator">=</span> delimiterIndex <span class="token number">16</span> <span class="token punctuation">}</span> <span class="token number">17</span> <span class="token number">18</span> <span class="token comment">// 此时 endIndex 是最终的文本内容的结尾索引,调用 slice 函数截取文本内容</span> <span class="token number">19</span> <span class="token keyword">const</span> content <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> endIndex<span class="token punctuation">)</span> <span class="token number">20</span> <span class="token comment">// 消耗文本内容</span> <span class="token number">21</span> context<span class="token punctuation">.</span><span class="token function">advanceBy</span><span class="token punctuation">(</span>content<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">22</span> <span class="token number">23</span> <span class="token comment">// 返回文本节点</span> <span class="token number">24</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> <span class="token number">25</span> <span class="token comment">// 节点类型</span> <span class="token number">26</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Text'</span><span class="token punctuation">,</span> <span class="token number">27</span> <span class="token comment">// 文本内容</span> <span class="token number">28</span> content <span class="token number">29</span> <span class="token punctuation">}</span> <span class="token number">30</span> <span class="token punctuation">}</span> </code></pre> <p>如上面的代码所示,由于字符 < 与定界符 {{ 的出现顺序是未知的,所以我们需要取两者中较小的一个作为文本截取的终点。有了截取终点后,只需要调用字符串的 slice 函数对字符串进行截取即可,截取出来的内容就是文本节点的文本内容。最后,我们创建一个类型为 Text 的文本节点,将其作为 parseText 函数的返回值。</p> <p>配合上述 parseText 函数解析如下模板:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> ast <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><div>Text</div></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span> </code></pre> <p>得到如下 AST:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> ast <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Root'</span><span class="token punctuation">,</span> <span class="token number">03</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token number">04</span> <span class="token punctuation">{</span> <span class="token number">05</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Element'</span><span class="token punctuation">,</span> <span class="token number">06</span> <span class="token literal-property property">tag</span><span class="token operator">:</span> <span class="token string">'div'</span><span class="token punctuation">,</span> <span class="token number">07</span> <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">08</span> <span class="token literal-property property">isSelfClosing</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token number">09</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token number">10</span> <span class="token comment">// 文本节点</span> <span class="token number">11</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Text'</span><span class="token punctuation">,</span> <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token string">'Text'</span> <span class="token punctuation">}</span> <span class="token number">12</span> <span class="token punctuation">]</span> <span class="token number">13</span> <span class="token punctuation">}</span> <span class="token number">14</span> <span class="token punctuation">]</span> <span class="token number">15</span> <span class="token punctuation">}</span> </code></pre> <p>这样,我们就实现了对文本节点的解析。解析文本节点本身并不复杂,复杂点在于,我们需要对解析后的文本内容进行HTML 实体的解码工作。为此,我们有必要先了解什么是HTML 实体。</p> <h3>6.2、解码命名字符引用</h3> <p>HTML 实体是一段以字符 & 开始的文本内容。实体用来描述HTML 中的保留字符和一些难以通过普通键盘输入的字符,以及一些不可见的字符。例如,在 HTML 中,字符 < 具有特殊含义,如果希望以普通文本的方式来显示字符 <,需要通过实体来表达:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>div<span class="token operator">></span><span class="token constant">A</span><span class="token operator">&</span>lt<span class="token punctuation">;</span><span class="token constant">B</span><span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> </code></pre> <p>其中字符串 <code><</code> 就是一个 HTML 实体,用来表示字符 <。如果我们不用 HTML 实体,而是直接使用字符 <,那么将会产生非法的 HTML 内容:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>div<span class="token operator">></span><span class="token constant">A</span><span class="token operator"><</span><span class="token constant">B</span><span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> </code></pre> <p>这会导致浏览器的解析结果不符合预期。</p> <p>HTML 实体总是以字符 & 开头,以字符 ; 结尾。在 Web 诞生的初期,HTML 实体的数量较少,因此允许省略其中的尾分号。但随着 HTML 字符集越来越大,HTML 实体出现了包含的情况,例如 < 和 <cc 都是合法的实体,如果不加分号,浏览器将无法区分它们。因此,WHATWG 规范中明确规定,如果不为实体加分号,将会产生解析错误。但考虑到历史原因(互联网上存在大量省略分号的情况),现代浏览器都能够解析早期规范中定义的那些可以省略分号的 HTML 实体。</p> <p>HTML 实体有两类,一类叫作命名字符引用(named character reference),也叫命名实体(named entity),顾名思义,这类实体具有特定的名称,例如上文中的 <code><</code>。WHATWG 规范中给出了全部的命名字符引用,有 2000 多个,可以通过命名字符引用表查询。下面列出了部分内容:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token comment">// 共 2000+</span> <span class="token number">02</span> <span class="token punctuation">{</span> <span class="token number">03</span> <span class="token string">"GT"</span><span class="token operator">:</span> <span class="token string">">"</span><span class="token punctuation">,</span> <span class="token number">04</span> <span class="token string">"gt"</span><span class="token operator">:</span> <span class="token string">">"</span><span class="token punctuation">,</span> <span class="token number">05</span> <span class="token string">"LT"</span><span class="token operator">:</span> <span class="token string">"<"</span><span class="token punctuation">,</span> <span class="token number">06</span> <span class="token string">"lt"</span><span class="token operator">:</span> <span class="token string">"<"</span><span class="token punctuation">,</span> <span class="token number">07</span> <span class="token comment">// 省略部分代码</span> <span class="token number">08</span> <span class="token string">"awint;"</span><span class="token operator">:</span> <span class="token string">"⨑"</span><span class="token punctuation">,</span> <span class="token number">09</span> <span class="token string">"bcong;"</span><span class="token operator">:</span> <span class="token string">"≌"</span><span class="token punctuation">,</span> <span class="token number">10</span> <span class="token string">"bdquo;"</span><span class="token operator">:</span> <span class="token string">"„"</span><span class="token punctuation">,</span> <span class="token number">11</span> <span class="token string">"bepsi;"</span><span class="token operator">:</span> <span class="token string">"϶"</span><span class="token punctuation">,</span> <span class="token number">12</span> <span class="token string">"blank;"</span><span class="token operator">:</span> <span class="token string">"␣"</span><span class="token punctuation">,</span> <span class="token number">13</span> <span class="token string">"blk12;"</span><span class="token operator">:</span> <span class="token string">"▒"</span><span class="token punctuation">,</span> <span class="token number">14</span> <span class="token string">"blk14;"</span><span class="token operator">:</span> <span class="token string">"░"</span><span class="token punctuation">,</span> <span class="token number">15</span> <span class="token string">"blk34;"</span><span class="token operator">:</span> <span class="token string">"▓"</span><span class="token punctuation">,</span> <span class="token number">16</span> <span class="token string">"block;"</span><span class="token operator">:</span> <span class="token string">"█"</span><span class="token punctuation">,</span> <span class="token number">17</span> <span class="token string">"boxDL;"</span><span class="token operator">:</span> <span class="token string">"╗"</span><span class="token punctuation">,</span> <span class="token number">18</span> <span class="token string">"boxDl;"</span><span class="token operator">:</span> <span class="token string">"╖"</span><span class="token punctuation">,</span> <span class="token number">19</span> <span class="token string">"boxdL;"</span><span class="token operator">:</span> <span class="token string">"╕"</span><span class="token punctuation">,</span> <span class="token number">20</span> <span class="token comment">// 省略部分代码</span> <span class="token number">21</span> <span class="token punctuation">}</span> </code></pre> <p>除了命名字符引用之外,还有一类字符引用没有特定的名称,只能用数字表示,这类实体叫作数字字符引用(numeric character reference)。与命名字符引用不同,数字字符引用以字符串 <code>&#</code> 开头,比命名字符引用的开头部分多出了字符#,例如 <code><</code>;。实际上,<code><</code> 对应的字符也是 <,换句话说,<code><</code> 与 <code><</code> 是等价的。数字字符引用既可以用十进制来表示,也可以使用十六进制来表示。例如,十进制数字 60 对应的十六进制值为 3c,因此实体 <code><</code> 也可以表示为 <code><</code>。可以看到,当使用十六进制数表示实体时,需要以字符串 <code>&#x</code> 开头。</p> <p>理解了 HTML 实体后,我们再来讨论为什么 Vue.js 模板的解析器要对文本节点中的 HTML 实体进行解码。为了理解这个问题,我们需要先明白一个大前提:在 Vue.js 模板中,文本节点所包含的 HTML 实体不会被浏览器解析。这是因为模板中的文本节点最终将通过如 el.textContent 等文本操作方法设置到页面,而通过 el.textContent 设置的文本内容是不会经过 HTML 实体解码的,例如:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> el<span class="token punctuation">.</span>textContent <span class="token operator">=</span> <span class="token string">'<'</span> </code></pre> <p>最终 el 的文本内容将会原封不动地呈现为字符串 '<code><'</code>,而不会呈现字符 <。这就意味着,如果用户在 Vue.js 模板中编写了HTML 实体,而模板解析器不对其进行解码,那么最终渲染到页面的内容将不符合用户的预期。因此,我们应该在解析阶段对文本节点中存在的 HTML 实体进行解码。</p> <p>模板解析器的解码行为应该与浏览器的行为一致。因此,我们应该按照 WHATWG 规范实现解码逻辑。规范中明确定义了解码 HTML 实体时状态机的状态迁移流程。下图给出了简化版的状态迁移流程,我们会在后文中对其进行补充:<br> <a href="http://img.e-com-net.com/image/info8/ea84f90ffaba4fe9bdccd171eb2a022e.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/ea84f90ffaba4fe9bdccd171eb2a022e.jpg" alt="Vue解析器_第22张图片" width="650" height="366" style="border:1px solid black;"></a><br> 假定状态机当前处于初始的 DATA 模式。由上图可知,当解析器遇到字符 & 时,会进入“字符引用状态”,并消费字符&,接着解析下一个字符。如果下一个字符是 ASCII 字母或数字(ASCII alphanumeric),则进入“命名字符引用状态”,其中 ASCII 字母或数字指的是 0~9 这十个数字以及字符集合a~z 再加上字符集合 A~Z。当然,如果下一个字符是 #,则进入“数字字符引用状态”。</p> <p>一旦状态机进入命名字符引用状态,解析器将会执行比较复杂的匹配流程。我们通过几个例子来直观地感受一下这个过程。假设文本内容为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> a<span class="token operator">&</span>ltb </code></pre> <p>上面这段文本会被解析为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> a<span class="token operator"><</span>b </code></pre> <p>为什么会得到这样的解析结果呢?接下来,我们分析整个解析过程:</p> <ul> <li>首先,当解析器遇到字符 & 时,会进入字符引用状态。接着,解析下一个字符 l,这会使得解析器进入命名字符引用状态,并在命名字符引用表(后文简称“引用表”)中查找以字符 l 开头的项。由于引用表中存在诸多以字符 l 开头的项,例如lt、lg、le 等,因此解析器认为此时是“匹配”的。</li> <li>于是开始解析下一个字符 t,并尝试去引用表中查找以 lt 开头的项。由于引用表中也存在多个以 lt 开头的项,例如 lt、ltcc;、ltri; 等,因此解析器认为此时也是“匹配”的。</li> <li>于是又开始解析下一个字符 b,并尝试去引用表中查找以 ltb 开头的项,结果发现引用表中不存在符合条件的项,至此匹配结束。</li> </ul> <p>当匹配结束时,解析器会检查最后一个匹配的字符。如果该字符是分号(;),则会产生一个合法的匹配,并渲染对应字符。但在上例中,最后一个匹配的字符是字符 t,并不是分号(;),因此会产生一个解析错误,但由于历史原因,浏览器仍然能够解析它。在这种情况下,浏览器的解析规则是:最短原则。其中“最短”指的是命名字符引用的名称最短。举个例子,假设文本内容为:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> a<span class="token operator">&</span>ltcc<span class="token punctuation">;</span> </code></pre> <p>我们知道 <code>⪦</code> 是一个合法的命名字符引用,因此上述文本会被渲染为:a⪦。但如果去掉上述文本中的分号,即:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> a<span class="token operator">&</span>ltcc </code></pre> <p>解析器在处理这段文本中的实体时,最后匹配的字符将不再是分号,而是字符 c。按照“最短原则”,解析器只会渲染名称更短的字符引用。在字符串 <cc 中,< 的名称要短于 <cc,因此最终会将 < 作为合法的字符引用来渲染,而字符串 cc 将作为普通字符来渲染。所以上面的文本最终会被渲染为:a<cc。</p> <p>需要说明的是,上述解析过程仅限于不用作属性值的普通文本。换句话说,用作属性值的文本会有不同的解析规则。举例来说,给出如下 HTML 文本:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token operator"><</span>a href<span class="token operator">=</span><span class="token string">"foo.com?a=1<=2"</span><span class="token operator">></span>foo<span class="token punctuation">.</span>com<span class="token operator">?</span>a<span class="token operator">=</span><span class="token number">1</span><span class="token operator">&</span>lt<span class="token operator">=</span><span class="token number">2</span><span class="token operator"><</span><span class="token operator">/</span>a<span class="token operator">></span> </code></pre> <p>可以看到,a 标签的 href 属性值与它的文本子节点具有同样的内容,但它们被解析之后的结果不同。其中属性值中出现的 < 将原封不动地展示,而文本子节点中出现的 < 将会被解析为字符 <。这也是符合期望的,很明显,<=2 将构成链接中的查询参数,如果将其中的 < 解码为字符 <,将会破坏用户的URL。实际上,WHATWG 规范中对此也有完整的定义,出于历史原因的考虑,对于属性值中的字符引用,如果最后一个匹配的字符不是分号,并且该匹配的字符的下一个字符是等于号、ASCII 字母或数字,那么该匹配项将作为普通文本被解析。</p> <p>明白了原理,我们就着手实现。我们面临的第一个问题是,如何处理省略分号的情况?关于字符引用中的分号,我们可以总结如下:</p> <ul> <li>当存在分号时:执行完整匹配。</li> <li>当省略分号时:执行最短匹配。</li> </ul> <p>为此,我们需要精心设计命名字符引用表。由于命名字符引用的数量非常多,因此这里我们只取其中一部分作为命名字符引用表的内容,如下面的代码所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> namedCharacterReferences <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token string">"gt"</span><span class="token operator">:</span> <span class="token string">">"</span><span class="token punctuation">,</span> <span class="token number">03</span> <span class="token string">"gt;"</span><span class="token operator">:</span> <span class="token string">">"</span><span class="token punctuation">,</span> <span class="token number">04</span> <span class="token string">"lt"</span><span class="token operator">:</span> <span class="token string">"<"</span><span class="token punctuation">,</span> <span class="token number">05</span> <span class="token string">"lt;"</span><span class="token operator">:</span> <span class="token string">"<"</span><span class="token punctuation">,</span> <span class="token number">06</span> <span class="token string">"ltcc;"</span><span class="token operator">:</span> <span class="token string">"⪦"</span> <span class="token number">07</span> <span class="token punctuation">}</span> </code></pre> <p>上面这张表是经过精心设计的。观察namedCharacterReferences 对象可以发现,相同的字符对应的实体会有多个,即带分号的版本和不带分号的版本,例如"gt" 和 “gt;”。另外一些实体则只有带分号的版本,因为这些实体不允许省略分号,例如 “ltcc;”。我们可以根据这张表来实现实体的解码逻辑。假设我们有如下文本内容:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> a<span class="token operator">&</span>ltccbbb </code></pre> <p>在解码这段文本时,我们首先根据字符 & 将文本分为两部分:</p> <ul> <li>一部分是普通文本:a。</li> <li>另一部分则是:<ccbbb。</li> </ul> <p>对于普通文本部分,由于它不需要被解码,因此索引原封不动地保留。而对于可能是字符引用的部分,执行解码工作:</p> <ul> <li>第一步:计算出命名字符引用表中实体名称的最大长度。由于在 namedCharacterReferences 对象中,名称最长的实体是ltcc;,它具有 5 个字符,因此最大长度是 5。</li> <li>第二步:根据最大长度截取字符串 ltccbbb,即’ltccbbb’.slice(0, 5),最终结果是:‘ltccb’</li> <li>第三步:用截取后的字符串 ‘ltccb’ 作为键去命名字符引用表中查询对应的值,即解码。由于引用表namedCharacterReferences 中不存在键值为 ‘ltccb’ 的项,因此不匹配。</li> <li>第四步:当发现不匹配时,我们将最大长度减 1,并重新执行第二步,直到找到匹配项为止。在上面这个例子中,最终的匹配项将会是 ‘lt’。因此,上述文本最终会被解码为:</li> </ul> <pre><code class="prism language-javascript"><span class="token number">01</span> a<span class="token operator"><</span>ccbbb </code></pre> <p>这样,我们就实现了当字符引用省略分号时按照“最短原则”进行解码。</p> <p>下面的 decodeHtml 函数给出了具体实现:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token comment">// 第一个参数为要被解码的文本内容</span> <span class="token number">02</span> <span class="token comment">// 第二个参数是一个布尔值,代表文本内容是否作为属性值</span> <span class="token number">03</span> <span class="token keyword">function</span> <span class="token function">decodeHtml</span><span class="token punctuation">(</span><span class="token parameter">rawText<span class="token punctuation">,</span> asAttr <span class="token operator">=</span> <span class="token boolean">false</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">04</span> <span class="token keyword">let</span> offset <span class="token operator">=</span> <span class="token number">0</span> <span class="token number">05</span> <span class="token keyword">const</span> end <span class="token operator">=</span> rawText<span class="token punctuation">.</span>length <span class="token number">06</span> <span class="token comment">// 经过解码后的文本将作为返回值被返回</span> <span class="token number">07</span> <span class="token keyword">let</span> decodedText <span class="token operator">=</span> <span class="token string">''</span> <span class="token number">08</span> <span class="token comment">// 引用表中实体名称的最大长度</span> <span class="token number">09</span> <span class="token keyword">let</span> maxCRNameLength <span class="token operator">=</span> <span class="token number">0</span> <span class="token number">10</span> <span class="token number">11</span> <span class="token comment">// advance 函数用于消费指定长度的文本</span> <span class="token number">12</span> <span class="token keyword">function</span> <span class="token function">advance</span><span class="token punctuation">(</span><span class="token parameter">length</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">13</span> offset <span class="token operator">+=</span> length <span class="token number">14</span> rawText <span class="token operator">=</span> rawText<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span>length<span class="token punctuation">)</span> <span class="token number">15</span> <span class="token punctuation">}</span> <span class="token number">16</span> <span class="token number">17</span> <span class="token comment">// 消费字符串,直到处理完毕为止</span> <span class="token number">18</span> <span class="token keyword">while</span> <span class="token punctuation">(</span>offset <span class="token operator"><</span> end<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">19</span> <span class="token comment">// 用于匹配字符引用的开始部分,如果匹配成功,那么 head[0] 的值将有三种可能:</span> <span class="token number">20</span> <span class="token comment">// 1. head[0] === '&',这说明该字符引用是命名字符引用</span> <span class="token number">21</span> <span class="token comment">// 2. head[0] === '&#',这说明该字符引用是用十进制表示的数字字符引用</span> <span class="token number">22</span> <span class="token comment">// 3. head[0] === '&#x',这说明该字符引用是用十六进制表示的数字字符引用</span> <span class="token number">23</span> <span class="token keyword">const</span> head <span class="token operator">=</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">&(?:#x?)?</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>rawText<span class="token punctuation">)</span> <span class="token number">24</span> <span class="token comment">// 如果没有匹配,说明已经没有需要解码的内容了</span> <span class="token number">25</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>head<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">26</span> <span class="token comment">// 计算剩余内容的长度</span> <span class="token number">27</span> <span class="token keyword">const</span> remaining <span class="token operator">=</span> end <span class="token operator">-</span> offset <span class="token number">28</span> <span class="token comment">// 将剩余内容加到 decodedText 上</span> <span class="token number">29</span> decodedText <span class="token operator">+=</span> rawText<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> remaining<span class="token punctuation">)</span> <span class="token number">30</span> <span class="token comment">// 消费剩余内容</span> <span class="token number">31</span> <span class="token function">advance</span><span class="token punctuation">(</span>remaining<span class="token punctuation">)</span> <span class="token number">32</span> <span class="token keyword">break</span> <span class="token number">33</span> <span class="token punctuation">}</span> <span class="token number">34</span> <span class="token number">35</span> <span class="token comment">// head.index 为匹配的字符 & 在 rawText 中的位置索引</span> <span class="token number">36</span> <span class="token comment">// 截取字符 & 之前的内容加到 decodedText 上</span> <span class="token number">37</span> decodedText <span class="token operator">+=</span> rawText<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> head<span class="token punctuation">.</span>index<span class="token punctuation">)</span> <span class="token number">38</span> <span class="token comment">// 消费字符 & 之前的内容</span> <span class="token number">39</span> <span class="token function">advance</span><span class="token punctuation">(</span>head<span class="token punctuation">.</span>index<span class="token punctuation">)</span> <span class="token number">40</span> <span class="token number">41</span> <span class="token comment">// 如果满足条件,则说明是命名字符引用,否则为数字字符引用</span> <span class="token number">42</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>head<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">'&'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">43</span> <span class="token keyword">let</span> name <span class="token operator">=</span> <span class="token string">''</span> <span class="token number">44</span> <span class="token keyword">let</span> value <span class="token number">45</span> <span class="token comment">// 字符 & 的下一个字符必须是 ASCII 字母或数字,这样才是合法的命名字符引用</span> <span class="token number">46</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">[0-9a-z]</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">.</span><span class="token function">test</span><span class="token punctuation">(</span>rawText<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">47</span> <span class="token comment">// 根据引用表计算实体名称的最大长度,</span> <span class="token number">48</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>maxCRNameLength<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">49</span> maxCRNameLength <span class="token operator">=</span> Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>namedCharacterReferences<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span> <span class="token number">50</span> <span class="token punctuation">(</span><span class="token parameter">max<span class="token punctuation">,</span> name</span><span class="token punctuation">)</span> <span class="token operator">=></span> Math<span class="token punctuation">.</span><span class="token function">max</span><span class="token punctuation">(</span>max<span class="token punctuation">,</span> name<span class="token punctuation">.</span>length<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">51</span> <span class="token number">0</span> <span class="token number">52</span> <span class="token punctuation">)</span> <span class="token number">53</span> <span class="token punctuation">}</span> <span class="token number">54</span> <span class="token comment">// 从最大长度开始对文本进行截取,并试图去引用表中找到对应的项</span> <span class="token number">55</span> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> length <span class="token operator">=</span> maxCRNameLength<span class="token punctuation">;</span> <span class="token operator">!</span>value <span class="token operator">&&</span> length <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">;</span> <span class="token operator">--</span>length<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">56</span> <span class="token comment">// 截取字符 & 到最大长度之间的字符作为实体名称</span> <span class="token number">57</span> name <span class="token operator">=</span> rawText<span class="token punctuation">.</span><span class="token function">substr</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> length<span class="token punctuation">)</span> <span class="token number">58</span> <span class="token comment">// 使用实体名称去索引表中查找对应项的值</span> <span class="token number">59</span> value <span class="token operator">=</span> <span class="token punctuation">(</span>namedCharacterReferences<span class="token punctuation">)</span><span class="token punctuation">[</span>name<span class="token punctuation">]</span> <span class="token number">60</span> <span class="token punctuation">}</span> <span class="token number">61</span> <span class="token comment">// 如果找到了对应项的值,说明解码成功</span> <span class="token number">62</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>value<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">63</span> <span class="token comment">// 检查实体名称的最后一个匹配字符是否是分号</span> <span class="token number">64</span> <span class="token keyword">const</span> semi <span class="token operator">=</span> name<span class="token punctuation">.</span><span class="token function">endsWith</span><span class="token punctuation">(</span><span class="token string">';'</span><span class="token punctuation">)</span> <span class="token number">65</span> <span class="token comment">// 如果解码的文本作为属性值,最后一个匹配的字符不是分号,</span> <span class="token number">66</span> <span class="token comment">// 并且最后一个匹配字符的下一个字符是等于号(=)、ASCII 字母或数字,</span> <span class="token number">67</span> <span class="token comment">// 由于历史原因,将字符 & 和实体名称 name 作为普通文本</span> <span class="token number">68</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token number">69</span> asAttr <span class="token operator">&&</span> <span class="token number">70</span> <span class="token operator">!</span>semi <span class="token operator">&&</span> <span class="token number">71</span> <span class="token operator">/</span><span class="token punctuation">[</span><span class="token operator">=</span>a<span class="token operator">-</span>z0<span class="token operator">-</span><span class="token number">9</span><span class="token punctuation">]</span><span class="token operator">/</span>i<span class="token punctuation">.</span><span class="token function">test</span><span class="token punctuation">(</span>rawText<span class="token punctuation">[</span>name<span class="token punctuation">.</span>length <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">||</span> <span class="token string">''</span><span class="token punctuation">)</span> <span class="token number">72</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">73</span> decodedText <span class="token operator">+=</span> <span class="token string">'&'</span> <span class="token operator">+</span> name <span class="token number">74</span> <span class="token function">advance</span><span class="token punctuation">(</span><span class="token number">1</span> <span class="token operator">+</span> name<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">75</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token number">76</span> <span class="token comment">// 其他情况下,正常使用解码后的内容拼接到 decodedText 上</span> <span class="token number">77</span> decodedText <span class="token operator">+=</span> value <span class="token number">78</span> <span class="token function">advance</span><span class="token punctuation">(</span><span class="token number">1</span> <span class="token operator">+</span> name<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">79</span> <span class="token punctuation">}</span> <span class="token number">80</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token number">81</span> <span class="token comment">// 如果没有找到对应的值,说明解码失败</span> <span class="token number">82</span> decodedText <span class="token operator">+=</span> <span class="token string">'&'</span> <span class="token operator">+</span> name <span class="token number">83</span> <span class="token function">advance</span><span class="token punctuation">(</span><span class="token number">1</span> <span class="token operator">+</span> name<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">84</span> <span class="token punctuation">}</span> <span class="token number">85</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token number">86</span> <span class="token comment">// 如果字符 & 的下一个字符不是 ASCII 字母或数字,则将字符 & 作为普通文本</span> <span class="token number">87</span> decodedText <span class="token operator">+=</span> <span class="token string">'&'</span> <span class="token number">88</span> <span class="token function">advance</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token number">89</span> <span class="token punctuation">}</span> <span class="token number">90</span> <span class="token punctuation">}</span> <span class="token number">91</span> <span class="token punctuation">}</span> <span class="token number">92</span> <span class="token keyword">return</span> decodedText <span class="token number">93</span> <span class="token punctuation">}</span> </code></pre> <p>有了 decodeHtml 函数之后,我们就可以在解析文本节点时通过它对文本内容进行解码:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseText</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 省略部分代码</span> <span class="token number">03</span> <span class="token number">04</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> <span class="token number">05</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Text'</span><span class="token punctuation">,</span> <span class="token number">06</span> <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token function">decodeHtml</span><span class="token punctuation">(</span>content<span class="token punctuation">)</span> <span class="token comment">// 调用 decodeHtml 函数解码内容</span> <span class="token number">07</span> <span class="token punctuation">}</span> <span class="token number">08</span> <span class="token punctuation">}</span> </code></pre> <h3>6.3、解码数字字符引用</h3> <p>在上一节中,我们使用下面的正则表达式来匹配一个文本中字符引用的开始部分:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> head <span class="token operator">=</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">&(?:#x?)?</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>rawText<span class="token punctuation">)</span> </code></pre> <p>我们可以根据该正则的匹配结果,来判断字符引用的类型:</p> <p>-如果 <code>head[0] === '&',则说明匹配的是命名字符引用。●如果 head[0] === '&#'</code>,则说明匹配的是以十进制表示的数字字符引用。</p> <p>-如果 <code>head[0] === '&#x'</code>,则说明匹配的是以十六进制表示的数字字符引用。</p> <ul> <li>如果 <code>head[0] === '&#x</code>’,则说明匹配的是以十六进制表示的数字字符引用。</li> </ul> <p>数字字符引用的格式是:前缀 + Unicode 码点。解码数字字符引用的关键在于,如何提取字符引用中的 Unicode 码点。考虑到数字字符引用的前缀可以是以十进制表示(&#),也可以是以十六进制表示(&#x),所以我们使用下面的代码来完成码点的提取:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token comment">// 判断是以十进制表示还是以十六进制表示</span> <span class="token number">02</span> <span class="token keyword">const</span> hex <span class="token operator">=</span> head<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">'&#x'</span> <span class="token number">03</span> <span class="token comment">// 根据不同进制表示法,选用不同的正则</span> <span class="token number">04</span> <span class="token keyword">const</span> pattern <span class="token operator">=</span> hex <span class="token operator">?</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^&#x([0-9a-f]+);?</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span> <span class="token operator">:</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^&#([0-9]+);?</span><span class="token regex-delimiter">/</span></span> <span class="token number">05</span> <span class="token comment">// 最终,body[1] 的值就是 Unicode 码点</span> <span class="token number">06</span> <span class="token keyword">const</span> body <span class="token operator">=</span> pattern<span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>rawText<span class="token punctuation">)</span> </code></pre> <p>有了 Unicode 码点之后,只需要调用 String.fromCodePoint 函数即可将其解码为对应的字符:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>body<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 根据对应的进制,将码点字符串转换为数字</span> <span class="token number">03</span> <span class="token keyword">const</span> cp <span class="token operator">=</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>body<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> hex <span class="token operator">?</span> <span class="token number">16</span> <span class="token operator">:</span> <span class="token number">10</span><span class="token punctuation">)</span> <span class="token number">04</span> <span class="token comment">// 解码</span> <span class="token number">05</span> <span class="token keyword">const</span> char <span class="token operator">=</span> String<span class="token punctuation">.</span><span class="token function">fromCodePoint</span><span class="token punctuation">(</span>cp<span class="token punctuation">)</span> <span class="token number">06</span> <span class="token punctuation">}</span> </code></pre> <p>不过,在真正进行解码前,需要对码点的值进行合法性检查。WHATWG 规范中对此也有明确的定义:</p> <ul> <li>如果码点值为 0x00,即十进制的数字 0,它在 Unicode 中代表空字符(NULL),这将是一个解析错误,解析器会将码点值替换为 0xFFFD。</li> <li>如果码点值大于 0x10FFFF(0x10FFFF 为 Unicode 的最大值),这也是一个解析错误,解析器会将码点值替换为0xFFFD。</li> <li>如果码点值处于代理对(surrogate pair)范围内,这也是一个解析错误,解析器会将码点值替换为 0xFFFD,其中surrogate pair 是预留给 UTF-16 的码位,其范围是:[0xD800, 0xDFFF]。</li> <li>如果码点值是 noncharacter,这也是一个解析错误,但什么都不需要做。这里的 noncharacter 代表 Unicode 永久保留的码点,用于 Unicode 内部,它的取值范围是:[0xFDD0,0xFDEF],还包括:0xFFFE、0xFFFF、0x1FFFE、0x1FFFF、0x2FFFE、0x2FFFF、0x3FFFE、0x3FFFF、0x4FFFE、0x4FFFF、0x5FFFE、0x5FFFF、0x6FFFE、0x6FFFF、0x7FFFE、0x7FFFF、0x8FFFE、0x8FFFF、0x9FFFE、0x9FFFF、0xAFFFE、0xAFFFF、0xBFFFE、0xBFFFF、0xCFFFE、0xCFFFF、0xDFFFE、0xDFFFF、0xEFFFE、0xEFFFF、0xFFFFE、0xFFFFF、0x10FFFE、0x10FFFF。</li> </ul> <p>如果码点值对应的字符是回车符(0x0D),或者码点值为控制字符集(control character)中的非 ASCII 空白符(ASCII whitespace),则是一个解析错误。这时需要将码点作为索引,在下表中查找对应的替换码点:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> <span class="token constant">CCR_REPLACEMENTS</span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token number">0x80</span><span class="token operator">:</span> <span class="token number">0x20ac</span><span class="token punctuation">,</span> <span class="token number">03</span> <span class="token number">0x82</span><span class="token operator">:</span> <span class="token number">0x201a</span><span class="token punctuation">,</span> <span class="token number">04</span> <span class="token number">0x83</span><span class="token operator">:</span> <span class="token number">0x0192</span><span class="token punctuation">,</span> <span class="token number">05</span> <span class="token number">0x84</span><span class="token operator">:</span> <span class="token number">0x201e</span><span class="token punctuation">,</span> <span class="token number">06</span> <span class="token number">0x85</span><span class="token operator">:</span> <span class="token number">0x2026</span><span class="token punctuation">,</span> <span class="token number">07</span> <span class="token number">0x86</span><span class="token operator">:</span> <span class="token number">0x2020</span><span class="token punctuation">,</span> <span class="token number">08</span> <span class="token number">0x87</span><span class="token operator">:</span> <span class="token number">0x2021</span><span class="token punctuation">,</span> <span class="token number">09</span> <span class="token number">0x88</span><span class="token operator">:</span> <span class="token number">0x02c6</span><span class="token punctuation">,</span> <span class="token number">10</span> <span class="token number">0x89</span><span class="token operator">:</span> <span class="token number">0x2030</span><span class="token punctuation">,</span> <span class="token number">11</span> <span class="token number">0x8a</span><span class="token operator">:</span> <span class="token number">0x0160</span><span class="token punctuation">,</span> <span class="token number">12</span> <span class="token number">0x8b</span><span class="token operator">:</span> <span class="token number">0x2039</span><span class="token punctuation">,</span> <span class="token number">13</span> <span class="token number">0x8c</span><span class="token operator">:</span> <span class="token number">0x0152</span><span class="token punctuation">,</span> <span class="token number">14</span> <span class="token number">0x8e</span><span class="token operator">:</span> <span class="token number">0x017d</span><span class="token punctuation">,</span> <span class="token number">15</span> <span class="token number">0x91</span><span class="token operator">:</span> <span class="token number">0x2018</span><span class="token punctuation">,</span> <span class="token number">16</span> <span class="token number">0x92</span><span class="token operator">:</span> <span class="token number">0x2019</span><span class="token punctuation">,</span> <span class="token number">17</span> <span class="token number">0x93</span><span class="token operator">:</span> <span class="token number">0x201c</span><span class="token punctuation">,</span> <span class="token number">18</span> <span class="token number">0x94</span><span class="token operator">:</span> <span class="token number">0x201d</span><span class="token punctuation">,</span> <span class="token number">19</span> <span class="token number">0x95</span><span class="token operator">:</span> <span class="token number">0x2022</span><span class="token punctuation">,</span> <span class="token number">20</span> <span class="token number">0x96</span><span class="token operator">:</span> <span class="token number">0x2013</span><span class="token punctuation">,</span> <span class="token number">21</span> <span class="token number">0x97</span><span class="token operator">:</span> <span class="token number">0x2014</span><span class="token punctuation">,</span> <span class="token number">22</span> <span class="token number">0x98</span><span class="token operator">:</span> <span class="token number">0x02dc</span><span class="token punctuation">,</span> <span class="token number">23</span> <span class="token number">0x99</span><span class="token operator">:</span> <span class="token number">0x2122</span><span class="token punctuation">,</span> <span class="token number">24</span> <span class="token number">0x9a</span><span class="token operator">:</span> <span class="token number">0x0161</span><span class="token punctuation">,</span> <span class="token number">25</span> <span class="token number">0x9b</span><span class="token operator">:</span> <span class="token number">0x203a</span><span class="token punctuation">,</span> <span class="token number">26</span> <span class="token number">0x9c</span><span class="token operator">:</span> <span class="token number">0x0153</span><span class="token punctuation">,</span> <span class="token number">27</span> <span class="token number">0x9e</span><span class="token operator">:</span> <span class="token number">0x017e</span><span class="token punctuation">,</span> <span class="token number">28</span> <span class="token number">0x9f</span><span class="token operator">:</span> <span class="token number">0x0178</span> <span class="token number">29</span> <span class="token punctuation">}</span> </code></pre> <p>如果存在对应的替换码点,则渲染该替换码点对应的字符,否则直接渲染原码点对应的字符。</p> <p>上述关于码点合法性检查的具体实现如下:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>body<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 根据对应的进制,将码点字符串转换为数字</span> <span class="token number">03</span> <span class="token keyword">const</span> cp <span class="token operator">=</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>body<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> hex <span class="token operator">?</span> <span class="token number">16</span> <span class="token operator">:</span> <span class="token number">10</span><span class="token punctuation">)</span> <span class="token number">04</span> <span class="token comment">// 检查码点的合法性</span> <span class="token number">05</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>cp <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">06</span> <span class="token comment">// 如果码点值为 0x00,替换为 0xfffd</span> <span class="token number">07</span> cp <span class="token operator">=</span> <span class="token number">0xfffd</span> <span class="token number">08</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>cp <span class="token operator">></span> <span class="token number">0x10ffff</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">09</span> <span class="token comment">// 如果码点值超过 Unicode 的最大值,替换为 0xfffd</span> <span class="token number">10</span> cp <span class="token operator">=</span> <span class="token number">0xfffd</span> <span class="token number">11</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>cp <span class="token operator">>=</span> <span class="token number">0xd800</span> <span class="token operator">&&</span> cp <span class="token operator"><=</span> <span class="token number">0xdfff</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">12</span> <span class="token comment">// 如果码点值处于 surrogate pair 范围内,替换为 0xfffd</span> <span class="token number">13</span> cp <span class="token operator">=</span> <span class="token number">0xfffd</span> <span class="token number">14</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>cp <span class="token operator">>=</span> <span class="token number">0xfdd0</span> <span class="token operator">&&</span> cp <span class="token operator"><=</span> <span class="token number">0xfdef</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token punctuation">(</span>cp <span class="token operator">&</span> <span class="token number">0xfffe</span><span class="token punctuation">)</span> <span class="token operator">===</span> <span class="token number">0xfffe</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">15</span> <span class="token comment">// 如果码点值处于 noncharacter 范围内,则什么都不做,交给平台处理</span> <span class="token number">16</span> <span class="token comment">// noop</span> <span class="token number">17</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token number">18</span> <span class="token comment">// 控制字符集的范围是:[0x01, 0x1f] 加上 [0x7f, 0x9f]</span> <span class="token number">19</span> <span class="token comment">// 去掉 ASICC 空白符:0x09(TAB)、0x0A(LF)、0x0C(FF)</span> <span class="token number">20</span> <span class="token comment">// 0x0D(CR) 虽然也是 ASICC 空白符,但需要包含</span> <span class="token number">21</span> <span class="token punctuation">(</span>cp <span class="token operator">>=</span> <span class="token number">0x01</span> <span class="token operator">&&</span> cp <span class="token operator"><=</span> <span class="token number">0x08</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token number">22</span> cp <span class="token operator">===</span> <span class="token number">0x0b</span> <span class="token operator">||</span> <span class="token number">23</span> <span class="token punctuation">(</span>cp <span class="token operator">>=</span> <span class="token number">0x0d</span> <span class="token operator">&&</span> cp <span class="token operator"><=</span> <span class="token number">0x1f</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token number">24</span> <span class="token punctuation">(</span>cp <span class="token operator">>=</span> <span class="token number">0x7f</span> <span class="token operator">&&</span> cp <span class="token operator"><=</span> <span class="token number">0x9f</span><span class="token punctuation">)</span> <span class="token number">25</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">26</span> <span class="token comment">// 在 CCR_REPLACEMENTS 表中查找替换码点,如果找不到,则使用原码点</span> <span class="token number">27</span> cp <span class="token operator">=</span> <span class="token constant">CCR_REPLACEMENTS</span><span class="token punctuation">[</span>cp<span class="token punctuation">]</span> <span class="token operator">||</span> cp <span class="token number">28</span> <span class="token punctuation">}</span> <span class="token number">29</span> <span class="token comment">// 最后进行解码</span> <span class="token number">30</span> <span class="token keyword">const</span> char <span class="token operator">=</span> String<span class="token punctuation">.</span><span class="token function">fromCodePoint</span><span class="token punctuation">(</span>cp<span class="token punctuation">)</span> <span class="token number">31</span> <span class="token punctuation">}</span> </code></pre> <p>在上面这段代码中,我们完整地还原了码点合法性检查的逻辑,它有如下几个关键点:</p> <ul> <li>其中控制字符集(control character)的码点范围是:[0x01,0x1f] 和 [0x7f, 0x9f]。这个码点范围包含了 ASCII 空白符:0x09(TAB)、0x0A(LF)、0x0C(FF) 和 0x0D(CR),但WHATWG 规范中要求包含 0x0D(CR)。</li> <li>码点 0xfffd 对应的符号是 �。你一定在出现“乱码”的情况下见过这个字符,它是 Unicode 中的替换字符,通常表示在解码过程中出现“错误”,例如使用了错误的解码方式等。</li> </ul> <p>最后,我们将上述代码整合到 decodeHtml 函数中,这样就实现一个完善的 HTML 文本解码函数:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">decodeHtml</span><span class="token punctuation">(</span><span class="token parameter">rawText<span class="token punctuation">,</span> asAttr <span class="token operator">=</span> <span class="token boolean">false</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 省略部分代码</span> <span class="token number">03</span> <span class="token number">04</span> <span class="token comment">// 消费字符串,直到处理完毕为止</span> <span class="token number">05</span> <span class="token keyword">while</span> <span class="token punctuation">(</span>offset <span class="token operator"><</span> end<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">06</span> <span class="token comment">// 省略部分代码</span> <span class="token number">07</span> <span class="token number">08</span> <span class="token comment">// 如果满足条件,则说明是命名字符引用,否则为数字字符引用</span> <span class="token number">09</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>head<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">'&'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">10</span> <span class="token comment">// 省略部分代码</span> <span class="token number">11</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token number">12</span> <span class="token comment">// 判断是十进制表示还是十六进制表示</span> <span class="token number">13</span> <span class="token keyword">const</span> hex <span class="token operator">=</span> head<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string">'&#x'</span> <span class="token number">14</span> <span class="token comment">// 根据不同进制表示法,选用不同的正则</span> <span class="token number">15</span> <span class="token keyword">const</span> pattern <span class="token operator">=</span> hex <span class="token operator">?</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^&#x([0-9a-f]+);?</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span> <span class="token operator">:</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^&#([0-9]+);?</span><span class="token regex-delimiter">/</span></span> <span class="token number">16</span> <span class="token comment">// 最终,body[1] 的值就是 Unicode 码点</span> <span class="token number">17</span> <span class="token keyword">const</span> body <span class="token operator">=</span> pattern<span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>rawText<span class="token punctuation">)</span> <span class="token number">18</span> <span class="token number">19</span> <span class="token comment">// 如果匹配成功,则调用 String.fromCodePoint 函数进行解码</span> <span class="token number">20</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>body<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">21</span> <span class="token comment">// 根据对应的进制,将码点字符串转换为数字</span> <span class="token number">22</span> <span class="token keyword">const</span> cp <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token function">parseInt</span><span class="token punctuation">(</span>body<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> hex <span class="token operator">?</span> <span class="token number">16</span> <span class="token operator">:</span> <span class="token number">10</span><span class="token punctuation">)</span> <span class="token number">23</span> <span class="token comment">// 码点的合法性检查</span> <span class="token number">24</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>cp <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">25</span> <span class="token comment">// 如果码点值为 0x00,替换为 0xfffd</span> <span class="token number">26</span> cp <span class="token operator">=</span> <span class="token number">0xfffd</span> <span class="token number">27</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>cp <span class="token operator">></span> <span class="token number">0x10ffff</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">28</span> <span class="token comment">// 如果码点值超过 Unicode 的最大值,替换为 0xfffd</span> <span class="token number">29</span> cp <span class="token operator">=</span> <span class="token number">0xfffd</span> <span class="token number">30</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>cp <span class="token operator">>=</span> <span class="token number">0xd800</span> <span class="token operator">&&</span> cp <span class="token operator"><=</span> <span class="token number">0xdfff</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">31</span> <span class="token comment">// 如果码点值处于 surrogate pair 范围内,替换为 0xfffd</span> <span class="token number">32</span> cp <span class="token operator">=</span> <span class="token number">0xfffd</span> <span class="token number">33</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>cp <span class="token operator">>=</span> <span class="token number">0xfdd0</span> <span class="token operator">&&</span> cp <span class="token operator"><=</span> <span class="token number">0xfdef</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token punctuation">(</span>cp <span class="token operator">&</span> <span class="token number">0xfffe</span><span class="token punctuation">)</span> <span class="token operator">===</span> <span class="token number">0xfffe</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">34</span> <span class="token comment">// 如果码点值处于 noncharacter 范围内,则什么都不做,交给平台处理</span> <span class="token number">35</span> <span class="token comment">// noop</span> <span class="token number">36</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token number">37</span> <span class="token comment">// 控制字符集的范围是:[0x01, 0x1f] 加上 [0x7f, 0x9f]</span> <span class="token number">38</span> <span class="token comment">// 去掉 ASICC 空白符:0x09(TAB)、0x0A(LF)、0x0C(FF)</span> <span class="token number">39</span> <span class="token comment">// 0x0D(CR) 虽然也是 ASICC 空白符,但需要包含</span> <span class="token number">40</span> <span class="token punctuation">(</span>cp <span class="token operator">>=</span> <span class="token number">0x01</span> <span class="token operator">&&</span> cp <span class="token operator"><=</span> <span class="token number">0x08</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token number">41</span> cp <span class="token operator">===</span> <span class="token number">0x0b</span> <span class="token operator">||</span> <span class="token number">42</span> <span class="token punctuation">(</span>cp <span class="token operator">>=</span> <span class="token number">0x0d</span> <span class="token operator">&&</span> cp <span class="token operator"><=</span> <span class="token number">0x1f</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token number">43</span> <span class="token punctuation">(</span>cp <span class="token operator">>=</span> <span class="token number">0x7f</span> <span class="token operator">&&</span> cp <span class="token operator"><=</span> <span class="token number">0x9f</span><span class="token punctuation">)</span> <span class="token number">44</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">45</span> <span class="token comment">// 在 CCR_REPLACEMENTS 表中查找替换码点,如果找不到,则使用原码点</span> <span class="token number">46</span> cp <span class="token operator">=</span> <span class="token constant">CCR_REPLACEMENTS</span><span class="token punctuation">[</span>cp<span class="token punctuation">]</span> <span class="token operator">||</span> cp <span class="token number">47</span> <span class="token punctuation">}</span> <span class="token number">48</span> <span class="token comment">// 解码后追加到 decodedText 上</span> <span class="token number">49</span> decodedText <span class="token operator">+=</span> String<span class="token punctuation">.</span><span class="token function">fromCodePoint</span><span class="token punctuation">(</span>cp<span class="token punctuation">)</span> <span class="token number">50</span> <span class="token comment">// 消费整个数字字符引用的内容</span> <span class="token number">51</span> <span class="token function">advance</span><span class="token punctuation">(</span>body<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">52</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token number">53</span> <span class="token comment">// 如果没有匹配,则不进行解码操作,只是把 head[0] 追加到 decodedText 上并消费</span> <span class="token number">54</span> decodedText <span class="token operator">+=</span> head<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token number">55</span> <span class="token function">advance</span><span class="token punctuation">(</span>head<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">56</span> <span class="token punctuation">}</span> <span class="token number">57</span> <span class="token punctuation">}</span> <span class="token number">58</span> <span class="token punctuation">}</span> <span class="token number">59</span> <span class="token keyword">return</span> decodedText <span class="token number">60</span> <span class="token punctuation">}</span> </code></pre> <h2>7、解析插值与注释</h2> <p>文本插值是 Vue.js 模板中用来渲染动态数据的常用方法:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> count <span class="token punctuation">}</span><span class="token punctuation">}</span> </code></pre> <p>默认情况下,插值以字符串 {{ 开头,并以字符串 }} 结尾。我们通常将这两个特殊的字符串称为定界符。定界符中间的内容可以是任意合法的 JavaScript 表达式,例如:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> obj<span class="token punctuation">.</span>foo <span class="token punctuation">}</span><span class="token punctuation">}</span> </code></pre> <p>或</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> obj<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">}</span> </code></pre> <p>解析器在遇到文本插值的起始定界符({{)时,会进入文本“插值状态 6”,并调用 parseInterpolation 函数来解析插值内容,如下图所示:<br> <a href="http://img.e-com-net.com/image/info8/ce8c18a2cf8a4a27a7307ea2f8a1b6b8.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/ce8c18a2cf8a4a27a7307ea2f8a1b6b8.jpg" alt="Vue解析器_第23张图片" width="650" height="513" style="border:1px solid black;"></a><br> 解析器在解析插值时,只需要将文本插值的开始定界符与结束定界符之间的内容提取出来,作为 JavaScript 表达式即可,具体实现如下:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseInterpolation</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 消费开始定界符</span> <span class="token number">03</span> context<span class="token punctuation">.</span><span class="token function">advanceBy</span><span class="token punctuation">(</span><span class="token string">'{{'</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">04</span> <span class="token comment">// 找到结束定界符的位置索引</span> <span class="token number">05</span> closeIndex <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'}}'</span><span class="token punctuation">)</span> <span class="token number">06</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>closeIndex <span class="token operator"><</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">07</span> console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'插值缺少结束定界符'</span><span class="token punctuation">)</span> <span class="token number">08</span> <span class="token punctuation">}</span> <span class="token number">09</span> <span class="token comment">// 截取开始定界符与结束定界符之间的内容作为插值表达式</span> <span class="token number">10</span> <span class="token keyword">const</span> content <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> closeIndex<span class="token punctuation">)</span> <span class="token number">11</span> <span class="token comment">// 消费表达式的内容</span> <span class="token number">12</span> context<span class="token punctuation">.</span><span class="token function">advanceBy</span><span class="token punctuation">(</span>content<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">13</span> <span class="token comment">// 消费结束定界符</span> <span class="token number">14</span> context<span class="token punctuation">.</span><span class="token function">advanceBy</span><span class="token punctuation">(</span><span class="token string">'}}'</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">15</span> <span class="token number">16</span> <span class="token comment">// 返回类型为 Interpolation 的节点,代表插值节点</span> <span class="token number">17</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> <span class="token number">18</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Interpolation'</span><span class="token punctuation">,</span> <span class="token number">19</span> <span class="token comment">// 插值节点的 content 是一个类型为 Expression 的表达式节点</span> <span class="token number">20</span> <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token number">21</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Expression'</span><span class="token punctuation">,</span> <span class="token number">22</span> <span class="token comment">// 表达式节点的内容则是经过 HTML 解码后的插值表达式</span> <span class="token number">23</span> <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token function">decodeHtml</span><span class="token punctuation">(</span>content<span class="token punctuation">)</span> <span class="token number">24</span> <span class="token punctuation">}</span> <span class="token number">25</span> <span class="token punctuation">}</span> <span class="token number">26</span> <span class="token punctuation">}</span> </code></pre> <p>配合上面的 parseInterpolation 函数,解析如下模板内容:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> ast <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><div>foo {{ bar }} baz</div></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span> </code></pre> <p>最终将得到如下 AST:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> ast <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Root'</span><span class="token punctuation">,</span> <span class="token number">03</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token number">04</span> <span class="token punctuation">{</span> <span class="token number">05</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Element'</span><span class="token punctuation">,</span> <span class="token number">06</span> <span class="token literal-property property">tag</span><span class="token operator">:</span> <span class="token string">'div'</span><span class="token punctuation">,</span> <span class="token number">07</span> <span class="token literal-property property">isSelfClosing</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token number">08</span> <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">09</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token number">10</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Text'</span><span class="token punctuation">,</span> <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token string">'foo '</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">11</span> <span class="token comment">// 插值节点</span> <span class="token number">12</span> <span class="token punctuation">{</span> <span class="token number">13</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Interpolation'</span><span class="token punctuation">,</span> <span class="token number">14</span> <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token number">15</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Expression'</span><span class="token punctuation">,</span> <span class="token number">16</span> <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token string">' bar '</span> <span class="token number">17</span> <span class="token punctuation">]</span> <span class="token number">18</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">19</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Text'</span><span class="token punctuation">,</span> <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token string">' baz'</span> <span class="token punctuation">}</span> <span class="token number">20</span> <span class="token punctuation">]</span> <span class="token number">21</span> <span class="token punctuation">}</span> <span class="token number">22</span> <span class="token punctuation">]</span> <span class="token number">23</span> <span class="token punctuation">}</span> </code></pre> <p>解析注释的思路与解析插值非常相似,如下面的parseComment 函数所示:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">function</span> <span class="token function">parseComment</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token comment">// 消费注释的开始部分</span> <span class="token number">03</span> context<span class="token punctuation">.</span><span class="token function">advanceBy</span><span class="token punctuation">(</span><span class="token string">'<!--'</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">04</span> <span class="token comment">// 找到注释结束部分的位置索引</span> <span class="token number">05</span> closeIndex <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'-->'</span><span class="token punctuation">)</span> <span class="token number">06</span> <span class="token comment">// 截取注释节点的内容</span> <span class="token number">07</span> <span class="token keyword">const</span> content <span class="token operator">=</span> context<span class="token punctuation">.</span>source<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> closeIndex<span class="token punctuation">)</span> <span class="token number">08</span> <span class="token comment">// 消费内容</span> <span class="token number">09</span> context<span class="token punctuation">.</span><span class="token function">advanceBy</span><span class="token punctuation">(</span>content<span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">10</span> <span class="token comment">// 消费注释的结束部分</span> <span class="token number">11</span> context<span class="token punctuation">.</span><span class="token function">advanceBy</span><span class="token punctuation">(</span><span class="token string">'-->'</span><span class="token punctuation">.</span>length<span class="token punctuation">)</span> <span class="token number">12</span> <span class="token comment">// 返回类型为 Comment 的节点</span> <span class="token number">13</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> <span class="token number">14</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Comment'</span><span class="token punctuation">,</span> <span class="token number">15</span> content <span class="token number">16</span> <span class="token punctuation">}</span> <span class="token number">17</span> <span class="token punctuation">}</span> </code></pre> <p>配合 parseComment 函数,解析如下模板内容:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> ast <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><div><!-- comments --></div></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span> </code></pre> <p>最终得到如下 AST:</p> <pre><code class="prism language-javascript"><span class="token number">01</span> <span class="token keyword">const</span> ast <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token number">02</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Root'</span><span class="token punctuation">,</span> <span class="token number">03</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token number">04</span> <span class="token punctuation">{</span> <span class="token number">05</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Element'</span><span class="token punctuation">,</span> <span class="token number">06</span> <span class="token literal-property property">tag</span><span class="token operator">:</span> <span class="token string">'div'</span><span class="token punctuation">,</span> <span class="token number">07</span> <span class="token literal-property property">isSelfClosing</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token number">08</span> <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">09</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token number">10</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'Comment'</span><span class="token punctuation">,</span> <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token string">' comments '</span> <span class="token punctuation">}</span> <span class="token number">11</span> <span class="token punctuation">]</span> <span class="token number">12</span> <span class="token punctuation">}</span> <span class="token number">13</span> <span class="token punctuation">]</span> <span class="token number">14</span> <span class="token punctuation">}</span> </code></pre> </div> </div> </div> </div> </div> <!--PC和WAP自适应版--> <div id="SOHUCS" sid="1728020491224035328"></div> <script type="text/javascript" src="/views/front/js/chanyan.js"></script> <!-- 文章页-底部 动态广告位 --> <div class="youdao-fixed-ad" id="detail_ad_bottom"></div> </div> <div class="col-md-3"> <div class="row" id="ad"> <!-- 文章页-右侧1 动态广告位 --> <div id="right-1" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad"> <div class="youdao-fixed-ad" id="detail_ad_1"> </div> </div> <!-- 文章页-右侧2 动态广告位 --> <div id="right-2" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad"> <div class="youdao-fixed-ad" id="detail_ad_2"></div> </div> <!-- 文章页-右侧3 动态广告位 --> <div id="right-3" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad"> <div class="youdao-fixed-ad" id="detail_ad_3"></div> </div> </div> </div> </div> </div> </div> <div class="container"> <h4 class="pt20 mb15 mt0 border-top">你可能感兴趣的:(#,Vue+TypeScript,Web,vue.js,flutter,前端,javascript,开发语言)</h4> <div id="paradigm-article-related"> <div class="recommend-post mb30"> <ul class="widget-links"> <li><a href="/article/1773610251875057664.htm" title="Android和IOS应用开发-Flutter应用让屏幕在 app 运行期间保持常亮的方法" target="_blank">Android和IOS应用开发-Flutter应用让屏幕在 app 运行期间保持常亮的方法</a> <span class="text-muted">江上清风山间明月</span> <a class="tag" taget="_blank" href="/search/Flutter/1.htm">Flutter</a><a class="tag" taget="_blank" href="/search/android/1.htm">android</a><a class="tag" taget="_blank" href="/search/ios/1.htm">ios</a><a class="tag" taget="_blank" href="/search/flutter/1.htm">flutter</a><a class="tag" taget="_blank" href="/search/KeepAlive/1.htm">KeepAlive</a><a class="tag" taget="_blank" href="/search/%E5%B1%8F%E5%B9%95%E5%B8%B8%E4%BA%AE/1.htm">屏幕常亮</a><a class="tag" taget="_blank" href="/search/wakelock/1.htm">wakelock</a><a class="tag" taget="_blank" href="/search/%E7%86%84%E5%B1%8F/1.htm">熄屏</a> <div>文章目录Flutter应用让屏幕在app运行期间保持常亮的方法方法一:使用系统插件方法二:使用Widgets注意事项Flutter应用让屏幕在app运行期间保持常亮的方法在Flutter开发中,可以使用以下两种方法让屏幕在app运行期间保持常亮:方法一:使用系统插件Flutter社区中已经有很多相关插件可供使用,比如wakelock:https://pub.dev/packages/wakeloc</div> </li> <li><a href="/article/1773602697044361216.htm" title="浪潮 M5系列服务器IPMI无法监控存储RAID卡问题." target="_blank">浪潮 M5系列服务器IPMI无法监控存储RAID卡问题.</a> <span class="text-muted">Songxwn</span> <a class="tag" taget="_blank" href="/search/%E7%A1%AC%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8/1.htm">硬件服务器</a><a class="tag" taget="_blank" href="/search/%E6%9C%8D%E5%8A%A1%E5%99%A8/1.htm">服务器</a><a class="tag" taget="_blank" href="/search/%E8%BF%90%E7%BB%B4/1.htm">运维</a> <div>简介浪潮的M5代服务器,可能有WebBMC无法查看存储RAID/SAS卡状态的情况,可以通过以下方式修改。修改完成后重启BMC即可生效。ESXiIPMITools使用:https://songxwn.com/ESXi8_IPMI/(Linux也可以直接使用)Linux/ESXiIPMITool下载:https://songxwn.com/file/ipmitoolWindows下载:https:/</div> </li> <li><a href="/article/1773510684068347904.htm" title="请简单介绍一下Shiro框架是什么?Shiro在Java安全领域的主要作用是什么?Shiro主要提供了哪些安全功能?" target="_blank">请简单介绍一下Shiro框架是什么?Shiro在Java安全领域的主要作用是什么?Shiro主要提供了哪些安全功能?</a> <span class="text-muted">AaronWang94</span> <a class="tag" taget="_blank" href="/search/shiro/1.htm">shiro</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E5%AE%89%E5%85%A8/1.htm">安全</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a> <div>请简单介绍一下Shiro框架是什么?Shiro框架是一个强大且灵活的开源安全框架,为Java应用程序提供了全面的安全解决方案。它主要用于身份验证、授权、加密和会话管理等功能,可以轻松地集成到任何JavaWeb应用程序中,并提供了易于理解和使用的API,使开发人员能够快速实现安全特性。Shiro的核心组件包括Subject、SecurityManager和Realms。Subject代表了当前与应用</div> </li> <li><a href="/article/1773504513622212608.htm" title="大前端-postcss安装使用指南" target="_blank">大前端-postcss安装使用指南</a> <span class="text-muted">黑夜照亮前行的路</span> <a class="tag" taget="_blank" href="/search/postcss/1.htm">postcss</a> <div>PostCSS是一款强大的CSS处理工具,可以用来自动添加浏览器前缀、代码合并、代码压缩等,提升代码的可读性,并支持使用最新的CSS语法。以下是一份简化的PostCSS安装使用指南:一、安装PostCSS在你的项目目录中,通过npm(NodePackageManager)来安装PostCSS。打开命令行窗口,输入以下命令:bash复制代码npminstallpostcss--save-dev这将把</div> </li> <li><a href="/article/1773504261557125120.htm" title="谷歌浏览器驱动Chromedriver(114-120版本)文件以及驱动下载教程" target="_blank">谷歌浏览器驱动Chromedriver(114-120版本)文件以及驱动下载教程</a> <span class="text-muted">pigerr杨</span> <a class="tag" taget="_blank" href="/search/Python/1.htm">Python</a><a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/chrome/1.htm">chrome</a><a class="tag" taget="_blank" href="/search/drivers/1.htm">drivers</a> <div>ChromeDriver官方网站GitHub||GoogleChromeLabs/chrome-for-testingChromeDriver113-125_JSONChromeforTestingavailability123-125zip白月黑羽Python基础|进阶|Qt图形界面|Django|自动化测试|性能测试|JS语言|JS前端|原理与安装</div> </li> <li><a href="/article/1773501994674225152.htm" title="虚拟 DOM 的优缺点有哪些" target="_blank">虚拟 DOM 的优缺点有哪些</a> <span class="text-muted">咕噜签名分发</span> <a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a> <div>虚拟DOM(VirtualDOM)技术作为现代前端开发中的重要组成部分,已经成为了众多流行前端框架的核心特性。它的引入为前端开发带来了诸多优势,同时也需要我们认真思考其潜在的考量。下面简单的介绍一下虚拟DOM技术的优势与缺点,深入探讨其在实际应用中的影响。提升性能虚拟DOM的最大优势之一是提升页面性能。通过比较前后两次虚拟DOM树的差异,最小化实际DOM操作,从而减少页面重渲染时的性能消耗。这种优</div> </li> <li><a href="/article/1773495574226599936.htm" title="3、JavaWeb-Ajax/Axios-前端工程化-Element" target="_blank">3、JavaWeb-Ajax/Axios-前端工程化-Element</a> <span class="text-muted">所谓远行Misnearch</span> <a class="tag" taget="_blank" href="/search/%23/1.htm">#</a><a class="tag" taget="_blank" href="/search/JavaWeb/1.htm">JavaWeb</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/ajax/1.htm">ajax</a><a class="tag" taget="_blank" href="/search/elementui/1.htm">elementui</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF%E6%A1%86%E6%9E%B6/1.htm">前端框架</a> <div>P34Ajax介绍Ajax:AsynchroousJavaScriptAndXML,异步的JS和XMLJS网页动作,XML一种标记语言,存储数据,作用:数据交换:通过Ajax给服务器发送请求,并获取服务器响应的数据异步交互:在不重新加载整个页面的情况下,与服务器交换数据并实现更新部分网页的技术,例如:搜索联想、用户名是否可用的校验等等。同步与异步:同步:服务器在处理中客户端要处于等待状态,输入域名</div> </li> <li><a href="/article/1773495447948689408.htm" title="docker怎么端口映射" target="_blank">docker怎么端口映射</a> <span class="text-muted">Lance_mu</span> <a class="tag" taget="_blank" href="/search/docker/1.htm">docker</a><a class="tag" taget="_blank" href="/search/%E5%AE%B9%E5%99%A8/1.htm">容器</a><a class="tag" taget="_blank" href="/search/%E8%BF%90%E7%BB%B4/1.htm">运维</a> <div>1、默认固定的端口#Web服务器:WebApache或Nginx通常使用80端口HTTP:80HTTPS:443#数据库服务器MySQL:3306PostgreSQL:5432MongoDB:27017Redis:6379#邮件服务器SMTP:25POP3:110IMAP:143#其他服务SSH:22FTP:21DNS(域名解析):53代理服务器Squid:3128版本控制系统Git:9418(S</div> </li> <li><a href="/article/1773482608366256128.htm" title="Flutter运行flutter doctor 命令长时间未响应如何解决" target="_blank">Flutter运行flutter doctor 命令长时间未响应如何解决</a> <span class="text-muted">咕噜签名分发-淼淼</span> <a class="tag" taget="_blank" href="/search/flutter/1.htm">flutter</a> <div>Hello大家好!我是咕噜铁蛋!在移动应用开发领域,Flutter以其高效、跨平台的特性吸引了众多开发者的关注。然而,在使用Flutter进行项目开发时,开发者可能会遇到各种问题,其中之一就是运行flutterdoctor命令时长时间未响应。今天铁蛋将深入探讨这一问题的成因、解决方案以及相关的Flutter环境配置知识。一、Flutter与flutterdoctor命令简介Flutter是Goog</div> </li> <li><a href="/article/1773471029155397632.htm" title="网络安全(黑客)——自学2024" target="_blank">网络安全(黑客)——自学2024</a> <span class="text-muted">小言同学喜欢挖漏洞</span> <a class="tag" taget="_blank" href="/search/web%E5%AE%89%E5%85%A8/1.htm">web安全</a><a class="tag" taget="_blank" href="/search/%E5%AE%89%E5%85%A8/1.htm">安全</a><a class="tag" taget="_blank" href="/search/%E7%BD%91%E7%BB%9C/1.htm">网络</a><a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/1.htm">网络安全</a><a class="tag" taget="_blank" href="/search/%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8/1.htm">信息安全</a><a class="tag" taget="_blank" href="/search/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/1.htm">渗透测试</a> <div>01什么是网络安全网络安全可以基于攻击和防御视角来分类,我们经常听到的“红队”、“渗透测试”等就是研究攻击技术,而“蓝队”、“安全运营”、“安全运维”则研究防御技术。无论网络、Web、移动、桌面、云等哪个领域,都有攻与防两面性,例如Web安全技术,既有Web渗透,也有Web防御技术(WAF)。作为一个合格的网络安全工程师,应该做到攻守兼备,毕竟知己知彼,才能百战百胜。02怎样规划网络安全如果你是一</div> </li> <li><a href="/article/1773470776930926592.htm" title="黑客(网络安全)技术自学30天" target="_blank">黑客(网络安全)技术自学30天</a> <span class="text-muted">一个迷人的黑客</span> <a class="tag" taget="_blank" href="/search/web%E5%AE%89%E5%85%A8/1.htm">web安全</a><a class="tag" taget="_blank" href="/search/%E5%AE%89%E5%85%A8/1.htm">安全</a><a class="tag" taget="_blank" href="/search/%E7%BD%91%E7%BB%9C/1.htm">网络</a><a class="tag" taget="_blank" href="/search/%E7%AC%94%E8%AE%B0/1.htm">笔记</a><a class="tag" taget="_blank" href="/search/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/1.htm">网络安全</a><a class="tag" taget="_blank" href="/search/%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8/1.htm">信息安全</a><a class="tag" taget="_blank" href="/search/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/1.htm">渗透测试</a> <div>01什么是网络安全网络安全可以基于攻击和防御视角来分类,我们经常听到的“红队”、“渗透测试”等就是研究攻击技术,而“蓝队”、“安全运营”、“安全运维”则研究防御技术。无论网络、Web、移动、桌面、云等哪个领域,都有攻与防两面性,例如Web安全技术,既有Web渗透,也有Web防御技术(WAF)。作为一个合格的网络安全工程师,应该做到攻守兼备,毕竟知己知彼,才能百战百胜。02怎样规划网络安全如果你是一</div> </li> <li><a href="/article/1773469895019790336.htm" title="flutter boost 如何从native跳转到flutter页面" target="_blank">flutter boost 如何从native跳转到flutter页面</a> <span class="text-muted">Icarus_</span> <a class="tag" taget="_blank" href="/search/flutter/1.htm">flutter</a><a class="tag" taget="_blank" href="/search/flutter/1.htm">flutter</a> <div>FlutterBoost是一个Flutter插件,它可以帮助开发者在原生应用和Flutter应用之间无缝跳转。以下是一些基本步骤,展示了如何使用FlutterBoost从原生(Native)页面跳转到Flutter页面。1.配置FlutterBoost在你的Flutter项目中集成FlutterBoost插件。这通常涉及到修改`pubspec.yaml`文件来添加依赖项,并根据FlutterBoo</div> </li> <li><a href="/article/1773468510236770304.htm" title="flutter 修改app名字和图标" target="_blank">flutter 修改app名字和图标</a> <span class="text-muted">肥肥呀呀呀</span> <a class="tag" taget="_blank" href="/search/flutter/1.htm">flutter</a> <div>一、修改名字在Android中修改应用程序名称:在AndroidManifest.xml文件中修改应用程序名称:打开Flutter项目中的android/app/src/main/AndroidManifest.xml文件。找到标签,然后在android:label属性中修改应用程序的名称,例如:android:label="YourNewAppName"。在strings.xml文件中修改应用程</div> </li> <li><a href="/article/1773467628816367616.htm" title="掌握Flutter底部导航栏:畅游导航之旅" target="_blank">掌握Flutter底部导航栏:畅游导航之旅</a> <span class="text-muted">繁依Fanyi</span> <a class="tag" taget="_blank" href="/search/xml/1.htm">xml</a><a class="tag" taget="_blank" href="/search/json/1.htm">json</a><a class="tag" taget="_blank" href="/search/sql/1.htm">sql</a><a class="tag" taget="_blank" href="/search/flutter/1.htm">flutter</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/git/1.htm">git</a> <div>1.引言在移动应用开发中,底部导航栏是一种常见且非常实用的用户界面元素。它提供了快速导航至不同功能模块或页面的便捷方式,使用户可以轻松访问应用程序的各个部分。在Flutter中,底部导航栏也是一项强大的功能,开发者可以利用Flutter框架提供的丰富组件和灵活性,轻松实现各种样式和交互效果的底部导航栏。本文将深入探讨Flutter中底部导航栏的实现方法,从基础的结构搭建到高级功能的应用,带领读者逐</div> </li> <li><a href="/article/1773408210510741504.htm" title="webpack.prod.js(webpack生产环境配置文件)" target="_blank">webpack.prod.js(webpack生产环境配置文件)</a> <span class="text-muted">门板_</span> <a class="tag" taget="_blank" href="/search/webpack/1.htm">webpack</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a> <div>生产环境:只打包不运行本地服务器对于在config目录下的webpack.prod.js1.在根目录下运行npxwebpack--config./config/webpack.prod.js2.在package.json文件中配置"build":"npxwebpack--config./config/webpack.prod.js"constpath=require('path')constESL</div> </li> <li><a href="/article/1773403175781466112.htm" title="Python dict字符串转json对象,小数精度丢失问题" target="_blank">Python dict字符串转json对象,小数精度丢失问题</a> <span class="text-muted">朝如青丝 暮成雪</span> <a class="tag" taget="_blank" href="/search/json/1.htm">json</a><a class="tag" taget="_blank" href="/search/python/1.htm">python</a> <div>一前言JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式,dict是Python的一种数据格式。本篇介绍一个float数据转换时精度丢失的案例。二问题描述importjsontest_str1='{"π":3.1415926535897932384626433832795028841971}'test_str2='{"value":10.00000}'print</div> </li> <li><a href="/article/1773389331080216576.htm" title="VUE 页面禁止缩放(华为平板浏览器可能失效)" target="_blank">VUE 页面禁止缩放(华为平板浏览器可能失效)</a> <span class="text-muted">唐屁屁儿</span> <a class="tag" taget="_blank" href="/search/JS/1.htm">JS</a><a class="tag" taget="_blank" href="/search/vue/1.htm">vue</a><a class="tag" taget="_blank" href="/search/webview/1.htm">webview</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a> <div>h5页面移动端禁止缩放、web页面禁止浏览器缩放移动端优先,可禁止用户缩放和双击放大;在App.vue中的script内的方法里加入以下代码:window.onload=function(){document.addEventListener('touchstart',function(event){if(event.touches.length>1){event.preventDefault()</div> </li> <li><a href="/article/1773382031552610304.htm" title="java实体中返回前端的double类型四舍五入(格式化)" target="_blank">java实体中返回前端的double类型四舍五入(格式化)</a> <span class="text-muted">婲落ヽ紅顏誶</span> <a class="tag" taget="_blank" href="/search/java/1.htm">java</a> <div>根据业务,需要通过后端给前端返回部分double类型的数值,一般需要保留两位小数,使用jackson转换对象packagecom.ruoyi.common.core.config;importcom.fasterxml.jackson.core.JsonGenerator;importcom.fasterxml.jackson.databind.JsonSerializer;importcom.f</div> </li> <li><a href="/article/1773360885226602496.htm" title="Django forms组件" target="_blank">Django forms组件</a> <span class="text-muted">在飞行-米龙</span> <a class="tag" taget="_blank" href="/search/Django/1.htm">Django</a><a class="tag" taget="_blank" href="/search/django/1.htm">django</a><a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/%E5%90%8E%E7%AB%AF/1.htm">后端</a> <div>【一】引入【1】实现登陆验证功能(1)需求分析登陆验证需要前后端交互,采用form表单提交数据对数据进行校验用户名必须以英文大写字母开头密码必须大于三位数反馈给用户错误的信息除了反馈错误的信息还有保留原始输入内容(2)后端代码使用user_info_dict字典每次刷新存储存储前端发送的信息存储后端进行验证的信息defhome(request):#每次后刷新这个信息字典user_info_dict</div> </li> <li><a href="/article/1773347793847517184.htm" title="UNDERSTANDING HTML WITH LARGE LANGUAGE MODELS" target="_blank">UNDERSTANDING HTML WITH LARGE LANGUAGE MODELS</a> <span class="text-muted">liferecords</span> <a class="tag" taget="_blank" href="/search/LLM/1.htm">LLM</a><a class="tag" taget="_blank" href="/search/%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B/1.htm">语言模型</a><a class="tag" taget="_blank" href="/search/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/1.htm">人工智能</a><a class="tag" taget="_blank" href="/search/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%A4%84%E7%90%86/1.htm">自然语言处理</a> <div>UNDERSTANDINGHTMLWITHLARGELANGUAGEMODELS相关链接:arXiv关键字:大型语言模型、HTML理解、Web自动化、自然语言处理、机器学习摘要大型语言模型(LLMs)在各种自然语言任务上表现出色。然而,它们在HTML理解方面的能力——即解析网页的原始HTML,对于自动化基于Web的任务、爬取和浏览器辅助检索等应用——尚未被充分探索。我们为HTML理解模型(经过微调</div> </li> <li><a href="/article/1773347164047605760.htm" title="自学黑客(网络安全)技术——2024最新" target="_blank">自学黑客(网络安全)技术——2024最新</a> <span class="text-muted">九九归二</span> <a class="tag" taget="_blank" href="/search/web%E5%AE%89%E5%85%A8/1.htm">web安全</a><a class="tag" taget="_blank" href="/search/%E5%AE%89%E5%85%A8/1.htm">安全</a><a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/%E7%AC%94%E8%AE%B0/1.htm">笔记</a><a class="tag" taget="_blank" href="/search/%E7%BD%91%E7%BB%9C/1.htm">网络</a><a class="tag" taget="_blank" href="/search/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/1.htm">网络安全</a><a class="tag" taget="_blank" href="/search/%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8/1.htm">信息安全</a> <div>01什么是网络安全网络安全可以基于攻击和防御视角来分类,我们经常听到的“红队”、“渗透测试”等就是研究攻击技术,而“蓝队”、“安全运营”、“安全运维”则研究防御技术。无论网络、Web、移动、桌面、云等哪个领域,都有攻与防两面性,例如Web安全技术,既有Web渗透,也有Web防御技术(WAF)。作为一个合格的网络安全工程师,应该做到攻守兼备,毕竟知己知彼,才能百战百胜。02怎样规划网络安全如果你是一</div> </li> <li><a href="/article/1773344429185236992.htm" title="Webpack构建优化——区分环境" target="_blank">Webpack构建优化——区分环境</a> <span class="text-muted">oWSQo</span> <div>为什么需要区分环境在开发网页的时候,一般都会有多套运行环境,例如:在开发过程中方便开发调试的环境。发布到线上给用户使用的运行环境。这两套不同的环境虽然都是由同一套源代码编译而来,但是代码内容却不一样,差异包括:线上代码被特殊压缩过。开发用的代码包含一些用于提示开发者的提示日志,这些日志普通用户不可能去看它。开发用的代码所连接的后端数据接口地址也可能和线上环境不同,因为要避免开发过程中造成对线上数据</div> </li> <li><a href="/article/1773308900838277120.htm" title="Web前端Html的表单" target="_blank">Web前端Html的表单</a> <span class="text-muted">任家伟</span> <a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/html/1.htm">html</a> <div>表单的关键字:form标签表示一个表单区域action=“后端地址”method=“提交数据方式:get/post”input单行输入框type=“text”文本name=“定义名称名字自定义”向后端提交的键readonly=“readonly”只读,不可修改,但是可以提交disabled=“disabled”禁用组件不可修改,不能提交type=“password”密码框type=“radio”单</div> </li> <li><a href="/article/1773279695408791552.htm" title="Thinkphp - 详细实现网站系统登录功能,附带 Mysql 数据库设置、Web 前端展示界面、信息校验等(详细代码,即设计过程)" target="_blank">Thinkphp - 详细实现网站系统登录功能,附带 Mysql 数据库设置、Web 前端展示界面、信息校验等(详细代码,即设计过程)</a> <span class="text-muted">王佳斌</span> <a class="tag" taget="_blank" href="/search/%2B/1.htm">+</a><a class="tag" taget="_blank" href="/search/Thinkphp/1.htm">Thinkphp</a><a class="tag" taget="_blank" href="/search/mysql/1.htm">mysql</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%BA%93/1.htm">数据库</a> <div>前言登录功能,是我们几乎开发每个系统都必须的模块。登录功能设计思路,主要包括几个方面。用户输入网址展示登录页面用户输入用户名,密码等点击登录进行信息校验校验通过之后,记录用户登录信息,跳转指定页面用户校验失败,提示失败信息页面目录具体功能实现为了快速搭建可用、美观的页面,我们采用一个比较成熟的前端框架Bootstrap。下面我们到Bootstrap的官网Bootsrap官网下载bootstrap。</div> </li> <li><a href="/article/1773256158274977792.htm" title="javascript 日期转换为时间戳,时间戳转换为日期的函数" target="_blank">javascript 日期转换为时间戳,时间戳转换为日期的函数</a> <span class="text-muted">cdcdhj</span> <a class="tag" taget="_blank" href="/search/javascript%E5%AD%A6%E4%B9%A0%E6%97%A5%E8%AE%B0/1.htm">javascript学习日记</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a><a class="tag" taget="_blank" href="/search/ecmascript/1.htm">ecmascript</a> <div>日期转化为时间戳,主要用valueOf()来进行转化为毫秒时间戳,getTime()IOS系统无法解析转换,所以都有valueOf()letgetTimestampOrDate=function(timestamp){lettimeStamp='';constregex=/^\d{4}(-|\/)\d{2}(-|\/)\d{2}$/;constregex2=/^\d{4}(-|\/)\d{2}(-</div> </li> <li><a href="/article/1773198631940194304.htm" title="COMP315 JavaScript Cloud Computing for E Commerce" target="_blank">COMP315 JavaScript Cloud Computing for E Commerce</a> <span class="text-muted">zhuyu0206girl</span> <a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a><a class="tag" taget="_blank" href="/search/ecmascript/1.htm">ecmascript</a> <div>Assignment1:Javascript1IntroductionAcommontaskincloudcomputingisdatacleaning,whichistheprocessoftakinganinitialdatasetthatmaycontainerroneousorincompletedata,andremovingorfixingthoseelementsbeforeform</div> </li> <li><a href="/article/1773191331473063936.htm" title="JSON与AJAX:网页交互的利器" target="_blank">JSON与AJAX:网页交互的利器</a> <span class="text-muted">入冉心</span> <a class="tag" taget="_blank" href="/search/json/1.htm">json</a><a class="tag" taget="_blank" href="/search/ajax/1.htm">ajax</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a> <div>在现代Web开发中,JSON(JavaScriptObjectNotation)和AJAX(AsynchronousJavaScriptandXML)是两项不可或缺的技术。它们共同为网页提供了动态、实时的数据交互能力,为用户带来了更加流畅和丰富的体验。本文将详细介绍JSON和AJAX的概念、原理,并通过代码示例展示它们在实际开发中的应用。一、JSON:轻量级的数据交换格式JSON是一种轻量级的数据</div> </li> <li><a href="/article/1772795036136701952.htm" title="程序员开发技术整理" target="_blank">程序员开发技术整理</a> <span class="text-muted">laizhixue</span> <a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF%E6%A1%86%E6%9E%B6/1.htm">前端框架</a> <div>前端技术:vue-前端框架element-前端框架bootstrap-前端框架echarts-图标组件C#后端技术:webservice:soap架构:简单的通信协议,用于服务通信ORM框架:对象关系映射,如EF:对象实体模型,是ado.net中的应用技术soap服务通讯:xml通讯ado.net:OAuth2:登录授权认证:Token认证:JWT:jsonwebtokenJava后端技术:便捷工</div> </li> <li><a href="/article/1772775523957669888.htm" title="centos7 安装influxdb+telegraf+grafana 监控服务器" target="_blank">centos7 安装influxdb+telegraf+grafana 监控服务器</a> <span class="text-muted">吕吕-lvlv</span> <a class="tag" taget="_blank" href="/search/grafana/1.htm">grafana</a><a class="tag" taget="_blank" href="/search/%E6%9C%8D%E5%8A%A1%E5%99%A8/1.htm">服务器</a><a class="tag" taget="_blank" href="/search/%E8%BF%90%E7%BB%B4/1.htm">运维</a> <div>influxdbinfluxdb是一个时间序列数据库,所有数据记录都会打上时间戳,适合存储数字类型的内容telegraftelegraf可以用于收集系统和服务的统计数据并发送到influxdbgrafanagrafana是一个界面非常漂亮,可直接读取influxdb数据展示成各种图表的开源可视化web软件安装并启动influxdb数据库vim/etc/yum.repos.d/influxdb.re</div> </li> <li><a href="/article/1772773132000624640.htm" title="【前端学习——js篇】7.函数缓存" target="_blank">【前端学习——js篇】7.函数缓存</a> <span class="text-muted">笔下无竹墨下有鱼</span> <a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF%E5%AD%A6%E4%B9%A0/1.htm">前端学习</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a> <div>具体见:https://github.com/febobo/web-interview7.函数缓存函数缓存,就是将函数运算过的结果进行缓存本质上就是用空间(缓存存储)换时间(计算过程)常用于缓存数据计算结果和缓存对象。其实现主要通过闭包、柯里化和高阶函数。下面主要介绍下柯里化:①柯里化柯里化(currying)是一种函数式编程的概念,指的是将一个带有多个参数的函数转换成一系列只接受一个参数的函数的</div> </li> <li><a href="/article/103.htm" title="SAX解析xml文件" target="_blank">SAX解析xml文件</a> <span class="text-muted">小猪猪08</span> <a class="tag" taget="_blank" href="/search/xml/1.htm">xml</a> <div>1.创建SAXParserFactory实例 2.通过SAXParserFactory对象获取SAXParser实例 3.创建一个类SAXParserHander继续DefaultHandler,并且实例化这个类 4.SAXParser实例的parse来获取文件     public static void main(String[] args) { //</div> </li> <li><a href="/article/230.htm" title="为什么mysql里的ibdata1文件不断的增长?" target="_blank">为什么mysql里的ibdata1文件不断的增长?</a> <span class="text-muted">brotherlamp</span> <a class="tag" taget="_blank" href="/search/linux/1.htm">linux</a><a class="tag" taget="_blank" href="/search/linux%E8%BF%90%E7%BB%B4/1.htm">linux运维</a><a class="tag" taget="_blank" href="/search/linux%E8%B5%84%E6%96%99/1.htm">linux资料</a><a class="tag" taget="_blank" href="/search/linux%E8%A7%86%E9%A2%91/1.htm">linux视频</a><a class="tag" taget="_blank" href="/search/linux%E8%BF%90%E7%BB%B4%E8%87%AA%E5%AD%A6/1.htm">linux运维自学</a> <div>我们在 Percona 支持栏目经常收到关于 MySQL 的 ibdata1 文件的这个问题。 当监控服务器发送一个关于 MySQL 服务器存储的报警时,恐慌就开始了 —— 就是说磁盘快要满了。 一番调查后你意识到大多数地盘空间被 InnoDB 的共享表空间 ibdata1 使用。而你已经启用了 innodbfileper_table,所以问题是: ibdata1存了什么? 当你启用了 i</div> </li> <li><a href="/article/357.htm" title="Quartz-quartz.properties配置" target="_blank">Quartz-quartz.properties配置</a> <span class="text-muted">eksliang</span> <a class="tag" taget="_blank" href="/search/quartz/1.htm">quartz</a> <div>其实Quartz JAR文件的org.quartz包下就包含了一个quartz.properties属性配置文件并提供了默认设置。如果需要调整默认配置,可以在类路径下建立一个新的quartz.properties,它将自动被Quartz加载并覆盖默认的设置。   下面是这些默认值的解释 #-----集群的配置 org.quartz.scheduler.instanceName =</div> </li> <li><a href="/article/484.htm" title="informatica session的使用" target="_blank">informatica session的使用</a> <span class="text-muted">18289753290</span> <a class="tag" taget="_blank" href="/search/workflow/1.htm">workflow</a><a class="tag" taget="_blank" href="/search/session/1.htm">session</a><a class="tag" taget="_blank" href="/search/log/1.htm">log</a><a class="tag" taget="_blank" href="/search/Informatica/1.htm">Informatica</a> <div>如果希望workflow存储最近20次的log,在session里的Config  Object设置,log  options做配置,save  session log :sessions  run  ;savesessio log for  these runs:20 session下面的source 里面有个tracing </div> </li> <li><a href="/article/611.htm" title="Scrapy抓取网页时出现CRC check failed 0x471e6e9a != 0x7c07b839L的错误" target="_blank">Scrapy抓取网页时出现CRC check failed 0x471e6e9a != 0x7c07b839L的错误</a> <span class="text-muted">酷的飞上天空</span> <a class="tag" taget="_blank" href="/search/scrapy/1.htm">scrapy</a> <div>Scrapy版本0.14.4 出现问题现象: ERROR: Error downloading <GET http://xxxxx  CRC check failed   解决方法   1.设置网络请求时的header中的属性'Accept-Encoding': '*;q=0'   明确表示不支持任何形式的压缩格式,避免程序的解压</div> </li> <li><a href="/article/738.htm" title="java Swing小集锦" target="_blank">java Swing小集锦</a> <span class="text-muted">永夜-极光</span> <a class="tag" taget="_blank" href="/search/java+swing/1.htm">java swing</a> <div>1.关闭窗体弹出确认对话框   1.1   this.setDefaultCloseOperation (JFrame.DO_NOTHING_ON_CLOSE);   1.2   this.addWindowListener ( new WindowAdapter () { public void windo</div> </li> <li><a href="/article/865.htm" title="强制删除.svn文件夹" target="_blank">强制删除.svn文件夹</a> <span class="text-muted">随便小屋</span> <a class="tag" taget="_blank" href="/search/java/1.htm">java</a> <div>          在windows上,从别处复制的项目中可能带有.svn文件夹,手动删除太麻烦,并且每个文件夹下都有。所以写了个程序进行删除。因为.svn文件夹在windows上是只读的,所以用File中的delete()和deleteOnExist()方法都不能将其删除,所以只能采用windows命令方式进行删除</div> </li> <li><a href="/article/992.htm" title="GET和POST有什么区别?及为什么网上的多数答案都是错的。" target="_blank">GET和POST有什么区别?及为什么网上的多数答案都是错的。</a> <span class="text-muted">aijuans</span> <a class="tag" taget="_blank" href="/search/get+post/1.htm">get post</a> <div>     如果有人问你,GET和POST,有什么区别?你会如何回答? 我的经历      前几天有人问我这个问题。我说GET是用于获取数据的,POST,一般用于将数据发给服务器之用。     这个答案好像并不是他想要的。于是他继续追问有没有别的区别?我说这就是个名字而已,如果服务器支持,他完全可以把G</div> </li> <li><a href="/article/1119.htm" title="谈谈新浪微博背后的那些算法" target="_blank">谈谈新浪微博背后的那些算法</a> <span class="text-muted">aoyouzi</span> <a class="tag" taget="_blank" href="/search/%E8%B0%88%E8%B0%88%E6%96%B0%E6%B5%AA%E5%BE%AE%E5%8D%9A%E8%83%8C%E5%90%8E%E7%9A%84%E9%82%A3%E4%BA%9B%E7%AE%97%E6%B3%95/1.htm">谈谈新浪微博背后的那些算法</a> <div>本文对微博中常见的问题的对应算法进行了简单的介绍,在实际应用中的算法比介绍的要复杂的多。当然,本文覆盖的主题并不全,比如好友推荐、热点跟踪等就没有涉及到。但古人云“窥一斑而见全豹”,希望本文的介绍能帮助大家更好的理解微博这样的社交网络应用。 微博是一个很多人都在用的社交应用。天天刷微博的人每天都会进行着这样几个操作:原创、转发、回复、阅读、关注、@等。其中,前四个是针对短博文,最后的关注和@则针</div> </li> <li><a href="/article/1246.htm" title="Connection reset 连接被重置的解决方法" target="_blank">Connection reset 连接被重置的解决方法</a> <span class="text-muted">百合不是茶</span> <a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E5%AD%97%E7%AC%A6%E6%B5%81/1.htm">字符流</a><a class="tag" taget="_blank" href="/search/%E8%BF%9E%E6%8E%A5%E8%A2%AB%E9%87%8D%E7%BD%AE/1.htm">连接被重置</a> <div>流是java的核心部分,,昨天在做android服务器连接服务器的时候出了问题,就将代码放到java中执行,结果还是一样连接被重置   被重置的代码如下;   客户端代码; package 通信软件服务器; import java.io.BufferedWriter; import java.io.OutputStream; import java.io.O</div> </li> <li><a href="/article/1373.htm" title="web.xml配置详解之filter" target="_blank">web.xml配置详解之filter</a> <span class="text-muted">bijian1013</span> <a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/web.xml/1.htm">web.xml</a><a class="tag" taget="_blank" href="/search/filter/1.htm">filter</a> <div>一.定义 <filter> <filter-name>encodingfilter</filter-name> <filter-class>com.my.app.EncodingFilter</filter-class> <init-param> <param-name>encoding<</div> </li> <li><a href="/article/1500.htm" title="Heritrix" target="_blank">Heritrix</a> <span class="text-muted">Bill_chen</span> <a class="tag" taget="_blank" href="/search/%E5%A4%9A%E7%BA%BF%E7%A8%8B/1.htm">多线程</a><a class="tag" taget="_blank" href="/search/xml/1.htm">xml</a><a class="tag" taget="_blank" href="/search/%E7%AE%97%E6%B3%95/1.htm">算法</a><a class="tag" taget="_blank" href="/search/%E5%88%B6%E9%80%A0/1.htm">制造</a><a class="tag" taget="_blank" href="/search/%E9%85%8D%E7%BD%AE%E7%AE%A1%E7%90%86/1.htm">配置管理</a> <div>作为纯Java语言开发的、功能强大的网络爬虫Heritrix,其功能极其强大,且扩展性良好,深受热爱搜索技术的盆友们的喜爱,但它配置较为复杂,且源码不好理解,最近又使劲看了下,结合自己的学习和理解,跟大家分享Heritrix的点点滴滴。 Heritrix的下载(http://sourceforge.net/projects/archive-crawler/)安装、配置,就不罗嗦了,可以自己找找资</div> </li> <li><a href="/article/1627.htm" title="【Zookeeper】FAQ" target="_blank">【Zookeeper】FAQ</a> <span class="text-muted">bit1129</span> <a class="tag" taget="_blank" href="/search/zookeeper/1.htm">zookeeper</a> <div>1.脱离IDE,运行简单的Java客户端程序 #ZkClient是简单的Zookeeper~$ java -cp "./:zookeeper-3.4.6.jar:./lib/*" ZKClient    1. Zookeeper是的Watcher回调是同步操作,需要添加异步处理的代码 2. 如果Zookeeper集群跨越多个机房,那么Leader/</div> </li> <li><a href="/article/1754.htm" title="The user specified as a definer ('aaa'@'localhost') does not exist" target="_blank">The user specified as a definer ('aaa'@'localhost') does not exist</a> <span class="text-muted">白糖_</span> <a class="tag" taget="_blank" href="/search/localhost/1.htm">localhost</a> <div>今天遇到一个客户BUG,当前的jdbc连接用户是root,然后部分删除操作都会报下面这个错误:The user specified as a definer ('aaa'@'localhost') does not exist 最后找原因发现删除操作做了触发器,而触发器里面有这样一句 /*!50017 DEFINER = ''aaa@'localhost' */  原来最初</div> </li> <li><a href="/article/1881.htm" title="javascript中showModelDialog刷新父页面" target="_blank">javascript中showModelDialog刷新父页面</a> <span class="text-muted">bozch</span> <a class="tag" taget="_blank" href="/search/JavaScript/1.htm">JavaScript</a><a class="tag" taget="_blank" href="/search/%E5%88%B7%E6%96%B0%E7%88%B6%E9%A1%B5%E9%9D%A2/1.htm">刷新父页面</a><a class="tag" taget="_blank" href="/search/showModalDialog/1.htm">showModalDialog</a> <div>在页面中使用showModalDialog打开模式子页面窗口的时候,如果想在子页面中操作父页面中的某个节点,可以通过如下的进行:       window.showModalDialog('url',self,‘status...’); // 首先中间参数使用self       在子页面使用w</div> </li> <li><a href="/article/2008.htm" title="编程之美-买书折扣" target="_blank">编程之美-买书折扣</a> <span class="text-muted">bylijinnan</span> <a class="tag" taget="_blank" href="/search/%E7%BC%96%E7%A8%8B%E4%B9%8B%E7%BE%8E/1.htm">编程之美</a> <div> import java.util.Arrays; public class BookDiscount { /**编程之美 买书折扣 书上的贪心算法的分析很有意思,我看了半天看不懂,结果作者说,贪心算法在这个问题上是不适用的。。 下面用动态规划实现。 哈利波特这本书一共有五卷,每卷都是8欧元,如果读者一次购买不同的两卷可扣除5%的折扣,三卷10%,四卷20%,五卷</div> </li> <li><a href="/article/2135.htm" title="关于struts2.3.4项目跨站执行脚本以及远程执行漏洞修复概要" target="_blank">关于struts2.3.4项目跨站执行脚本以及远程执行漏洞修复概要</a> <span class="text-muted">chenbowen00</span> <a class="tag" taget="_blank" href="/search/struts/1.htm">struts</a><a class="tag" taget="_blank" href="/search/WEB%E5%AE%89%E5%85%A8/1.htm">WEB安全</a> <div>因为近期负责的几个银行系统软件,需要交付客户,因此客户专门请了安全公司对系统进行了安全评测,结果发现了诸如跨站执行脚本,远程执行漏洞以及弱口令等问题。 下面记录下本次解决的过程以便后续 1、首先从最简单的开始处理,服务器的弱口令问题,首先根据安全工具提供的测试描述中发现应用服务器中存在一个匿名用户,默认是不需要密码的,经过分析发现服务器使用了FTP协议, 而使用ftp协议默认会产生一个匿名用</div> </li> <li><a href="/article/2262.htm" title="[电力与暖气]煤炭燃烧与电力加温" target="_blank">[电力与暖气]煤炭燃烧与电力加温</a> <span class="text-muted">comsci</span> <div>       在宇宙中,用贝塔射线观测地球某个部分,看上去,好像一个个马蜂窝,又像珊瑚礁一样,原来是某个国家的采煤区.....       不过,这个采煤区的煤炭看来是要用完了.....那么依赖将起燃烧并取暖的城市,在极度严寒的季节中...该怎么办呢?   &nbs</div> </li> <li><a href="/article/2389.htm" title="oracle O7_DICTIONARY_ACCESSIBILITY参数" target="_blank">oracle O7_DICTIONARY_ACCESSIBILITY参数</a> <span class="text-muted">daizj</span> <a class="tag" taget="_blank" href="/search/oracle/1.htm">oracle</a> <div>O7_DICTIONARY_ACCESSIBILITY参数控制对数据字典的访问.设置为true,如果用户被授予了如select any table等any table权限,用户即使不是dba或sysdba用户也可以访问数据字典.在9i及以上版本默认为false,8i及以前版本默认为true.如果设置为true就可能会带来安全上的一些问题.这也就为什么O7_DICTIONARY_ACCESSIBIL</div> </li> <li><a href="/article/2516.htm" title="比较全面的MySQL优化参考" target="_blank">比较全面的MySQL优化参考</a> <span class="text-muted">dengkane</span> <a class="tag" taget="_blank" href="/search/mysql/1.htm">mysql</a> <div>本文整理了一些MySQL的通用优化方法,做个简单的总结分享,旨在帮助那些没有专职MySQL DBA的企业做好基本的优化工作,至于具体的SQL优化,大部分通过加适当的索引即可达到效果,更复杂的就需要具体分析了,可以参考本站的一些优化案例或者联系我,下方有我的联系方式。这是上篇。   1、硬件层相关优化   1.1、CPU相关   在服务器的BIOS设置中,可</div> </li> <li><a href="/article/2643.htm" title="C语言homework2,有一个逆序打印数字的小算法" target="_blank">C语言homework2,有一个逆序打印数字的小算法</a> <span class="text-muted">dcj3sjt126com</span> <a class="tag" taget="_blank" href="/search/c/1.htm">c</a> <div>#h1#   0、完成课堂例子 1、将一个四位数逆序打印 1234 ==> 4321 实现方法一: # include <stdio.h> int main(void) { int i = 1234; int one = i%10; int two = i / 10 % 10; int three = i / 100 % 10; </div> </li> <li><a href="/article/2770.htm" title="apacheBench对网站进行压力测试" target="_blank">apacheBench对网站进行压力测试</a> <span class="text-muted">dcj3sjt126com</span> <a class="tag" taget="_blank" href="/search/apachebench/1.htm">apachebench</a> <div>   ab 的全称是 ApacheBench , 是 Apache 附带的一个小工具 , 专门用于 HTTP Server 的 benchmark testing , 可以同时模拟多个并发请求。前段时间看到公司的开发人员也在用它作一些测试,看起来也不错,很简单,也很容易使用,所以今天花一点时间看了一下。 通过下面的一个简单的例子和注释,相信大家可以更容易理解这个工具的使用。 </div> </li> <li><a href="/article/2897.htm" title="2种办法让HashMap线程安全" target="_blank">2种办法让HashMap线程安全</a> <span class="text-muted">flyfoxs</span> <a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/jdk/1.htm">jdk</a><a class="tag" taget="_blank" href="/search/jni/1.htm">jni</a> <div>多线程之--2种办法让HashMap线程安全 多线程之--synchronized 和reentrantlock的优缺点 多线程之--2种JAVA乐观锁的比较( NonfairSync VS. FairSync)     HashMap不是线程安全的,往往在写程序时需要通过一些方法来回避.其实JDK原生的提供了2种方法让HashMap支持线程安全.   </div> </li> <li><a href="/article/3024.htm" title="Spring Security(04)——认证简介" target="_blank">Spring Security(04)——认证简介</a> <span class="text-muted">234390216</span> <a class="tag" taget="_blank" href="/search/Spring+Security/1.htm">Spring Security</a><a class="tag" taget="_blank" href="/search/%E8%AE%A4%E8%AF%81/1.htm">认证</a><a class="tag" taget="_blank" href="/search/%E8%BF%87%E7%A8%8B/1.htm">过程</a> <div>认证简介 目录 1.1     认证过程 1.2     Web应用的认证过程 1.2.1    ExceptionTranslationFilter 1.2.2    在request之间共享SecurityContext   1</div> </li> <li><a href="/article/3151.htm" title="Java 位运算" target="_blank">Java 位运算</a> <span class="text-muted">Javahuhui</span> <a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E4%BD%8D%E8%BF%90%E7%AE%97/1.htm">位运算</a> <div>// 左移( << ) 低位补0 // 0000 0000 0000 0000 0000 0000 0000 0110 然后左移2位后,低位补0: // 0000 0000 0000 0000 0000 0000 0001 1000 System.out.println(6 << 2);// 运行结果是24 // 右移( >> ) 高位补"</div> </li> <li><a href="/article/3278.htm" title="mysql免安装版配置" target="_blank">mysql免安装版配置</a> <span class="text-muted">ldzyz007</span> <a class="tag" taget="_blank" href="/search/mysql/1.htm">mysql</a> <div>1、my-small.ini是为了小型数据库而设计的。不应该把这个模型用于含有一些常用项目的数据库。 2、my-medium.ini是为中等规模的数据库而设计的。如果你正在企业中使用RHEL,可能会比这个操作系统的最小RAM需求(256MB)明显多得多的物理内存。由此可见,如果有那么多RAM内存可以使用,自然可以在同一台机器上运行其它服务。 3、my-large.ini是为专用于一个SQL数据</div> </li> <li><a href="/article/3405.htm" title="MFC和ado数据库使用时遇到的问题" target="_blank">MFC和ado数据库使用时遇到的问题</a> <span class="text-muted">你不认识的休道人</span> <a class="tag" taget="_blank" href="/search/sql/1.htm">sql</a><a class="tag" taget="_blank" href="/search/C%2B%2B/1.htm">C++</a><a class="tag" taget="_blank" href="/search/mfc/1.htm">mfc</a> <div>=================================================================== 第一个 =================================================================== try{ CString sql; sql.Format("select * from p</div> </li> <li><a href="/article/3532.htm" title="表单重复提交Double Submits" target="_blank">表单重复提交Double Submits</a> <span class="text-muted">rensanning</span> <a class="tag" taget="_blank" href="/search/double/1.htm">double</a> <div>可能发生的场景: *多次点击提交按钮 *刷新页面 *点击浏览器回退按钮 *直接访问收藏夹中的地址 *重复发送HTTP请求(Ajax) (1)点击按钮后disable该按钮一会儿,这样能避免急躁的用户频繁点击按钮。 这种方法确实有些粗暴,友好一点的可以把按钮的文字变一下做个提示,比如Bootstrap的做法: http://getbootstrap.co</div> </li> <li><a href="/article/3659.htm" title="Java String 十大常见问题" target="_blank">Java String 十大常见问题</a> <span class="text-muted">tomcat_oracle</span> <a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/1.htm">正则表达式</a> <div> 1.字符串比较,使用“==”还是equals()?   "=="判断两个引用的是不是同一个内存地址(同一个物理对象)。   equals()判断两个字符串的值是否相等。   除非你想判断两个string引用是否同一个对象,否则应该总是使用equals()方法。   如果你了解字符串的驻留(String Interning)则会更好地理解这个问题。    </div> </li> <li><a href="/article/3786.htm" title="SpringMVC 登陆拦截器实现登陆控制" target="_blank">SpringMVC 登陆拦截器实现登陆控制</a> <span class="text-muted">xp9802</span> <a class="tag" taget="_blank" href="/search/springMVC/1.htm">springMVC</a> <div>思路,先登陆后,将登陆信息存储在session中,然后通过拦截器,对系统中的页面和资源进行访问拦截,同时对于登陆本身相关的页面和资源不拦截。   实现方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 </div> </li> </ul> </div> </div> </div> <div> <div class="container"> <div class="indexes"> <strong>按字母分类:</strong> <a href="/tags/A/1.htm" target="_blank">A</a><a href="/tags/B/1.htm" target="_blank">B</a><a href="/tags/C/1.htm" target="_blank">C</a><a href="/tags/D/1.htm" target="_blank">D</a><a href="/tags/E/1.htm" target="_blank">E</a><a href="/tags/F/1.htm" target="_blank">F</a><a href="/tags/G/1.htm" target="_blank">G</a><a href="/tags/H/1.htm" target="_blank">H</a><a href="/tags/I/1.htm" target="_blank">I</a><a href="/tags/J/1.htm" target="_blank">J</a><a href="/tags/K/1.htm" target="_blank">K</a><a href="/tags/L/1.htm" target="_blank">L</a><a href="/tags/M/1.htm" target="_blank">M</a><a href="/tags/N/1.htm" target="_blank">N</a><a href="/tags/O/1.htm" target="_blank">O</a><a href="/tags/P/1.htm" target="_blank">P</a><a href="/tags/Q/1.htm" target="_blank">Q</a><a href="/tags/R/1.htm" target="_blank">R</a><a href="/tags/S/1.htm" target="_blank">S</a><a href="/tags/T/1.htm" target="_blank">T</a><a href="/tags/U/1.htm" target="_blank">U</a><a href="/tags/V/1.htm" target="_blank">V</a><a href="/tags/W/1.htm" target="_blank">W</a><a href="/tags/X/1.htm" target="_blank">X</a><a href="/tags/Y/1.htm" target="_blank">Y</a><a href="/tags/Z/1.htm" target="_blank">Z</a><a href="/tags/0/1.htm" target="_blank">其他</a> </div> </div> </div> <footer id="footer" class="mb30 mt30"> <div class="container"> <div class="footBglm"> <a target="_blank" href="/">首页</a> - <a target="_blank" href="/custom/about.htm">关于我们</a> - <a target="_blank" href="/search/Java/1.htm">站内搜索</a> - <a target="_blank" href="/sitemap.txt">Sitemap</a> - <a target="_blank" href="/custom/delete.htm">侵权投诉</a> </div> <div class="copyright">版权所有 IT知识库 CopyRight © 2000-2050 E-COM-NET.COM , All Rights Reserved. <!-- <a href="https://beian.miit.gov.cn/" rel="nofollow" target="_blank">京ICP备09083238号</a><br>--> </div> </div> </footer> <!-- 代码高亮 --> <script type="text/javascript" src="/static/syntaxhighlighter/scripts/shCore.js"></script> <script type="text/javascript" src="/static/syntaxhighlighter/scripts/shLegacy.js"></script> <script type="text/javascript" src="/static/syntaxhighlighter/scripts/shAutoloader.js"></script> <link type="text/css" rel="stylesheet" href="/static/syntaxhighlighter/styles/shCoreDefault.css"/> <script type="text/javascript" src="/static/syntaxhighlighter/src/my_start_1.js"></script> </body> </html>