vue parseHTML函数解析器遇到结束标签

vue parseHTML函数解析器遇到结束标签

目录

引言

match函数匹配正则endTag

关键 parseEndTag 函数代码

总结parseEndTag 函数作用

handleStartTag函数后续

最后更新 stack 栈以及 lastTag

引言

承接上篇 parseHTML 函数源码解析拿到返回值后的处理

接下来我们将会讲解当 textEnd === 0 解析器遇到结束标签,parse 结束标签的代码如下:

// End tag: var endTagMatch = html.match(endTag); if (endTagMatch) { var curIndex = index; advance(endTagMatch[0].length); parseEndTag(endTagMatch[1], curIndex, index); continue } match函数匹配正则endTag

首先调用 html 字符串的 match 函数匹配正则 endTag ,将结果保存在常量endTagMatch中。正则 endTag 用来匹配结束标签,并且拥有一个捕获组用来捕获标签名字,比如有如下html 字符串:

<div></div>

endTagMatch 输出如下:

endTagMatch = [
  '</div>',
  'div'
]

第一个元素是整个匹配到的结束标签字符串

第二个元素是对应的标签名字。

如果匹配成功 if 语句块的代码将被执行,首先使用 curIndex 常量存储当前 index 的值,然后调用 advance 函数,并以 endTagMatch[0].length 作为参数,接着调用了 parseEndTag 函数对结束标签进行解析,传递给 parseEndTag 函数的三个参数分别是:标签名以及结束标签在 html 字符串中起始和结束的位置,最后调用 continue 语句结束此次循环。

关键 parseEndTag 函数代码

现在我们来讲解下关键 parseEndTag 函数代码如下:

