详细介绍:《Vuejs设计与实现》第 16 章(解析器) 中

news/2025/9/20 19:38:21/文章来源:https://www.cnblogs.com/lxjshuju/p/19102762

目录

16.4 解析标签节点

16.5 解析属性


16.4 解析标签节点

在上一节给出的 parseElement 函数的实现中,无论是解析开始标签还是闭合标签,我们都调用了 parseTag 函数。
同时,我们使用 parseChildren 函数来解析开始标签与闭合标签中间的部分,如下代码所示:

function parseElement(context, ancestors) {
// 调用 parseTag 函数解析开始标签
const element = parseTag(context)
if (element.isSelfClosing) return element
ancestors.push(element)
element.children = parseChildren(context, ancestors)
ancestors.pop()
if (context.source.startsWith(`

标签节点的整个解析过程如下图:

image.png


这里需要注意的是,由于开始标签与结束标签的格式非常类似,所以我们统一使用 parseTag 函数处理,并通过该函数的第二个参数来指定具体的处理类型。当第二个参数值为字符串 'end' 时,意味着解析的是结束标签。
另外,无论处理的是开始标签还是结束标签,parseTag 函数都会消费对应的内容。为了实现对模板内容的消费,我们需要在上下文对象中新增两个工具函数,如下所示:

function parse(str) {
// 上下文对象
const context = {
// 模板内容
source: str,
mode: TextModes.DATA,
// advanceBy 函数用来消费指定数量的字符,它接收一个数字作为参数
advanceBy(num) {
// 根据给定字符数 num,截取位置 num 后的模板内容,并替换当前模板内容
context.source = context.source.slice(num)
},
// 无论是开始标签还是结束标签,都可能存在无用的空白字符,例如
advanceSpaces() {
// 匹配空白字符
const match = /^[\t\r\n\f ]+/.exec(context.source)
if (match) {
// 调用 advanceBy 函数消费空白字符
context.advanceBy(match[0].length)
}
},
}
const nodes = parseChildren(context, [])
return {
type: 'Root',
children: nodes,
}
}

上述代码,我们为上下文对象增加了 advanceBy 函数和 advanceSpaces 函数。
其中 advanceBy 函数用来消费指定数量的字符。其实现原理很简单,即调用字符串的 slice 函数,
advanceSpaces 函数则用来消费无用的空白字符,因为标签中可能存在空白字符,例如在模板 <div----> 中减号(-)代表空白字符。
有了 advanceBy 和 advanceSpaces 函数后,我们就可以给出 parseTag 函数的实现:

// 由于 parseTag 既用来处理开始标签,也用来处理结束标签,因此我们设计第二个参数 type,
// 用来代表当前处理的是开始标签还是结束标签,type 的默认值为 'start',即默认作为开始标签处理
function parseTag(context, type = 'start') {
// 从上下文对象中拿到 advanceBy 函数
const { advanceBy, advanceSpaces } = context
// 处理开始标签和结束标签的正则表达式不同
const match =
type === 'start'
? // 匹配开始标签
/^]*)/i.exec(context.source)
: // 匹配结束标签
/^]*)/i.exec(context.source)
// 匹配成功后,正则表达式的第一个捕获组的值就是标签名称
const tag = match[1]
// 消费正则表达式匹配的全部内容,例如 '' 开头,则说明这是一个自闭合标签
const isSelfClosing = context.source.startsWith('/>')
// 如果是自闭合标签,则消费 '/>', 否则消费 '>'
advanceBy(isSelfClosing ? 2 : 1)
// 返回标签节点
return {
type: 'Element',
// 标签名称
tag,
// 标签的属性暂时留空
props: [],
// 子节点留空
children: [],
// 是否自闭合
isSelfClosing,
}
}

上面这段代码有两个关键点。

  • 由于 parseTag 函数既用于解析开始标签,又用于解析结束标签,因此需要用一个参数来标识当前处理的标签类型,即 type。
  • 对于开始标签和结束标签,用于匹配它们的正则表达式只有一点不同:结束标签是以字符串 </ 开头的。

匹配开始和结束标签的正则含义,如下图:

image.png


该正则有一个补货组,可以捕获标签名称。

  • 对于字符串 '<div>',会匹配出字符串 '<div',剩余 '>'
  • 对于字符串 '<div/>',会匹配出字符串 '<div',剩余 '/>'
  • 对于字符串 '<div---->',其中减号(-)代表空白符,会匹配出字符串 '<div',剩余 '---->'

除了正则表达式外,parseTag 函数的另外几个关键点如下:

  • 在完成正则匹配后,需要调用 advanceBy 函数消费由正则匹配的全部内容。
  • 根据上面给出的第三个正则匹配例子可知,由于标签中可能存在无用的空白字符,例如 <div---->,因此我们需要调用 advanceSpaces 函数消费空白字符。
  • 在消费由正则匹配的内容后,需要检查剩余模板内容是否以字符串 /> 开头。如果是,则说明当前解析的是一个自闭合标签,这时需要将标签节点的 isSelfClosing 属性设置为 true。
  • 最后,判断标签是否自闭合。如果是,则调用 advnaceBy 函数消费内容 />,否则只需要消费内容 > 即可。

过上述处理后,parseTag 函数会返回一个标签节点。
parseElement 函数在得到由 parseTag 函数产生的标签节点后,需要根据节点的类型完成文本模式的切换,如下代码所示:

function parseElement(context, ancestors) {
const element = parseTag(context)
if (element.isSelfClosing) return element
// 切换到正确的文本模式
if (element.tag === 'textarea' || element.tag === 'title') {
// 如果由 parseTag 解析得到的标签是  或 ,则切换到 RCDATA 模式
context.mode = TextModes.RCDATA
} else if (/style|xmp|iframe|noembed|noframes|noscript/.test(element.tag)) {
// 如果由 parseTag 解析得到的标签是:
// 、、、、、
// 则切换到 RAWTEXT 模式
context.mode = TextModes.RAWTEXT
} else {
// 否则切换到 DATA 模式
context.mode = TextModes.DATA
}
ancestors.push(element)
element.children = parseChildren(context, ancestors)
ancestors.pop()
if (context.source.startsWith(`

如此,我们实现了对标签节点的解析。但是目前的实现忽略了节点中的属性和指令,下一节将会讲解。

16.5 解析属性

上一节中介绍的 parseTag 解析函数会消费整个开始标签,这意味着该函数需要有能力处理开始标签中存在属性与指令,例如:

为了处理上面标签属性,我们需要在 parseTag 函数中增加 parseAttributes 解析函数,如下代码所示:

function parseTag(context, type = 'start') {
const { advanceBy, advanceSpaces } = context
const match =
type === 'start'
? /^]*)/i.exec(context.source)
: /^]*)/i.exec(context.source)
const tag = match[1]
advanceBy(match[0].length)
advanceSpaces()
// 调用 parseAttributes 函数完成属性与指令的解析,并得到 props 数组,
// props 数组是由指令节点与属性节点共同组成的数组
const props = parseAttributes(context)
const isSelfClosing = context.source.startsWith('/>')
advanceBy(isSelfClosing ? 2 : 1)
return {
type: 'Element',
tag,
props, // 将 props 数组添加到标签节点上
children: [],
isSelfClosing,
}
}

