vue parseHTML函数解析器遇到结束标签
作者:李李 发布时间:2023-07-02 16:56:09
引言
承接上篇 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 >= 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 基本形成
来源:https://zhuanlan.zhihu.com/p/90505278


猜你喜欢
- 第一种:import socket import fcntl import struct def get_ip_address(ifname
- 今天开始学习 YUI,加强一下对 JavaScript 的理解。1. 命名空间 YAHOO
- 目录Step 1: 生成spec文件Step 2: 使用pyinstaller 构建可执行文件虽然django项目我们一般通过部署服务器进行
- 简介:单例模式可以保证一个类仅有一个实例,并提供一个访问它的全局访问点。适用性于当类只能有一个实例而且客户可以从一个众所周知的访问点访问它,
- 数据库自增 ID搞一个数据库,什么也不干,就用于生成主键。你的系统里每次得到一个 id,都需要往那个专门生成主键的数据库中通过插入
- 昨晚在往MySQL数据库中插入一组数据时,出错了!数据库无情了给我报了个错误:ERROR 1365(22012):Division by 0
- 两个进程发生死锁的典型例子是:进程T1中获取锁A,申请锁B;进程T2中获取锁B,申请锁A,我们下面动手来演示一下这种情况:1. 创建一个Da
- 面试题有一个test.xml文件,要求读取该文件中products节点的所有子节点的值以及子节点的属性值。test.xml文件:<!-
- 下面是实现代码# coding:utf-8import time, serialfrom struct import *import bin
- 刷票一般要突破以下限制:1、验证码识别2、同一ip不可连续投票解决办法1、用tesseract工具,链接在此 https://code.go
- Go语言在进行文件操作的时候,可以有多种方法。最常见的比如直接对文件本身进行Read和Write; 除此之外,还可以使用bufio库的流式处
- 1 前言在咱们编写 SQL 语句操作数据库中的数据的时候,有可能会遇到一些不太爽的问题,例如对于同一字段拥有相同名称的记录,我们只需要显示一
- 如下所示:l = [1, 2, 3, 5]l_one = [2, 8, 6, 10]print set(l) & set(l_one
- 何为样本分布不均:样本分布不均衡就是指样本差异非常大,例如共1000条数据样本的数据集中,其中占有10条样本分类,其特征无论如何你和也无法实
- 这篇文章主要介绍了python操作openpyxl导出Excel 设置单元格格式及合并处理代码实例,文中通过示例代码介绍的非常详细,对大家的
- 有时候难免需要直接调用Shell命令来完成一些比较简单的操作,比如mount一个文件系统之类的。那么我们使用Python如何调用Linux的
- 关于电子邮件 大学之前,基本不用邮箱,所以基本感觉不到它的存在,也不知道有什么用;然而大学之后,随着认识的人越来越多,知识越来越广
- 学习爬虫有一段时间了,今天使用Scrapy框架将校花网的图片爬取到本地。Scrapy爬虫框架相对于使用requests库进行网页的爬取,拥有
- Django url pathDjango 路由在 urls.py 配置path('浏览器地址栏表示URL', '处
- 最近在学习python,动手做了一个自动填写日报的小工具;由于请求中包含时间,格式如:2016-08-04;所以就了解了一下python的时