function parseEndTag(tagName, start, end) { var pos, lowerCasedTagName; if (start == null) { start = index; } if (end == null) { end = index; } // Find the closest opened tag of the same type if (tagName) { lowerCasedTagName = tagName.toLowerCase(); for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { // If no tag name is provided, clean shop pos = 0; } if (pos >= 0) { // Close all the open elements, up the stack for (var i = stack.length - 1; i >= pos; i--) { if (i > pos || !tagName && options.warn ) { options.warn( ("tag <" + (stack[i].tag) + "> has no matching end tag.") ); } if (options.end) { options.end(stack[i].tag, start, end); } } // Remove the open elements from the stack stack.length = pos; lastTag = pos && stack[pos - 1].tag; } else if (lowerCasedTagName === 'br') { if (options.start) { options.start(tagName, [], true, start, end); } } else if (lowerCasedTagName === 'p') { if (options.start) { options.start(tagName, [], false, start, end); } if (options.end) { options.end(tagName, start, end); } } }

你需要知道 parseEndTag 函数调用之前已经获得到了结束标签的名字以及结束标签在html(template)字符串中的起始和结束位置。 但是这并不代表着 html parser 结束了。

为什么?

还记得我们之前讲的 stack 栈吗? 之前我们讲到通过stack可以检测是否有非一元标签是否微写闭合标签,接下来还会处理 stack 栈中剩余的标签。

除了这些功能之外,parseEndTag函数还会做一件事儿,如果你感兴趣你可以在任何html文件中写下如下内容:

<body>
  </br>
  </p>
</body>

上面的html片段中,我们分别写了</br>、</p>的结束标签,但注意我们并没有写起始标签,然后浏览器是能够正常解析他们的,其中 </br> 标签被正常解析为 <br> 标签,而</p>标签被正常解析为 <p></p> 。除了 br 与 p 其他任何标签如果你只写了结束标签那么浏览器都将会忽略。所以为了与浏览器的行为相同,parseEndTag 函数也需要专门处理br与p的结束标签,即:</br> 和</p>。

总结parseEndTag 函数作用

检测是否缺少闭合标签

处理 stack 栈中剩余的标签

解析</br> 与标签,与浏览器的行为相同

当一个函数拥有两个及以上功能的时候,最常用的技巧就是通过参数进行控制,还记得jQuery中的Access 吗?parseEndTag 函数接收三个参数,这三个参数其实都是可选的,根据传参的不同其功能也不同。

第一种是处理普通的结束标签,此时三个参数都传递

第二种是只传递第一个参数

第三种是不传递参数,处理 stack 栈剩余未处理的标签。

代码并不复杂我们一起来看下吧!

var pos, lowerCasedTagName; if (start == null) { start = index; } if (end == null) { end = index; }

定了两个变量:pos和 lowerCasedTagName,其中变量 pos 会在后面用于判断 html 字符串是否缺少结束标签,lowerCasedTagName 变量用来存储 tagName 的小写版。

接着是两句if 语句,当 start 和 end 不存在时,将这两个变量的值设置为当前字符流的读入位置,即index。

所以当我们看到这两个 if 语句时,我们就应该能够想到:parseEndTag 函数的第二个参数和第三个参数都是可选的。

其实这种使用 parseEndTag 函数的方式我们在handleStartTag 函数中见过,当时我们没有对其进行讲解一起来回顾下。

if (expectHTML) { if (lastTag === 'p' && isNonPhrasingTag(tagName)) { parseEndTag(lastTag) } if (canBeLeftOpenTag(tagName) && lastTag === tagName) { parseEndTag(tagName) } }

我们知道 lastTag 引用的是stack栈顶的元素,也就是最近(或者说上一次)遇到的开始标签,所以如下判断条件:

lastTag === 'p' && isNonPhrasingTag(tagName)

这里想表达的意思是:最近一次遇到的开始标签是 p 标签,并且当前正在解析的开始标签必须不能是段落式内容(Phrasing content)模型,这时候 if 语句块的代码才会执行,即调用parseEndTag(lastTag)。

首先大家要知道每一个 html 元素都拥有一个或多个内容模型(content model),其中p 标签本身的内容模型是流式内容(Flow content),并且 p 标签的特性是只允许包含段落式内容(Phrasing content)。

所以条件成立的情况如下:

<p><h1></h1></p>

 

在解析上面这段 html 字符串的时候,首先遇到p标签的开始标签,此时lastTag被设置为 p ,紧接着会遇到 h1 标签的开始标签,由于 h2 标签的内容模型属于非段落式内容(Phrasing content)模型,所以会立即调用 parseEndTag(lastTag) 函数闭合 p 标签,此时由于强行插入了</p> 标签,所以解析后的字符串将变为如下内容:

<p></p><h2></h2></p>

接着,继续解析该字符串,会遇到 <h2></h2> 标签并正常解析之,最后解析器会遇到一个单独的p 标签的结束标签,即:</p>。

这个时候就回到了我们前面讲过的,当解析器遇到 p 标签或者 br 标签的结束标签时会补全他们,最终<p><h2></h2></p> 这段 html 字符串将被解析为:

<p></p><h2></h2><p></p>

而这也就是浏览器的行为,以上是第一个if 分支的意义。还有第二个if分支,它的条件如下:

canBeLeftOpenTag(tagName) && lastTag === tagName

以上条件成立的意思是:当前正在解析的标签是一个可以省略结束标签的标签,并且与上一次解析到的开始标签相同,如下:

<p>max
<p>kaixin

p 标签是可以省略结束标签的标签,所以当解析到一个p标签的开始标签并且下一次遇到的标签也是p标签的开始标签时,会立即关闭第二个p标签。即调用:parseEndTag(tagName) 函数,然后由于第一个p标签缺少闭合标签所以会Vue会给你一个警告。

handleStartTag函数后续

接下来我们继续讲解handleStartTag函数后续的内容。

if (tagName) { lowerCasedTagName = tagName.toLowerCase(); for (pos = stack.length - 1; pos &gt;= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { // If no tag name is provided, clean shop pos = 0; }

如果tagName存在,lowerCasedTagName 获取的是 tagName 小写之后的值,接下来开启一个 for 循环从后向前遍历 stack 栈,直到找到相应的位置,并且该位置索引会保存到 pos 变量中,如果 tagName 不存在,则直接将 pos 设置为 0 。

开头我们讲到 pos 变量会被用来判断是否有元素缺少闭合标签。怎么做到的呢?看完下面的代码你就明白了。

if (pos >= 0) { // Close all the open elements, up the stack for (var i = stack.length - 1; i >= pos; i--) { if (i > pos || !tagName && options.warn ) { options.warn( ("tag <" + (stack[i].tag) + "> has no matching end tag.") ); } if (options.end) { options.end(stack[i].tag, start, end); } } // Remove the open elements from the stack stack.length = pos; lastTag = pos && stack[pos - 1].tag; } else if (lowerCasedTagName === 'br') { if (options.start) { options.start(tagName, [], true, start, end); } } else if (lowerCasedTagName === 'p') { if (options.start) { options.start(tagName, [], false, start, end); } if (options.end) { options.end(tagName, start, end); } }

上面代码由三部分组成,即if...else if...else if。首先我们查看 if 语句块,当 pos >= 0 的时候就会走 if 语句块。在 if 语句块内开启一个 for 循环,同样是从后向前遍历 stack 数组,如果发现 stack 数组中存在索引大于 pos 的元素,那么该元素一定是缺少闭合标签的,这个时候如果是在非生产环境那么 Vue 便会打印一句警告,告诉你缺少闭合标签。除了打印一句警告之外,随后会调用 options.end(stack[i].tag, start, end) 立即将其闭合,这是为了保证解析结果的正确性。

最后更新 stack 栈以及 lastTag stack.length = pos; lastTag = pos && stack[pos - 1].tag;

了解下剩下的两个else if:

if (pos >= 0) { // ... 省略 } else if (lowerCasedTagName === 'br') { if (options.start) { options.start(tagName, [], true, start, end) } } else if (lowerCasedTagName === 'p') { if (options.start) { options.start(tagName, [], false, start, end) } if (options.end) { options.end(tagName, start, end) } }

这两个else if 什么情况下成立呢?

当 tagName 没有在 stack 栈中找到对应的开始标签时,pos 为 -1 。

tagName为br 、p标签。

当你写了 br 标签的结束标签:</br> 或 p 标签的结束标签 </p> 时,解析器能够正常解析他们,其中对于 </br> 会将其解析为正常的 <br> 标签,而 </p> 标签也会正常解析为<p></p>。

可以发现对于 </br> 和 </p> 标签浏览器可以将其正常解析为 <br> 以及<p></p>,Vue 的 parser 与浏览器的行为是一致的。

现在我们还剩一个问题没有讲解,即parseEndTag是如何处理stack栈中剩余未处理的标签的。其实就是调用 parseEndTag() 函数时不传递任何参数,也就是说此时 tagName 参数也不存在。这个时候我们再次查看下面的代码:

由于 pos 为 0 ,所以 i >= pos 始终成立,这个时候 stack 栈中如果有剩余未处理的标签,则会逐个警告缺少闭合标签,并调用 options.end 将其闭合。

以上对于整个词法分析的过程重点部分就已经讲解完毕了,其实现方式就是通过读取字符流配合正则一点一点的解析字符串,直到整个字符串都被解析完毕为止。并且每当遇到一个特定的token 时都会调用相应的钩子函数,同时将有用的参数传递过去。比如每当遇到一个开始标签都会调用 options.start 钩子函数,遇到闭合标签调用 options.end 钩子函数。

下面我们来讲讲这两个重要的钩子函数,并且谈下AST的基本形成。

parseHTML 函数源码解析 AST 基本形成

以上就是vue parseHTML函数解析器遇到结束标签的详细内容,更多关于vue parseHTML函数的资料请关注易知道(ezd.cc)其它相关文章!

推荐阅读

    excel怎么用乘法函数

    excel怎么用乘法函数,乘法,函数,哪个,excel乘法函数怎么用?1、首先用鼠标选中要计算的单元格。2、然后选中单元格后点击左上方工具栏的fx公

    excel中乘法函数是什么?

    excel中乘法函数是什么?,乘法,函数,什么,打开表格,在C1单元格中输入“=A1*B1”乘法公式。以此类推到多个单元。1、A1*B1=C1的Excel乘法公式

    标准差excel用什么函数?

    标准差excel用什么函数?,函数,标准,什么,在数据单元格的下方输入l标准差公式函数公式“=STDEVPA(C2:C6)”。按下回车,求出标准公差值。详细

    excel常用函数都有哪些?

    excel常用函数都有哪些?,函数,哪些,常用,1、SUM函数:SUM函数的作用是求和。函数公式为=sum()例如:统计一个单元格区域:=sum(A1:A10)  统计多个

    标签尺寸设置|标签的尺寸

    标签尺寸设置|标签的尺寸,,1. 标签的尺寸有很多款尺寸的保鲜盒,容量;350ml,规格尺寸:135x102x52mm, 容量;800ml;规格尺寸:205x134x52mm, 容量;1.0L;

    个性标签在哪里设置|添加个性标签

    个性标签在哪里设置|添加个性标签,,1. 添加个性标签微信标签类似于qq印象,可以给自己的微信好友贴上一些个性标签,方面查找类似特性的好友。

    Safari+Glims快速切换标签页

    Safari+Glims快速切换标签页,,对于键盘控制的玩家来说,Chrome的命令+数字键可以方便地打开打开的标签页,但是Safari使用这个快捷方式打开书