我们需要在消费标签的“开始部分”和无用的空白字符之后,再调用 parseAttribute 函数。举个例子,假设标签的内容如下:

签的“开始部分”指的是字符串 <div,所以当消耗标签的“开始部分”以及无用空白字符后,剩下的内容为:

01 id="foo" v-show="display" >

上面这段内容才是 parseAttributes 函数要处理的内容。
由于该函数只用来解析属性和指令,因此它会不断地消费上面这段模板内容,直到遇到标签的“结束部分”为止。
其中,结束部分指的是字符 > 或者字符串 />。据此我们可以给出 parseAttributes 函数的整体框架,如下面的代码所示:

function parseAttributes(context) {
// 用来存储解析过程中产生的属性节点和指令节点
const props = []
// 开启 while 循环,不断地消费模板内容,直至遇到标签的“结束部分”为止
while (!context.source.startsWith('>') && !context.source.startsWith('/>')) {
// 解析属性或指令
}
// 将解析结果返回
return props
}

实际上,parseAttributes 函数消费模板内容的过程,就是不断地解析属性名称、等于号、属性值的过程,如图所示:

image.png


parseAttributes 函数会按照从左到右的顺序不断地消费字符串。以上图所示,该函数的解析过程如下:
首先,解析出第一个属性的名称 id,并消费字符串 'id'。此时剩余模板内容为:

