Vuejs设计与实现10-解析器
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
十三、解析器
文本模式
文本模式指的是解析器在工作时所进入的一些特殊状态
解析器默认模式为 DATA根据不同的标签会触发不同的模式
- RCDATA 模式
<title> 标签、<textarea> 标签
- RAWTEXT 模式
<style>、<xmp>、<iframe>、<noembed>、<noframes>、<noscript> 等
- CDATA 模式
<![CDATA[ 字符串
DATA 模式
遇到字符 < 时会切换到标签开始状态
遇到字符 & 时会切换到字符引用状态
能够处理 HTML 字符实体
RCDATA 模式
遇到字符 < 时切换到 RCDATA less-than sign state 状态
遇到字符 /切换到 RCDATA 的结束标签状态
在不使用引用符号 & 的情况下RCDATA 模式不会识别标签如下代码
会把 < 当做普通符号而无法识别内部的 div 标签
<textarea>
<div>asdf</div>asdfasdf
</textarea>
RAWTEXT 模式
与 RCDATA 模式类似只是不支持 HTML 实体
CDATA
任何字符都作为普通字符处理直到遇到 CDATA 的结束标志为止
梯度下降算法构造 AST
模板解析器基本架构
// 定义文本模式作为一个状态表
const TextModes = {
DATA: "DATA",
RCDATA: "RCDATA",
RAWTEXT: "RAWTEXT",
CDATA: "CDATA",
};
// 解析器函数接收模板作为参数
function parse(str) {
// 定义上下文对象
const context = {
// source 是模板内容用于在解析过程中进行消费
source: str,
// 解析器当前处于文本模式初始模式为 DATA
mode: TextModes.DATA,
};
// 调用 parseChildren 函数开始进行解析它返回解析后得到的子节点
// parseChildren 函数接收两个参数
// 第一个参数是上下文对象 context
// 第二个参数是由父代节点构成的节点栈初始时栈为空
const nodes = parseChildren(context, []);
// 解析器返回 Root 根节点
return {
type: "Root",
// 使用 nodes 作为根节点的 children
children: nodes,
};
}
parserChildren
可视为一状态机状态数取决于子节点类型数
元素子节点可以有
- 标签节点例如
<div>
- 文本插值节点例如
{{ val }}
- 普通文本节点例如
text
- 注释节点例如
<!---->
- CDATA 节点例如
<![CDATA[ xxx ]]>
模板代码
function parseChildren(context, ancestors) {
// 定义 nodes 数组存储子节点它将作为最终的返回值
let nodes = [];
// 从上下文对象中取得当前状态包括模式 mode 和模板内容 source
const { mode, source } = context;
// 开启 while 循环只要满足条件就会一直对字符串进行解析
while (!isEnd(context, ancestors)) {
let node;
// 只有 DATA 模式和 RCDATA 模式才支持插值节点的解析
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
// 只有 DATA 模式才支持标签节点的解析
if (mode === TextModes.DATA && source[0] === "<") {
if (source[1] === "!") {
if (source.startsWith("<!--")) {
// 注释
node = parseComment(context);
} else if (source.startsWith("<![CDATA[")) {
// CDATA
node = parseCDATA(context, ancestors);
}
} else if (source[1] === "/") {
// 结束标签这里需要抛出错误后文会详细解释原因
} else if (/[a-z]/i.test(source[1])) {
// 标签
node = parseElement(context, ancestors);
}
} else if (source.startsWith("{{")) {
// 解析插值
node = parseInterpolation(context);
}
}
// node 不存在说明处于其他模式即非 DATA 模式且非 RCDATA 模式
// 这时一切内容都作为文本处理
if (!node) {
// 解析文本节点
node = parseText(context);
}
// 将节点添加到 nodes 数组中
nodes.push(node);
}
// 当 while 循环停止后说明子节点解析完毕返回子节点
return nodes;
}
parseElement 解析
该解析分为三个主要部分
- 解析开始标签 parseTag
- 解析子节点 parseChildren
- 解析结束标签 parseEndTag
状态机开启与关闭
在调用 parseElement 时会不断递归调用 parseChildren从而开启新的状态机
执行顺序请看以下情境
状态机 1 执行 parseElement递归时开启了新的状态机 2此时状态机 2 具有优先执行权限先执行完毕后再回到状态机 1 中继续执行以此类推
牢记以下俩规则
- 解析器遇到开始标签时会将该标签压入父级节点栈同时开启新的状态机。
- 当解析器遇到结束标签并且父级节点栈中存在与该标签同名的开始标签节点时会停止当前正在运行的状态机。
解析标签节点
标签节点解析的完整流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-62OpiExn-1673567853581)(…/imgs/vue/vuejs_optimize/vp3.png)]
parseTag 中使用正则表达式匹配开始和结束标签
匹配过程
- 正则匹配时需使用 advanceSpaces 函数消费无用的空白字符
- 消费时检测模板是否存在
/>
如果有则表示其为自闭合标签需要做出标注 - 完成正则匹配后需要调用 advanceBy 函数消费由正则匹配的全部内容
- 如果自闭合则 advanceBy 消费
/>
解析属性
在 parseTag 函数中增加 parseAttributes 函数来解析属性
function parseAttributes(context) {
// 用来存储解析过程中产生的属性节点和指令节点
const props = [];
// 开启 while 循环不断地消费模板内容直至遇到标签的“结束部分”为止
while (!context.source.startsWith(">") && !context.source.startsWith("/>")) {
// 解析属性或指令
}
// 将解析结果返回
return props;
}
两个重要的匹配属性的正则表达式
/^[^\t\r\n\f />][^\t\r\n\f />=]*/
用来匹配属性名称/^[^\t\r\n\f >]+/
用来匹配没有使用引号引用的属性值。
解析文本与 HTML 实体
解析文本
const template = '<div>Text</div>'
根据三要素走状态机即可完美处理
解码命名字符引用
<div>A<B</div>
如果不使用命名字符引用后会造成错误解析因为存在两个 <
符号
下面俩引用方式表示的结果均为小于号且均有对应的表来查
命名字符引用 <
数字字符引用 <
根据我们之前学得遇见&符号就进入字符引用状态往后读取如果再遇到#则进入数字字符引用否则进入命名字符引用
有些情况下命名字符引用后面没有分号此时就需要处理该情况
- 当存在分号时执行完整匹配
- 当省略分号时执行最短匹配。
解码数字字符引用
'&#'
则说明匹配的是以十进制表示的数字字符引用
'&#x'
则说明匹配的是以十六进制表示的数字字符引用