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/1835512920797179904.htm" title="element实现动态路由+面包屑" target="_blank">element实现动态路由+面包屑</a> <span class="text-muted">软件技术NINI</span> <a class="tag" taget="_blank" href="/search/vue%E6%A1%88%E4%BE%8B/1.htm">vue案例</a><a class="tag" taget="_blank" href="/search/vue.js/1.htm">vue.js</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a> <div>el-breadcrumb是ElementUI组件库中的一个面包屑导航组件,它用于显示当前页面的路径,帮助用户快速理解和导航到应用的各个部分。在Vue.js项目中,如果你已经安装了ElementUI,就可以很方便地使用el-breadcrumb组件。以下是一个基本的使用示例:安装ElementUI(如果你还没有安装的话):你可以通过npm或yarn来安装ElementUI。bash复制代码npmi</div> </li> <li><a href="/article/1835511912843014144.htm" title="理解Gunicorn:Python WSGI服务器的基石" target="_blank">理解Gunicorn:Python WSGI服务器的基石</a> <span class="text-muted">范范0825</span> <a class="tag" taget="_blank" href="/search/ipython/1.htm">ipython</a><a class="tag" taget="_blank" href="/search/linux/1.htm">linux</a><a class="tag" taget="_blank" href="/search/%E8%BF%90%E7%BB%B4/1.htm">运维</a> <div>理解Gunicorn:PythonWSGI服务器的基石介绍Gunicorn,全称GreenUnicorn,是一个为PythonWSGI(WebServerGatewayInterface)应用设计的高效、轻量级HTTP服务器。作为PythonWeb应用部署的常用工具,Gunicorn以其高性能和易用性著称。本文将介绍Gunicorn的基本概念、安装和配置,帮助初学者快速上手。1.什么是Gunico</div> </li> <li><a href="/article/1835509897106649088.htm" title="Long类型前后端数据不一致" target="_blank">Long类型前后端数据不一致</a> <span class="text-muted">igotyback</span> <a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a> <div>响应给前端的数据浏览器控制台中response中看到的Long类型的数据是正常的到前端数据不一致前后端数据类型不匹配是一个常见问题,尤其是当后端使用Java的Long类型(64位)与前端JavaScript的Number类型(最大安全整数为2^53-1,即16位)进行数据交互时,很容易出现精度丢失的问题。这是因为JavaScript中的Number类型无法安全地表示超过16位的整数。为了解决这个问</div> </li> <li><a href="/article/1835504218178416640.htm" title="Google earth studio 简介" target="_blank">Google earth studio 简介</a> <span class="text-muted">陟彼高冈yu</span> <a class="tag" taget="_blank" href="/search/%E6%97%85%E6%B8%B8/1.htm">旅游</a> <div>GoogleEarthStudio是一个基于Web的动画工具,专为创作使用GoogleEarth数据的动画和视频而设计。它利用了GoogleEarth强大的三维地图和卫星影像数据库,使用户能够轻松地创建逼真的地球动画、航拍视频和动态地图可视化。网址为https://www.google.com/earth/studio/。GoogleEarthStudio是一个基于Web的动画工具,专为创作使用G</div> </li> <li><a href="/article/1835502578050363392.htm" title="PHP环境搭建详细教程" target="_blank">PHP环境搭建详细教程</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/php/1.htm">php</a> <div>PHP是一个流行的服务器端脚本语言,广泛用于Web开发。为了使PHP能够在本地或服务器上运行,我们需要搭建一个合适的PHP环境。本教程将结合最新资料,介绍在不同操作系统上搭建PHP开发环境的多种方法,包括Windows、macOS和Linux系统的安装步骤,以及本地和Docker环境的配置。1.PHP环境搭建概述PHP环境的搭建主要分为以下几类:集成开发环境:例如XAMPP、WAMP、MAMP,这</div> </li> <li><a href="/article/1835498925755297792.htm" title="DIV+CSS+JavaScript技术制作网页(旅游主题网页设计与制作)云南大理" target="_blank">DIV+CSS+JavaScript技术制作网页(旅游主题网页设计与制作)云南大理</a> <span class="text-muted">STU学生网页设计</span> <a class="tag" taget="_blank" href="/search/%E7%BD%91%E9%A1%B5%E8%AE%BE%E8%AE%A1/1.htm">网页设计</a><a class="tag" taget="_blank" href="/search/%E6%9C%9F%E6%9C%AB%E7%BD%91%E9%A1%B5%E4%BD%9C%E4%B8%9A/1.htm">期末网页作业</a><a class="tag" taget="_blank" href="/search/html%E9%9D%99%E6%80%81%E7%BD%91%E9%A1%B5/1.htm">html静态网页</a><a class="tag" taget="_blank" href="/search/html5%E6%9C%9F%E6%9C%AB%E5%A4%A7%E4%BD%9C%E4%B8%9A/1.htm">html5期末大作业</a><a class="tag" taget="_blank" href="/search/%E7%BD%91%E9%A1%B5%E8%AE%BE%E8%AE%A1/1.htm">网页设计</a><a class="tag" taget="_blank" href="/search/web%E5%A4%A7%E4%BD%9C%E4%B8%9A/1.htm">web大作业</a> <div>️精彩专栏推荐作者主页:【进入主页—获取更多源码】web前端期末大作业:【HTML5网页期末作业(1000套)】程序员有趣的告白方式:【HTML七夕情人节表白网页制作(110套)】文章目录二、网站介绍三、网站效果▶️1.视频演示2.图片演示四、网站代码HTML结构代码CSS样式代码五、更多源码二、网站介绍网站布局方面:计划采用目前主流的、能兼容各大主流浏览器、显示效果稳定的浮动网页布局结构。网站程</div> </li> <li><a href="/article/1835497792265613312.htm" title="【加密社】Solidity 中的事件机制及其应用" target="_blank">【加密社】Solidity 中的事件机制及其应用</a> <span class="text-muted">加密社</span> <a class="tag" taget="_blank" href="/search/%E9%97%B2%E4%BE%83/1.htm">闲侃</a><a class="tag" taget="_blank" href="/search/%E5%8C%BA%E5%9D%97%E9%93%BE/1.htm">区块链</a><a class="tag" taget="_blank" href="/search/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/1.htm">智能合约</a><a class="tag" taget="_blank" href="/search/%E5%8C%BA%E5%9D%97%E9%93%BE/1.htm">区块链</a> <div>加密社引言在Solidity合约开发过程中,事件(Events)是一种非常重要的机制。它们不仅能够让开发者记录智能合约的重要状态变更,还能够让外部系统(如前端应用)监听这些状态的变化。本文将详细介绍Solidity中的事件机制以及如何利用不同的手段来触发、监听和获取这些事件。事件存储的地方当我们在Solidity合约中使用emit关键字触发事件时,该事件会被记录在区块链的交易收据中。具体而言,事件</div> </li> <li><a href="/article/1835496149843275776.htm" title="关于城市旅游的HTML网页设计——(旅游风景云南 5页)HTML+CSS+JavaScript" target="_blank">关于城市旅游的HTML网页设计——(旅游风景云南 5页)HTML+CSS+JavaScript</a> <span class="text-muted">二挡起步</span> <a class="tag" taget="_blank" href="/search/web%E5%89%8D%E7%AB%AF%E6%9C%9F%E6%9C%AB%E5%A4%A7%E4%BD%9C%E4%B8%9A/1.htm">web前端期末大作业</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/html/1.htm">html</a><a class="tag" taget="_blank" href="/search/css/1.htm">css</a><a class="tag" taget="_blank" href="/search/%E6%97%85%E6%B8%B8/1.htm">旅游</a><a class="tag" taget="_blank" href="/search/%E9%A3%8E%E6%99%AF/1.htm">风景</a> <div>⛵源码获取文末联系✈Web前端开发技术描述网页设计题材,DIV+CSS布局制作,HTML+CSS网页设计期末课程大作业|游景点介绍|旅游风景区|家乡介绍|等网站的设计与制作|HTML期末大学生网页设计作业,Web大学生网页HTML:结构CSS:样式在操作方面上运用了html5和css3,采用了div+css结构、表单、超链接、浮动、绝对定位、相对定位、字体样式、引用视频等基础知识JavaScrip</div> </li> <li><a href="/article/1835496148601761792.htm" title="HTML网页设计制作大作业(div+css) 云南我的家乡旅游景点 带文字滚动" target="_blank">HTML网页设计制作大作业(div+css) 云南我的家乡旅游景点 带文字滚动</a> <span class="text-muted">二挡起步</span> <a class="tag" taget="_blank" href="/search/web%E5%89%8D%E7%AB%AF%E6%9C%9F%E6%9C%AB%E5%A4%A7%E4%BD%9C%E4%B8%9A/1.htm">web前端期末大作业</a><a class="tag" taget="_blank" href="/search/web%E8%AE%BE%E8%AE%A1%E7%BD%91%E9%A1%B5%E8%A7%84%E5%88%92%E4%B8%8E%E8%AE%BE%E8%AE%A1/1.htm">web设计网页规划与设计</a><a class="tag" taget="_blank" href="/search/html/1.htm">html</a><a class="tag" taget="_blank" href="/search/css/1.htm">css</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/dreamweaver/1.htm">dreamweaver</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a> <div>Web前端开发技术描述网页设计题材,DIV+CSS布局制作,HTML+CSS网页设计期末课程大作业游景点介绍|旅游风景区|家乡介绍|等网站的设计与制作HTML期末大学生网页设计作业HTML:结构CSS:样式在操作方面上运用了html5和css3,采用了div+css结构、表单、超链接、浮动、绝对定位、相对定位、字体样式、引用视频等基础知识JavaScript:做与用户的交互行为文章目录前端学习路线</div> </li> <li><a href="/article/1835495170972413952.htm" title="git - Webhook让部署自动化" target="_blank">git - Webhook让部署自动化</a> <span class="text-muted">大猪大猪</span> <div>我们现在有一个需求,将项目打包上传到gitlab或者github后,程序能自动部署,不用手动地去服务器中进行项目更新并运行,如何做到?这里我们可以使用gitlab与github的挂钩,挂钩的原理就是,每当我们有请求到gitlab与github服务器时,这时他俩会根据我们配置的挂钩地扯进行访问,webhook挂钩程序会一直监听着某个端口请求,一但收到他们发过来的请求,这时就知道用户有请求提交了,这时</div> </li> <li><a href="/article/1835493267907637248.htm" title="webpack图片等资源的处理" target="_blank">webpack图片等资源的处理</a> <span class="text-muted">dmengmeng</span> <div>需要的loaderfile-loader(让我们可以引入这些资源文件)url-loader(其实是file-loader的二次封装)img-loader(处理图片所需要的)在没有使用任何处理图片的loader之前,比如说css中用到了背景图片,那么最后打包会报错的,因为他没办法处理图片。其实你只想能够使用图片的话。只加一个file-loader就可以,打开网页能准确看到图片。{test:/\.(p</div> </li> <li><a href="/article/1835492740536823808.htm" title="node.js学习" target="_blank">node.js学习</a> <span class="text-muted">小猿L</span> <a class="tag" taget="_blank" href="/search/node.js/1.htm">node.js</a><a class="tag" taget="_blank" href="/search/node.js/1.htm">node.js</a><a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/vim/1.htm">vim</a> <div>node.js学习实操及笔记温故node.js,node.js学习实操过程及笔记~node.js学习视频node.js官网node.js中文网实操笔记githubcsdn笔记为什么学node.js可以让别人访问我们编写的网页为后续的框架学习打下基础,三大框架vuereactangular离不开node.jsnode.js是什么官网:node.js是一个开源的、跨平台的运行JavaScript的运行</div> </li> <li><a href="/article/1835470931783413760.htm" title="「豆包Marscode体验官」 | 云端 IDE 启动 & Rust 体验" target="_blank">「豆包Marscode体验官」 | 云端 IDE 启动 & Rust 体验</a> <span class="text-muted">张风捷特烈</span> <a class="tag" taget="_blank" href="/search/ide/1.htm">ide</a><a class="tag" taget="_blank" href="/search/rust/1.htm">rust</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%90%8E%E7%AB%AF/1.htm">后端</a> <div>theme:cyanosis我正在参加「豆包MarsCode初体验」征文活动MarsCode可以看作一个运行在服务端的远程VSCode开发环境。对于我这种想要学习体验某些语言,但不想在电脑里装环境的人来说非常友好。本文就来介绍一下在MarsCode里,我的体验rust开发体验。一、MarsCode是什么它的本质是:提供代码助手和云端IDE服务的web网站,可通过下面的链接访问https://www</div> </li> <li><a href="/article/1835455048277127168.htm" title="Python神器!WEB自动化测试集成工具 DrissionPage" target="_blank">Python神器!WEB自动化测试集成工具 DrissionPage</a> <span class="text-muted">亚丁号</span> <a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a> <div>一、前言用requests做数据采集面对要登录的网站时,要分析数据包、JS源码,构造复杂的请求,往往还要应付验证码、JS混淆、签名参数等反爬手段,门槛较高。若数据是由JS计算生成的,还须重现计算过程,体验不好,开发效率不高。使用浏览器,可以很大程度上绕过这些坑,但浏览器运行效率不高。因此,这个库设计初衷,是将它们合而为一,能够在不同须要时切换相应模式,并提供一种人性化的使用方法,提高开发和运行效率</div> </li> <li><a href="/article/1835454921990828032.htm" title="Java爬虫框架(一)--架构设计" target="_blank">Java爬虫框架(一)--架构设计</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/%E6%A1%86%E6%9E%B6/1.htm">框架</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E4%BB%BB%E5%8A%A1/1.htm">任务</a><a class="tag" taget="_blank" href="/search/html%E8%A7%A3%E6%9E%90%E5%99%A8/1.htm">html解析器</a><a class="tag" taget="_blank" href="/search/%E5%AD%98%E5%82%A8/1.htm">存储</a><a class="tag" taget="_blank" href="/search/%E7%94%B5%E5%AD%90%E5%95%86%E5%8A%A1/1.htm">电子商务</a> <div>一、架构图那里搜网络爬虫框架主要针对电子商务网站进行数据爬取,分析,存储,索引。爬虫:爬虫负责爬取,解析,处理电子商务网站的网页的内容数据库:存储商品信息索引:商品的全文搜索索引Task队列:需要爬取的网页列表Visited表:已经爬取过的网页列表爬虫监控平台:web平台可以启动,停止爬虫,管理爬虫,task队列,visited表。二、爬虫1.流程1)Scheduler启动爬虫器,TaskMast</div> </li> <li><a href="/article/1835454543471669248.htm" title="Java:爬虫框架" target="_blank">Java:爬虫框架</a> <span class="text-muted">dingcho</span> <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/%E7%88%AC%E8%99%AB/1.htm">爬虫</a> <div>一、ApacheNutch2【参考地址】Nutch是一个开源Java实现的搜索引擎。它提供了我们运行自己的搜索引擎所需的全部工具。包括全文搜索和Web爬虫。Nutch致力于让每个人能很容易,同时花费很少就可以配置世界一流的Web搜索引擎.为了完成这一宏伟的目标,Nutch必须能够做到:每个月取几十亿网页为这些网页维护一个索引对索引文件进行每秒上千次的搜索提供高质量的搜索结果简单来说Nutch支持分</div> </li> <li><a href="/article/1835451016456269824.htm" title="MongoDB知识概括" target="_blank">MongoDB知识概括</a> <span class="text-muted">GeorgeLin98</span> <a class="tag" taget="_blank" href="/search/%E6%8C%81%E4%B9%85%E5%B1%82/1.htm">持久层</a><a class="tag" taget="_blank" href="/search/mongodb/1.htm">mongodb</a> <div>MongoDB知识概括MongoDB相关概念单机部署基本常用命令索引-IndexSpirngDataMongoDB集成副本集分片集群安全认证MongoDB相关概念业务应用场景:传统的关系型数据库(如MySQL),在数据操作的“三高”需求以及应对Web2.0的网站需求面前,显得力不从心。解释:“三高”需求:①Highperformance-对数据库高并发读写的需求。②HugeStorage-对海量数</div> </li> <li><a href="/article/1835448238103162880.htm" title="springboot+vue项目实战一-创建SpringBoot简单项目" target="_blank">springboot+vue项目实战一-创建SpringBoot简单项目</a> <span class="text-muted">苹果酱0567</span> <a class="tag" taget="_blank" href="/search/%E9%9D%A2%E8%AF%95%E9%A2%98%E6%B1%87%E6%80%BB%E4%B8%8E%E8%A7%A3%E6%9E%90/1.htm">面试题汇总与解析</a><a class="tag" taget="_blank" href="/search/spring/1.htm">spring</a><a class="tag" taget="_blank" href="/search/boot/1.htm">boot</a><a class="tag" taget="_blank" href="/search/%E5%90%8E%E7%AB%AF/1.htm">后端</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E4%B8%AD%E9%97%B4%E4%BB%B6/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>这段时间抽空给女朋友搭建一个个人博客,想着记录一下建站的过程,就当做笔记吧。虽然复制zjblog只要一个小时就可以搞定一个网站,或者用cms系统,三四个小时就可以做出一个前后台都有的网站,而且想做成啥样也都行。但是就是要从新做,自己做的意义不一样,更何况,俺就是专门干这个的,嘿嘿嘿要做一个网站,而且从零开始,首先呢就是技术选型了,经过一番思量决定选择-SpringBoot做后端,前端使用Vue做一</div> </li> <li><a href="/article/1835448239864770560.htm" title="JavaScript 中,深拷贝(Deep Copy)和浅拷贝(Shallow Copy)" target="_blank">JavaScript 中,深拷贝(Deep Copy)和浅拷贝(Shallow Copy)</a> <span class="text-muted">跳房子的前端</span> <a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95/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><a class="tag" taget="_blank" href="/search/ecmascript/1.htm">ecmascript</a> <div>在JavaScript中,深拷贝(DeepCopy)和浅拷贝(ShallowCopy)是用于复制对象或数组的两种不同方法。了解它们的区别和应用场景对于避免潜在的bugs和高效地处理数据非常重要。以下是对深拷贝和浅拷贝的详细解释,包括它们的概念、用途、优缺点以及实现方式。1.浅拷贝(ShallowCopy)概念定义:浅拷贝是指创建一个新的对象或数组,其中包含了原对象或数组的基本数据类型的值和对引用数</div> </li> <li><a href="/article/1835443823287824384.htm" title="Python实现下载当前年份的谷歌影像" target="_blank">Python实现下载当前年份的谷歌影像</a> <span class="text-muted">sand&wich</span> <a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a> <div>在GIS项目和地图应用中,获取最新的地理影像数据是非常重要的。本文将介绍如何使用Python代码从Google地图自动下载当前年份的影像数据,并将其保存为高分辨率的TIFF格式文件。这个过程涉及地理坐标转换、多线程下载和图像处理。关键功能该脚本的核心功能包括:坐标转换:支持WGS-84与WebMercator投影之间转换,以及处理中国GCJ-02偏移。自动化下载:多线程下载地图瓦片,提高效率。图像</div> </li> <li><a href="/article/1835443569968640000.htm" title="Spring MVC 全面指南:从入门到精通的详细解析" target="_blank">Spring MVC 全面指南:从入门到精通的详细解析</a> <span class="text-muted">一杯梅子酱</span> <a class="tag" taget="_blank" href="/search/%E6%8A%80%E6%9C%AF%E6%A0%88%E5%AD%A6%E4%B9%A0/1.htm">技术栈学习</a><a class="tag" taget="_blank" href="/search/spring/1.htm">spring</a><a class="tag" taget="_blank" href="/search/mvc/1.htm">mvc</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a> <div>引言:SpringMVC,作为Spring框架的一个重要模块,为构建Web应用提供了强大的功能和灵活性。无论是初学者还是有一定经验的开发者,掌握SpringMVC都将显著提升你的Web开发技能。本文旨在为初学者提供一个全面且易于理解的学习路径,通过详细的知识点分析和实际案例,帮助你快速上手SpringMVC,让学习过程既深刻又高效。一、SpringMVC简介1.1什么是SpringMVC?Spri</div> </li> <li><a href="/article/1835438028768768000.htm" title="Spring Boot中实现跨域请求" target="_blank">Spring Boot中实现跨域请求</a> <span class="text-muted">BABA8891</span> <a class="tag" taget="_blank" href="/search/spring/1.htm">spring</a><a class="tag" taget="_blank" href="/search/boot/1.htm">boot</a><a class="tag" taget="_blank" href="/search/%E5%90%8E%E7%AB%AF/1.htm">后端</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a> <div>在SpringBoot中实现跨域请求(CORS,Cross-OriginResourceSharing)可以通过多种方式,以下是几种常见的方法:1.使用@CrossOrigin注解在SpringBoot中,你可以在控制器或者具体的请求处理方法上使用@CrossOrigin注解来允许跨域请求。在控制器上应用:importorg.springframework.web.bind.annotation.</div> </li> <li><a href="/article/1835438028009598976.htm" title="WebMagic:强大的Java爬虫框架解析与实战" target="_blank">WebMagic:强大的Java爬虫框架解析与实战</a> <span class="text-muted">Aaron_945</span> <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/%E7%88%AC%E8%99%AB/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>文章目录引言官网链接WebMagic原理概述基础使用1.添加依赖2.编写PageProcessor高级使用1.自定义Pipeline2.分布式抓取优点结论引言在大数据时代,网络爬虫作为数据收集的重要工具,扮演着不可或缺的角色。Java作为一门广泛使用的编程语言,在爬虫开发领域也有其独特的优势。WebMagic是一个开源的Java爬虫框架,它提供了简单灵活的API,支持多线程、分布式抓取,以及丰富的</div> </li> <li><a href="/article/1835437775344726016.htm" title="博客网站制作教程" target="_blank">博客网站制作教程</a> <span class="text-muted">2401_85194651</span> <a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/maven/1.htm">maven</a> <div>首先就是技术框架:后端:Java+SpringBoot数据库:MySQL前端:Vue.js数据库连接:JPA(JavaPersistenceAPI)1.项目结构blog-app/├──backend/│├──src/main/java/com/example/blogapp/││├──BlogApplication.java││├──config/│││└──DatabaseConfig.java</div> </li> <li><a href="/article/1835435506645692416.htm" title="00. 这里整理了最全的爬虫框架(Java + Python)" target="_blank">00. 这里整理了最全的爬虫框架(Java + Python)</a> <span class="text-muted">有一只柴犬</span> <a class="tag" taget="_blank" href="/search/%E7%88%AC%E8%99%AB%E7%B3%BB%E5%88%97/1.htm">爬虫系列</a><a class="tag" taget="_blank" href="/search/%E7%88%AC%E8%99%AB/1.htm">爬虫</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/python/1.htm">python</a> <div>目录1、前言2、什么是网络爬虫3、常见的爬虫框架3.1、java框架3.1.1、WebMagic3.1.2、Jsoup3.1.3、HttpClient3.1.4、Crawler4j3.1.5、HtmlUnit3.1.6、Selenium3.2、Python框架3.2.1、Scrapy3.2.2、BeautifulSoup+Requests3.2.3、Selenium3.2.4、PyQuery3.2</div> </li> <li><a href="/article/1835428948339683328.htm" title="JavaScript `Map` 和 `WeakMap`详细解释" target="_blank">JavaScript `Map` 和 `WeakMap`详细解释</a> <span class="text-muted">跳房子的前端</span> <a class="tag" taget="_blank" href="/search/JavaScript/1.htm">JavaScript</a><a class="tag" taget="_blank" href="/search/%E5%8E%9F%E7%94%9F%E6%96%B9%E6%B3%95/1.htm">原生方法</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><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a> <div>在JavaScript中,Map和WeakMap都是用于存储键值对的数据结构,但它们有一些关键的不同之处。MapMap是一种可以存储任意类型的键值对的集合。它保持了键值对的插入顺序,并且可以通过键快速查找对应的值。Map提供了一些非常有用的方法和属性来操作这些数据对:set(key,value):将一个键值对添加到Map中。如果键已经存在,则更新其对应的值。get(key):获取指定键的值。如果键</div> </li> <li><a href="/article/1835428317084348416.htm" title="最简单将静态网页挂载到服务器上(不用nginx)" target="_blank">最简单将静态网页挂载到服务器上(不用nginx)</a> <span class="text-muted">全能全知者</span> <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/nginx/1.htm">nginx</a><a class="tag" taget="_blank" href="/search/%E8%BF%90%E7%BB%B4/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/html/1.htm">html</a><a class="tag" taget="_blank" href="/search/%E7%AC%94%E8%AE%B0/1.htm">笔记</a> <div>最简单将静态网页挂载到服务器上(不用nginx)如果随便弄个静态网页挂在服务器都要用nignx就太麻烦了,所以直接使用Apache来搭建一些简单前端静态网页会相对方便很多检查Web服务器服务状态:sudosystemctlstatushttpd#ApacheWeb服务器如果发现没有安装web服务器:安装Apache:sudoyuminstallhttpd启动Apache:sudosystemctl</div> </li> <li><a href="/article/1835427057752961024.htm" title="补充元象二面" target="_blank">补充元象二面</a> <span class="text-muted">Redstone Monstrosity</span> <a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/%E9%9D%A2%E8%AF%95/1.htm">面试</a> <div>1.请尽可能详细地说明,防抖和节流的区别,应用场景?你的回答中不要写出示例代码。防抖(Debounce)和节流(Throttle)是两种常用的前端性能优化技术,它们的主要区别在于如何处理高频事件的触发。以下是防抖和节流的区别和应用场景的详细说明:防抖和节流的定义防抖:在一段时间内,多次执行变为只执行最后一次。防抖的原理是,当事件被触发后,设置一个延迟定时器。如果在这个延迟时间内事件再次被触发,则重</div> </li> <li><a href="/article/1835420753252675584.htm" title="微信小程序开发注意事项" target="_blank">微信小程序开发注意事项</a> <span class="text-muted">jun778895</span> <a class="tag" taget="_blank" href="/search/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/1.htm">微信小程序</a><a class="tag" taget="_blank" href="/search/%E5%B0%8F%E7%A8%8B%E5%BA%8F/1.htm">小程序</a> <div>微信小程序开发是一个融合了前端开发、用户体验设计、后端服务(可选)以及微信小程序平台特性的综合性项目。这里,我将详细介绍一个典型的小程序开发项目的全过程,包括项目规划、设计、开发、测试及部署上线等各个环节,并尽量使内容达到或超过2000字的要求。一、项目规划1.1项目背景与目标假设我们要开发一个名为“智慧校园助手”的微信小程序,旨在为学生提供一站式校园生活服务,包括课程表查询、图书馆座位预约、食堂</div> </li> <li><a href="/article/1835419870070665216.htm" title="切换淘宝最新npm镜像源是" target="_blank">切换淘宝最新npm镜像源是</a> <span class="text-muted">hai40587</span> <a class="tag" taget="_blank" href="/search/npm/1.htm">npm</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/node.js/1.htm">node.js</a> <div>切换淘宝最新npm镜像源是一个相对简单的过程,但首先需要明确当前淘宝npm镜像源的状态和最新的镜像地址。由于网络环境和服务更新,镜像源的具体地址可能会发生变化,因此,我将基于当前可获取的信息,提供一个通用的切换步骤,并附上最新的镜像地址(截至回答时)。一、了解npm镜像源npm(NodePackageManager)是JavaScript的包管理器,用于安装、更新和管理项目依赖。由于npm官方仓库</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>