="foo" v-show="display" >

在解析属性名称时,除了要消费属性名称之外,还要消费属性名称后面可能存在的空白字符。
如下面这段模板中,属性名称和等于号之间存在空白字符:

id  =  "foo" v-show="display" >

无论如何,在属性名称解析完毕之后,模板剩余内容一定是以等于号开头的,即:

=  "foo" v-show="display" >

如果消费属性名称之后,模板内容不以等于号开头,则说明模板内容不合法,我们可以选择性地抛出错误。
接着,我们需要消费等于号字符。
由于等于号和属性值之间也可能存在空白字符,所以我们也需要消费对应的空白字符。
在这一步操作过后,模板的剩余内容如下:

"foo" v-show="display" >

接下来,到了处理属性值的环节。模板中的属性值存在三种情况:

  • 属性值被双引号包裹:id="foo"。
  • 属性值被单引号包裹:id='foo'。
  • 属性值没有引号包裹:id=foo。

按照上述例子,此时模板的内容以双引号(")开头。
因此我们可以通过检查当前模板内容是否以引号开头来确定属性值是否被引用。
如果属性值被引号引用,则消费引号。此时模板的剩余内容为:

foo" v-show="display" >

既然属性值被引号引用了,就意味着在剩余模板内容中,下一个引号之前的内容都应该被解析为属性值。
在这个例子中,属性值的内容是字符串 foo。于是,我们消费属性值及其后面的引号。
当然,如果属性值没有被引号引用,那么在剩余模板内容中,下一个空白字符之前的所有字符都应该作为属性值。
当属性值和引号被消费之后,由于属性值与下一个属性名称之间可能存在空白字符,所以我们还要消费对应的空白字符。在这一步处理过后,剩余模板内容为:

v-show="display" >

经过上述操作之后,第一个属性就处理完毕了。
此时模板中还剩下一个指令,我们只需重新执行上述步骤,即可完成 v-show 指令的解析。
当 v-show 指令解析完毕后,将会遇到标签的“结束部分”,即字符 >。
parseAttributes 函数中的 while 循环将会停止,完成属性和指令的解析。
下面的 parseAttributes 函数给出了上述逻辑的具体实现:

function parseAttributes(context) {
const { advanceBy, advanceSpaces } = context
const props = []
while (!context.source.startsWith('>') && !context.source.startsWith('/>')) {
// 该正则用于匹配属性名称
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)
// 得到属性名称
const name = match[0]
// 消费属性名称
advanceBy(name.length)
// 消费属性名称与等于号之间的空白字符
advanceSpaces()
// 消费等于号
advanceBy(1)
// 消费等于号与属性值之间的空白字符
advanceSpaces()
// 属性值
let value = ''
// 获取当前模板内容的第一个字符
const quote = context.source[0]
// 判断属性值是否被引号引用
const isQuoted = quote === '"' || quote === "'"
if (isQuoted) {
// 属性值被引号引用,消费引号
advanceBy(1)
// 获取下一个引号的索引
const endQuoteIndex = context.source.indexOf(quote)
if (endQuoteIndex > -1) {
// 获取下一个引号之前的内容作为属性值
value = context.source.slice(0, endQuoteIndex)
// 消费属性值
advanceBy(value.length)
// 消费引号
advanceBy(1)
} else {
// 缺少引号错误
console.error('缺少引号')
}
} else {
// 代码运行到这里,说明属性值没有被引号引用
// 下一个空白字符之前的内容全部作为属性值
const match = /^[^\t\r\n\f >]+/.exec(context.source)
// 获取属性值
value = match[0]
// 消费属性值
advanceBy(value.length)
}
// 消费属性值后面的空白字符
advanceSpaces()
// 使用属性名称 + 属性值创建一个属性节点,添加到 props 数组中
props.push({
type: 'Attribute',
name,
value,
})
}
// 返回
return props
}

