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 &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 基本形成

来源:https://zhuanlan.zhihu.com/p/90505278

标签:vue,parseHTML,函数,解析器,结束标签
0
投稿

猜你喜欢

  • Dreamweaver技巧50问

    2008-10-16 14:00:00
  • Python Selenium参数配置方法解析

    2023-12-28 09:20:02
  • python爬虫之百度API调用方法

    2021-11-18 17:17:13
  • SQLite Delete详解及实例代码

    2024-01-12 17:15:46
  • Python爬虫实现爬取百度百科词条功能实例

    2021-01-09 06:23:30
  • Python Django框架url反向解析实现动态生成对应的url链接示例

    2021-08-23 04:30:38
  • 爬虫框架 Feapder 和 Scrapy 的对比分析

    2022-03-31 00:02:17
  • 多按钮共存——don’t make me think

    2010-01-11 20:20:00
  • SuperSocket 信息: (SpnRegister) : Error 1355。解决方法

    2024-01-17 22:54:02
  • Mysql中Insert into xxx on duplicate key update问题

    2024-01-23 22:32:36
  • Python 解析XML文件

    2021-12-08 02:24:46
  • 解析Python 偏函数用法全方位实现

    2023-12-22 00:06:03
  • Python图片检索之以图搜图

    2021-08-11 12:08:23
  • MySQL数据库学习之排序与单行处理函数详解

    2024-01-22 13:01:01
  • PHP实现视频文件上传完整实例

    2024-06-05 09:46:55
  • Python实现二叉堆

    2023-12-24 13:40:48
  • Python三元运算实现方法

    2021-12-27 06:02:52
  • 使用CSS3和RGBa创建超酷的按钮

    2009-06-02 12:41:00
  • MySql学习笔记之事务隔离级别详解

    2024-01-21 23:54:28
  • 牢不可破的九宫格布局

    2009-07-24 12:40:00
  • asp之家 网络编程 m.aspxhome.com