在上面这段代码中,有两个重要的正则表达式:

  • /^[^\t\r\n\f />][^\t\r\n\f />=]*/,用来匹配属性名称;
  • /^[^\t\r\n\f >]+/,用来匹配没有使用引号引用的属性值。

先来看看匹配属性名称的正则表达式原理:

image.png


上图,我们可以将这个正则表达式分为 A、B 两个部分来看:

  • 部分 A 用于匹配一个位置,这个位置不能是空白字符,也不能是字符 / 或字符 >,并且字符串要以该位置开头。
  • 部分 B 则用于匹配 0 个或多个位置,这些位置不能是空白字符,也不能是字符 /、>、=。注意,这些位置不允许出现等于号(=)字符,这就实现了只匹配等于号之前的内容,即属性名称。

我们再来看看匹配没有使用引号引用的属性值正则的原理:

image.png


该正则表达式从字符串的开始位置进行匹配,并且会匹配一个或多个非空白字符、非字符 >。
换句话说,该正则表达式会一直对字符串进行匹配,直到遇到空白字符或字符 > 为止,这就实现了属性值的提取。
配合 parseAttributes 函数,假设给出如下模板:

解析上面这段模板,将会得到如下 AST:

const ast = {
type: 'Root',
children: [
{
type: 'Element',
tag: 'div',
props: [
// 属性
{ type: 'Attribute', name: 'id', value: 'foo' },
{ type: 'Attribute', name: 'v-show', value: 'display' },
],
},
],
}

可以看到,在 div 标签节点的 props 属性中,包含两个类型为 Attribute 的节点,这两个节点就是 parseAttributes 函数的解析结果。

我们可以增加更多在 Vue.js 中常见的属性和指令进行测试,如以下模板所示:

上面这段模板经过解析后,得到如下 AST:

const ast = {
type: 'Root',
children: [
{
type: 'Element',
tag: 'div',
props: [
// 属性
{ type: 'Attribute', name: ':id', value: 'dynamicId' },
{ type: 'Attribute', name: '@click', value: 'handler' },
{ type: 'Attribute', name: 'v-on:mousedown', value: 'onMouseDown' },
],
},
],
}

可以看到,在类型为 Attribute 的属性节点中,其 name 字段完整地保留着模板中编写的属性名称。
我们可以对属性名称做进一步的分析,从而得到更具体的信息。
例如,属性名称以字符 @ 开头,则认为它是一个 v-on 指令绑定。我们甚至可以把以 v- 开头的属性看作指令绑定,从而为它赋予不同的节点类型,例如:

// 指令,类型为 Directive
{ type: 'Directive', name: 'v-on:mousedown', value: 'onMouseDown' }
{ type: 'Directive', name: '@click', value: 'handler' }
// 普通属性
{ type: 'Attribute', name: 'id', value: 'foo' }

不仅如此,为了得到更加具体的信息,我们甚至可以进一步分析指令节点的数据,也可以设计更多语法规则,这完全取决于框架设计者在语法层面的设计,以及为框架赋予的能力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/908520.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

spring项目部署后为什么会生成 logback-spring.xml记录

spring项目部署后为什么会生成 logback-spring.xml记录pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas&q…

React+antd搭建监听localStorage变化多页面更新+纯js单页面table模糊、精确查询、添加、展示功能

React+antd搭建监听localStorage变化多页面更新+纯js单页面table模糊、精确查询、添加、展示功能2025-09-20 19:22 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !im…

暗黑破坏神4 任务-坚守传统-向古老的雕像展示你坚守的传统

暗黑破坏神4 任务-坚守传统-向古老的雕像展示你坚守的传统 发一个“确定”的表情即可。

202509_NBWS_logbool

流量分析,正则匹配,布尔盲注,pyshark,Tags:流量分析,正则匹配,布尔盲注,pyshark,DASCTF 0x00. 题目 找到flag,格式为DASCTF{} 附件路径:https://pan.baidu.com/s/1GyH7kitkMYywGC9YJeQLJA?pwd=Zmxh#list/path=/CTF附…

Kubernetes权威指南-深入理解Pod Service

Pod是Kubernetes最小调度单元,将多个紧密协作的容器组合为一个逻辑主机,共享网络、存储与IP。通过YAML定义容器、卷、健康检查等配置,支持静态Pod、Init容器、ConfigMap等高级特性,并借助Service实现稳定的服务发现…

详细介绍:jeecg-boot3.7.0对接钉钉登录(OAuth2.0)

详细介绍:jeecg-boot3.7.0对接钉钉登录(OAuth2.0)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas&quo…

C++编程软件 Dev-C++ 安装及使用流程

目录第 1 步:下载 Dev-C++第 2 步:安装 Dev-C++第 3 步:第一次打开 Dev-C++ 时的配置第4步:编写一个 C++ 程序 第 1 步:下载 Dev-C++ 首先,我们需要找一个地方下载 Dev-C++ 软件。 可以直接点击 这个链接 下载。…

DLL植入漏洞分类与微软安全响应指南

本文详细解析微软对DLL植入漏洞的分类方法,涵盖应用程序目录、当前工作目录和PATH目录三种场景的威胁评估标准,并说明微软针对不同场景的安全响应策略与修复优先级。DLL植入漏洞的分类 | MSRC博客 本文章翻译自Secur…

4980:拯救行动

题目 总时间限制: 1000ms 内存限制: 65536kB 描述 公主被恶人抓走,被关押在牢房的某个地方。牢房用N*M (N, M <= 200)的矩阵来表示。矩阵中的每项可以代表道路(@)、墙壁(#)、和守卫(x)。 英勇的骑士(r)决定…

java03-wxj

好的,我们来逐一详细解答这些问题。1. 什么样的方法应该用static修饰?不用static修饰的方法往往具有什么特性? 应该使用 static 修饰的方法:工具方法(Utility Methods): 执行一个与任何特定对象实例无关的通用任…

题解:P13969 [VKOSHP 2024] Exchange and Deletion

题面: 我们考虑从图论意义计数,把 swap 改成连边,由于交换完前面的点直接被删了,所以只保留从后向前的连边。 那么最后连到 \(n-k\) 前的点的数值等于链头,而 \(n-k\) 后的点和链上非链头的点实际上都被删了。手玩…

基于MATLAB的车牌识别系统 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

市场交易反心理特征之二:忽视热点切换的苗头

案例:2017年8月18日,万科A。2017年8月18日,万科A 万科A小波段延续万科A一个波段完成​情况描述:从第一次触及无穷成本线止跌后大资金进入开始,连续2-3次出现买点,都能选出。但是都直接鼠标滑过,没有过脑子,显然…

Linux服务器上安装配置GitLab的步骤

在Linux服务器上安装GitLab是一个涉及多个步骤的过程。以下是详细的步骤,遵从GitLab官方推荐的做法以确保系统的稳定性和性能。 服务器要求和前提条件:一台运行支持的Linux操作系统的服务器,建议使用CentOS 7。 至少…

贪心算法应用:投资组合再平衡问题详解 - 实践

贪心算法应用:投资组合再平衡问题详解 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", &q…

MCP:Trae中集成Playwright 实现网页自动化测试

Trae IDE 可以通过智能问答的形式补齐代码,纠正程序中的错误,根据用户的自然语言,实现AI自动编程。近期使用了一下Trae,发现很强大。我把一个有前后端的项目导入Trae IDE,当时还有一些报错,但是很快在Trae 的提示…

C语言中的字符、字符串及内存操作函数详细讲解

在C语言中,字符和字符串的处理是基本且重要的概念。字符在C中通常由 char类型表示,而字符串则是以 null终止的字符数组。内存操作函数则提供了基本的内存处理能力,如复制、设置、比较等内存块。 字符操作 字符使用 …

06、訊息收集

1、使用nmap探测magedu.com开放的端口号和服务指纹 2、使用指纹识别工具探测magedu.com采用的建站模板 3、搜集magedu.com的子域名有哪些

AI 智能体与 Coze 工作流实践:小红书对标账号采集 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …